/**
 * This code is mostly from the old Etherpad. Please help us to comment this code.
 * This helps other people to understand this code better and helps them to improve it.
 * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
 */

/**
 * Copyright 2009 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS-IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
var _, $, jQuery, plugins, Ace2Common;
var browser = require('./browser');
if(browser.msie){
  // Honestly fuck IE royally.
  // Basically every hack we have since V11 causes a problem
  if(parseInt(browser.version) >= 11){
    delete browser.msie;
    browser.chrome = true;
    browser.modernIE = true;
  }
}

Ace2Common = require('./ace2_common');

plugins = require('ep_etherpad-lite/static/js/pluginfw/client_plugins');
$ = jQuery = require('./rjquery').$;
_ = require("./underscore");

var isNodeText = Ace2Common.isNodeText,
  getAssoc = Ace2Common.getAssoc,
  setAssoc = Ace2Common.setAssoc,
  isTextNode = Ace2Common.isTextNode,
  binarySearchInfinite = Ace2Common.binarySearchInfinite,
  htmlPrettyEscape = Ace2Common.htmlPrettyEscape,
  noop = Ace2Common.noop;
var hooks = require('./pluginfw/hooks');

function Ace2Inner(){

  var makeChangesetTracker = require('./changesettracker').makeChangesetTracker;
  var colorutils = require('./colorutils').colorutils;
  var makeContentCollector = require('./contentcollector').makeContentCollector;
  var makeCSSManager = require('./cssmanager').makeCSSManager;
  var domline = require('./domline').domline;
  var AttribPool = require('./AttributePool');
  var Changeset = require('./Changeset');
  var ChangesetUtils = require('./ChangesetUtils');
  var linestylefilter = require('./linestylefilter').linestylefilter;
  var SkipList = require('./skiplist');
  var undoModule = require('./undomodule').undoModule;
  var AttributeManager = require('./AttributeManager');
  var Scroll = require('./scroll');

  var DEBUG = false; //$$ build script replaces the string "var DEBUG=true;//$$" with "var DEBUG=false;"
  // changed to false
  var isSetUp = false;

  var THE_TAB = '    '; //4
  var MAX_LIST_LEVEL = 16;

  var LINE_NUMBER_PADDING_RIGHT = 4;
  var LINE_NUMBER_PADDING_LEFT = 4;
  var MIN_LINEDIV_WIDTH = 20;
  var EDIT_BODY_PADDING_TOP = 8;
  var EDIT_BODY_PADDING_LEFT = 8;

  var FORMATTING_STYLES = ['bold', 'italic', 'underline', 'strikethrough'];
  var SELECT_BUTTON_CLASS = 'selected';

  var caughtErrors = [];

  var thisAuthor = '';

  var disposed = false;
  var editorInfo = parent.editorInfo;


  var iframe = window.frameElement;
  var outerWin = iframe.ace_outerWin;
  iframe.ace_outerWin = null; // prevent IE 6 memory leak
  var sideDiv = iframe.nextSibling;
  var lineMetricsDiv = sideDiv.nextSibling;
  initLineNumbers();

  var scroll = Scroll.init(outerWin);

  var outsideKeyDown = noop;

  var outsideKeyPress = function(){return true;};

  var outsideNotifyDirty = noop;

  // selFocusAtStart -- determines whether the selection extends "backwards", so that the focus
  // point (controlled with the arrow keys) is at the beginning; not supported in IE, though
  // native IE selections have that behavior (which we try not to interfere with).
  // Must be false if selection is collapsed!
  var rep = {
    lines: new SkipList(),
    selStart: null,
    selEnd: null,
    selFocusAtStart: false,
    alltext: "",
    alines: [],
    apool: new AttribPool()
  };

  // lines, alltext, alines, and DOM are set up in init()
  if (undoModule.enabled)
  {
    undoModule.apool = rep.apool;
  }

  var root, doc; // set in init()
  var isEditable = true;
  var doesWrap = true;
  var hasLineNumbers = true;
  var isStyled = true;

  // space around the innermost iframe element
  var iframePadLeft = MIN_LINEDIV_WIDTH + LINE_NUMBER_PADDING_RIGHT + EDIT_BODY_PADDING_LEFT;
  var iframePadTop = EDIT_BODY_PADDING_TOP;
  var iframePadBottom = 0,
      iframePadRight = 0;

  var console = (DEBUG && window.console);
  var documentAttributeManager;

  if (!window.console)
  {
    var names = ["log", "debug", "info", "warn", "error", "assert", "dir", "dirxml", "group", "groupEnd", "time", "timeEnd", "count", "trace", "profile", "profileEnd"];
    console = {};
    for (var i = 0; i < names.length; ++i)
    console[names[i]] = noop;
    //console.error = function(str) { alert(str); };
  }

  var PROFILER = window.PROFILER;
  if (!PROFILER)
  {
    PROFILER = function()
    {
      return {
        start: noop,
        mark: noop,
        literal: noop,
        end: noop,
        cancel: noop
      };
    };
  }

  // "dmesg" is for displaying messages in the in-page output pane
  // visible when "?djs=1" is appended to the pad URL.  It generally
  // remains a no-op unless djs is enabled, but we make a habit of
  // only calling it in error cases or while debugging.
  var dmesg = noop;
  window.dmesg = noop;

  var scheduler = parent; // hack for opera required

  var textFace = 'monospace';
  var textSize = 12;


  function textLineHeight()
  {
    return Math.round(textSize * 4 / 3);
  }

  var dynamicCSS = null;
  var outerDynamicCSS = null;
  var parentDynamicCSS = null;

  function initDynamicCSS()
  {
    dynamicCSS = makeCSSManager("dynamicsyntax");
    outerDynamicCSS = makeCSSManager("dynamicsyntax", "outer");
    parentDynamicCSS = makeCSSManager("dynamicsyntax", "parent");
  }

  var changesetTracker = makeChangesetTracker(scheduler, rep.apool, {
    withCallbacks: function(operationName, f)
    {
      inCallStackIfNecessary(operationName, function()
      {
        fastIncorp(1);
        f(
        {
          setDocumentAttributedText: function(atext)
          {
            setDocAText(atext);
          },
          applyChangesetToDocument: function(changeset, preferInsertionAfterCaret)
          {
            var oldEventType = currentCallStack.editEvent.eventType;
            currentCallStack.startNewEvent("nonundoable");

            performDocumentApplyChangeset(changeset, preferInsertionAfterCaret);

            currentCallStack.startNewEvent(oldEventType);
          }
        });
      });
    }
  });

  var authorInfos = {}; // presence of key determines if author is present in doc

  function getAuthorInfos(){
    return authorInfos;
  };
  editorInfo.ace_getAuthorInfos= getAuthorInfos;

  function setAuthorStyle(author, info)
  {
    if (!dynamicCSS) {
      return;
    }
    var authorSelector = getAuthorColorClassSelector(getAuthorClassName(author));

    var authorStyleSet = hooks.callAll('aceSetAuthorStyle', {
      dynamicCSS: dynamicCSS,
      parentDynamicCSS: parentDynamicCSS,
      outerDynamicCSS: outerDynamicCSS,
      info: info,
      author: author,
      authorSelector: authorSelector,
    });

    // Prevent default behaviour if any hook says so
    if (_.any(authorStyleSet, function(it) { return it }))
    {
      return
    }

    if (!info)
    {
      dynamicCSS.removeSelectorStyle(authorSelector);
      parentDynamicCSS.removeSelectorStyle(authorSelector);
    }
    else
    {
      if (info.bgcolor)
      {
        var bgcolor = info.bgcolor;
        if ((typeof info.fade) == "number")
        {
          bgcolor = fadeColor(bgcolor, info.fade);
        }

        var authorStyle = dynamicCSS.selectorStyle(authorSelector);
        var parentAuthorStyle = parentDynamicCSS.selectorStyle(authorSelector);
        var anchorStyle = dynamicCSS.selectorStyle(authorSelector + ' > a')

        // author color
        authorStyle.backgroundColor = bgcolor;
        parentAuthorStyle.backgroundColor = bgcolor;

        // text contrast
        if(colorutils.luminosity(colorutils.css2triple(bgcolor)) < 0.5)
        {
          authorStyle.color = '#ffffff';
          parentAuthorStyle.color = '#ffffff';
        }else{
          authorStyle.color = null;
          parentAuthorStyle.color = null;
        }

        // anchor text contrast
        if(colorutils.luminosity(colorutils.css2triple(bgcolor)) < 0.55)
        {
          anchorStyle.color = colorutils.triple2css(colorutils.complementary(colorutils.css2triple(bgcolor)));
        }else{
          anchorStyle.color = null;
        }
      }
    }
  }

  function setAuthorInfo(author, info)
  {
    if ((typeof author) != "string")
    {
      throw new Error("setAuthorInfo: author (" + author + ") is not a string");
    }
    if (!info)
    {
      delete authorInfos[author];
    }
    else
    {
      authorInfos[author] = info;
    }
    setAuthorStyle(author, info);
  }

  function getAuthorClassName(author)
  {
    return "author-" + author.replace(/[^a-y0-9]/g, function(c)
    {
      if (c == ".") return "-";
      return 'z' + c.charCodeAt(0) + 'z';
    });
  }

  function className2Author(className)
  {
    if (className.substring(0, 7) == "author-")
    {
      return className.substring(7).replace(/[a-y0-9]+|-|z.+?z/g, function(cc)
      {
        if (cc == '-') return '.';
        else if (cc.charAt(0) == 'z')
        {
          return String.fromCharCode(Number(cc.slice(1, -1)));
        }
        else
        {
          return cc;
        }
      });
    }
    return null;
  }

  function getAuthorColorClassSelector(oneClassName)
  {
    return ".authorColors ." + oneClassName;
  }

  function setUpTrackingCSS()
  {
    if (dynamicCSS)
    {
      var backgroundHeight = lineMetricsDiv.offsetHeight;
      var lineHeight = textLineHeight();
      var extraBodding = 0;
      var extraTodding = 0;
      if (backgroundHeight < lineHeight)
      {
        extraBodding = Math.ceil((lineHeight - backgroundHeight) / 2);
        extraTodding = lineHeight - backgroundHeight - extraBodding;
      }
      var spanStyle = dynamicCSS.selectorStyle("#innerdocbody span");
      spanStyle.paddingTop = extraTodding + "px";
      spanStyle.paddingBottom = extraBodding + "px";
    }
  }

  function fadeColor(colorCSS, fadeFrac)
  {
    var color = colorutils.css2triple(colorCSS);
    color = colorutils.blend(color, [1, 1, 1], fadeFrac);
    return colorutils.triple2css(color);
  }

  editorInfo.ace_getRep = function()
  {
    return rep;
  };

  editorInfo.ace_getAuthor = function()
  {
    return thisAuthor;
  }

  var _nonScrollableEditEvents = {
    "applyChangesToBase": 1
  };

  _.each(hooks.callAll('aceRegisterNonScrollableEditEvents'), function(eventType) {
      _nonScrollableEditEvents[eventType] = 1;
  });

  function isScrollableEditEvent(eventType)
  {
    return !_nonScrollableEditEvents[eventType];
  }

  var currentCallStack = null;

  function inCallStack(type, action)
  {
    if (disposed) return;

    if (currentCallStack)
    {
      console.error("Can't enter callstack " + type + ", already in " + currentCallStack.type);
    }

    var profiling = false;

    function profileRest()
    {
      profiling = true;
      console.profile();
    }

    function newEditEvent(eventType)
    {
      return {
        eventType: eventType,
        backset: null
      };
    }

    function submitOldEvent(evt)
    {
      if (rep.selStart && rep.selEnd)
      {
        var selStartChar = rep.lines.offsetOfIndex(rep.selStart[0]) + rep.selStart[1];
        var selEndChar = rep.lines.offsetOfIndex(rep.selEnd[0]) + rep.selEnd[1];
        evt.selStart = selStartChar;
        evt.selEnd = selEndChar;
        evt.selFocusAtStart = rep.selFocusAtStart;
      }
      if (undoModule.enabled)
      {
        var undoWorked = false;
        try
        {
          if (isPadLoading(evt.eventType))
          {
            undoModule.clearHistory();
          }
          else if (evt.eventType == "nonundoable")
          {
            if (evt.changeset)
            {
              undoModule.reportExternalChange(evt.changeset);
            }
          }
          else
          {
            undoModule.reportEvent(evt);
          }
          undoWorked = true;
        }
        finally
        {
          if (!undoWorked)
          {
            undoModule.enabled = false; // for safety
          }
        }
      }
    }

    function startNewEvent(eventType, dontSubmitOld)
    {
      var oldEvent = currentCallStack.editEvent;
      if (!dontSubmitOld)
      {
        submitOldEvent(oldEvent);
      }
      currentCallStack.editEvent = newEditEvent(eventType);
      return oldEvent;
    }

    currentCallStack = {
      type: type,
      docTextChanged: false,
      selectionAffected: false,
      userChangedSelection: false,
      domClean: false,
      profileRest: profileRest,
      isUserChange: false,
      // is this a "user change" type of call-stack
      repChanged: false,
      editEvent: newEditEvent(type),
      startNewEvent: startNewEvent
    };
    var cleanExit = false;
    var result;
    try
    {
      result = action();

      hooks.callAll('aceEditEvent', {
        callstack: currentCallStack,
        editorInfo: editorInfo,
        rep: rep,
        documentAttributeManager: documentAttributeManager
      });

      //console.log("Just did action for: "+type);
      cleanExit = true;
    }
    catch (e)
    {
      caughtErrors.push(
      {
        error: e,
        time: +new Date()
      });
      dmesg(e.toString());
      throw e;
    }
    finally
    {
      var cs = currentCallStack;
      //console.log("Finished action for: "+type);
      if (cleanExit)
      {
        submitOldEvent(cs.editEvent);
        if (cs.domClean && cs.type != "setup")
        {
          // if (cs.isUserChange)
          // {
          //  if (cs.repChanged) parenModule.notifyChange();
          //  else parenModule.notifyTick();
          // }
          if (cs.selectionAffected)
          {
            updateBrowserSelectionFromRep();
          }
          if ((cs.docTextChanged || cs.userChangedSelection) && isScrollableEditEvent(cs.type))
          {
            scrollSelectionIntoView();
          }
          if (cs.docTextChanged && cs.type.indexOf("importText") < 0)
          {
            outsideNotifyDirty();
          }
        }
      }
      else
      {
        // non-clean exit
        if (currentCallStack.type == "idleWorkTimer")
        {
          idleWorkTimer.atLeast(1000);
        }
      }
      currentCallStack = null;
      if (profiling) console.profileEnd();
    }
    return result;
  }
  editorInfo.ace_inCallStack = inCallStack;

  function inCallStackIfNecessary(type, action)
  {
    if (!currentCallStack)
    {
      inCallStack(type, action);
    }
    else
    {
      action();
    }
  }
  editorInfo.ace_inCallStackIfNecessary = inCallStackIfNecessary;

  function dispose()
  {
    disposed = true;
    if (idleWorkTimer) idleWorkTimer.never();
    teardown();
  }

  function checkALines()
  {
    return; // disable for speed


    function error()
    {
      throw new Error("checkALines");
    }
    if (rep.alines.length != rep.lines.length())
    {
      error();
    }
    for (var i = 0; i < rep.alines.length; i++)
    {
      var aline = rep.alines[i];
      var lineText = rep.lines.atIndex(i).text + "\n";
      var lineTextLength = lineText.length;
      var opIter = Changeset.opIterator(aline);
      var alineLength = 0;
      while (opIter.hasNext())
      {
        var o = opIter.next();
        alineLength += o.chars;
        if (opIter.hasNext())
        {
          if (o.lines !== 0) error();
        }
        else
        {
          if (o.lines != 1) error();
        }
      }
      if (alineLength != lineTextLength)
      {
        error();
      }
    }
  }

  function setWraps(newVal)
  {
    doesWrap = newVal;
    var dwClass = "doesWrap";
    setClassPresence(root, "doesWrap", doesWrap);
    scheduler.setTimeout(function()
    {
      inCallStackIfNecessary("setWraps", function()
      {
        fastIncorp(7);
        recreateDOM();
        fixView();
      });
    }, 0);

    // Chrome can't handle the truth..  If CSS rule white-space:pre-wrap
    // is true then any paste event will insert two lines..
    // Sadly this will mean you get a walking Caret in Chrome when clicking on a URL
    // So this has to be set to pre-wrap ;(
    // We need to file a bug w/ the Chromium team.
    if(browser.chrome){
      $("#innerdocbody").addClass("noprewrap");
    }

  }

  function setStyled(newVal)
  {
    var oldVal = isStyled;
    isStyled = !! newVal;

    if (newVal != oldVal)
    {
      if (!newVal)
      {
        // clear styles
        inCallStackIfNecessary("setStyled", function()
        {
          fastIncorp(12);
          var clearStyles = [];
          for (var k in STYLE_ATTRIBS)
          {
            clearStyles.push([k, '']);
          }
          performDocumentApplyAttributesToCharRange(0, rep.alltext.length, clearStyles);
        });
      }
    }
  }

  function setTextFace(face)
  {
    textFace = face;
    root.style.fontFamily = textFace;
    lineMetricsDiv.style.fontFamily = textFace;
    scheduler.setTimeout(function()
    {
      setUpTrackingCSS();
    }, 0);
  }

  function setTextSize(size)
  {
    textSize = size;
    root.style.fontSize = textSize + "px";
    root.style.lineHeight = textLineHeight() + "px";
    sideDiv.style.lineHeight = textLineHeight() + "px";
    lineMetricsDiv.style.fontSize = textSize + "px";
    scheduler.setTimeout(function()
    {
      setUpTrackingCSS();
    }, 0);
  }

  function recreateDOM()
  {
    // precond: normalized
    recolorLinesInRange(0, rep.alltext.length);
  }

  function setEditable(newVal)
  {
    isEditable = newVal;

    // the following may fail, e.g. if iframe is hidden
    if (!isEditable)
    {
      setDesignMode(false);
    }
    else
    {
      setDesignMode(true);
    }
    setClassPresence(root, "static", !isEditable);
  }

  function enforceEditability()
  {
    setEditable(isEditable);
  }

  function importText(text, undoable, dontProcess)
  {
    var lines;
    if (dontProcess)
    {
      if (text.charAt(text.length - 1) != "\n")
      {
        throw new Error("new raw text must end with newline");
      }
      if (/[\r\t\xa0]/.exec(text))
      {
        throw new Error("new raw text must not contain CR, tab, or nbsp");
      }
      lines = text.substring(0, text.length - 1).split('\n');
    }
    else
    {
      lines = _.map(text.split('\n'), textify);
    }
    var newText = "\n";
    if (lines.length > 0)
    {
      newText = lines.join('\n') + '\n';
    }

    inCallStackIfNecessary("importText" + (undoable ? "Undoable" : ""), function()
    {
      setDocText(newText);
    });

    if (dontProcess && rep.alltext != text)
    {
      throw new Error("mismatch error setting raw text in importText");
    }
  }

  function importAText(atext, apoolJsonObj, undoable)
  {
    atext = Changeset.cloneAText(atext);
    if (apoolJsonObj)
    {
      var wireApool = (new AttribPool()).fromJsonable(apoolJsonObj);
      atext.attribs = Changeset.moveOpsToNewPool(atext.attribs, wireApool, rep.apool);
    }
    inCallStackIfNecessary("importText" + (undoable ? "Undoable" : ""), function()
    {
      setDocAText(atext);
    });
  }

  function setDocAText(atext)
  {
    fastIncorp(8);

    var oldLen = rep.lines.totalWidth();
    var numLines = rep.lines.length();
    var upToLastLine = rep.lines.offsetOfIndex(numLines - 1);
    var lastLineLength = rep.lines.atIndex(numLines - 1).text.length;
    var assem = Changeset.smartOpAssembler();
    var o = Changeset.newOp('-');
    o.chars = upToLastLine;
    o.lines = numLines - 1;
    assem.append(o);
    o.chars = lastLineLength;
    o.lines = 0;
    assem.append(o);
    Changeset.appendATextToAssembler(atext, assem);
    var newLen = oldLen + assem.getLengthChange();
    var changeset = Changeset.checkRep(
    Changeset.pack(oldLen, newLen, assem.toString(), atext.text.slice(0, -1)));
    performDocumentApplyChangeset(changeset);

    performSelectionChange([0, rep.lines.atIndex(0).lineMarker], [0, rep.lines.atIndex(0).lineMarker]);

    idleWorkTimer.atMost(100);

    if (rep.alltext != atext.text)
    {
      dmesg(htmlPrettyEscape(rep.alltext));
      dmesg(htmlPrettyEscape(atext.text));
      throw new Error("mismatch error setting raw text in setDocAText");
    }
  }

  function setDocText(text)
  {
    setDocAText(Changeset.makeAText(text));
  }

  function getDocText()
  {
    var alltext = rep.alltext;
    var len = alltext.length;
    if (len > 0) len--; // final extra newline
    return alltext.substring(0, len);
  }

  function exportText()
  {
    if (currentCallStack && !currentCallStack.domClean)
    {
      inCallStackIfNecessary("exportText", function()
      {
        fastIncorp(2);
      });
    }
    return getDocText();
  }

  function editorChangedSize()
  {
    fixView();
  }

  function setOnKeyPress(handler)
  {
    outsideKeyPress = handler;
  }

  function setOnKeyDown(handler)
  {
    outsideKeyDown = handler;
  }

  function setNotifyDirty(handler)
  {
    outsideNotifyDirty = handler;
  }

  function getFormattedCode()
  {
    if (currentCallStack && !currentCallStack.domClean)
    {
      inCallStackIfNecessary("getFormattedCode", incorporateUserChanges);
    }
    var buf = [];
    if (rep.lines.length() > 0)
    {
      // should be the case, even for empty file
      var entry = rep.lines.atIndex(0);
      while (entry)
      {
        var domInfo = entry.domInfo;
        buf.push((domInfo && domInfo.getInnerHTML()) || domline.processSpaces(domline.escapeHTML(entry.text), doesWrap) || '&nbsp;' /*empty line*/ );
        entry = rep.lines.next(entry);
      }
    }
    return '<div class="syntax"><div>' + buf.join('</div>\n<div>') + '</div></div>';
  }

  var CMDS = {
    clearauthorship: function(prompt)
    {
      if ((!(rep.selStart && rep.selEnd)) || isCaret())
      {
        if (prompt)
        {
          prompt();
        }
        else
        {
          performDocumentApplyAttributesToCharRange(0, rep.alltext.length, [
            ['author', '']
          ]);
        }
      }
      else
      {
        setAttributeOnSelection('author', '');
      }
    }
  };

  function execCommand(cmd)
  {
    cmd = cmd.toLowerCase();
    var cmdArgs = Array.prototype.slice.call(arguments, 1);
    if (CMDS[cmd])
    {
      inCallStackIfNecessary(cmd, function()
      {
        fastIncorp(9);
        CMDS[cmd].apply(CMDS, cmdArgs);
      });
    }
  }

  function replaceRange(start, end, text)
  {
    inCallStackIfNecessary('replaceRange', function()
    {
      fastIncorp(9);
      performDocumentReplaceRange(start, end, text);
    });
  }

  editorInfo.ace_focus = focus;
  editorInfo.ace_importText = importText;
  editorInfo.ace_importAText = importAText;
  editorInfo.ace_exportText = exportText;
  editorInfo.ace_editorChangedSize = editorChangedSize;
  editorInfo.ace_setOnKeyPress = setOnKeyPress;
  editorInfo.ace_setOnKeyDown = setOnKeyDown;
  editorInfo.ace_setNotifyDirty = setNotifyDirty;
  editorInfo.ace_dispose = dispose;
  editorInfo.ace_getFormattedCode = getFormattedCode;
  editorInfo.ace_setEditable = setEditable;
  editorInfo.ace_execCommand = execCommand;
  editorInfo.ace_replaceRange = replaceRange;
  editorInfo.ace_getAuthorInfos= getAuthorInfos;
  editorInfo.ace_performDocumentReplaceRange = performDocumentReplaceRange;
  editorInfo.ace_performDocumentReplaceCharRange = performDocumentReplaceCharRange;
  editorInfo.ace_renumberList = renumberList;
  editorInfo.ace_doReturnKey = doReturnKey;
  editorInfo.ace_isBlockElement = isBlockElement;
  editorInfo.ace_getLineListType = getLineListType;

  editorInfo.ace_callWithAce = function(fn, callStack, normalize)
  {
    var wrapper = function()
    {
      return fn(editorInfo);
    };

    if (normalize !== undefined)
    {
      var wrapper1 = wrapper;
      wrapper = function()
      {
        editorInfo.ace_fastIncorp(9);
        wrapper1();
      };
    }

    if (callStack !== undefined)
    {
      return editorInfo.ace_inCallStack(callStack, wrapper);
    }
    else
    {
      return wrapper();
    }
  };

  // This methed exposes a setter for some ace properties
  // @param key the name of the parameter
  // @param value the value to set to
  editorInfo.ace_setProperty = function(key, value)
  {

    // Convinience function returning a setter for a class on an element
    var setClassPresenceNamed = function(element, cls){
      return function(value){
         setClassPresence(element, cls, !! value)
      }
    };

    // These properties are exposed
    var setters = {
      wraps: setWraps,
      showsauthorcolors: setClassPresenceNamed(root, "authorColors"),
      showsuserselections: setClassPresenceNamed(root, "userSelections"),
      showslinenumbers : function(value){
        hasLineNumbers = !! value;
        // disable line numbers on mobile devices
        if (browser.mobile) hasLineNumbers = false;
        setClassPresence(sideDiv, "sidedivhidden", !hasLineNumbers);
        fixView();
      },
      grayedout: setClassPresenceNamed(outerWin.document.body, "grayedout"),
      dmesg: function(){ dmesg = window.dmesg = value; },
      userauthor: function(value){
        thisAuthor = String(value);
        documentAttributeManager.author = thisAuthor;
      },
      styled: setStyled,
      textface: setTextFace,
      textsize: setTextSize,
      rtlistrue: function(value) {
        setClassPresence(root, "rtl", value)
        setClassPresence(root, "ltr", !value)
        document.documentElement.dir = value? 'rtl' : 'ltr'
      }
    };

    var setter = setters[key.toLowerCase()];

    // check if setter is present
    if(setter !== undefined){
      setter(value)
    }
  };

  editorInfo.ace_setBaseText = function(txt)
  {
    changesetTracker.setBaseText(txt);
  };
  editorInfo.ace_setBaseAttributedText = function(atxt, apoolJsonObj)
  {
    setUpTrackingCSS();
    changesetTracker.setBaseAttributedText(atxt, apoolJsonObj);
  };
  editorInfo.ace_applyChangesToBase = function(c, optAuthor, apoolJsonObj)
  {
    changesetTracker.applyChangesToBase(c, optAuthor, apoolJsonObj);
  };
  editorInfo.ace_prepareUserChangeset = function()
  {
    return changesetTracker.prepareUserChangeset();
  };
  editorInfo.ace_applyPreparedChangesetToBase = function()
  {
    changesetTracker.applyPreparedChangesetToBase();
  };
  editorInfo.ace_setUserChangeNotificationCallback = function(f)
  {
    changesetTracker.setUserChangeNotificationCallback(f);
  };
  editorInfo.ace_setAuthorInfo = function(author, info)
  {
    setAuthorInfo(author, info);
  };
  editorInfo.ace_setAuthorSelectionRange = function(author, start, end)
  {
    changesetTracker.setAuthorSelectionRange(author, start, end);
  };

  editorInfo.ace_getUnhandledErrors = function()
  {
    return caughtErrors.slice();
  };

  editorInfo.ace_getDocument = function()
  {
    return doc;
  };

  editorInfo.ace_getDebugProperty = function(prop)
  {
    if (prop == "debugger")
    {
      // obfuscate "eval" so as not to scare yuicompressor
      window['ev' + 'al']("debugger");
    }
    else if (prop == "rep")
    {
      return rep;
    }
    else if (prop == "window")
    {
      return window;
    }
    else if (prop == "document")
    {
      return document;
    }
    return undefined;
  };

  function now()
  {
    return (new Date()).getTime();
  }

  function newTimeLimit(ms)
  {
    //console.debug("new time limit");
    var startTime = now();
    var lastElapsed = 0;
    var exceededAlready = false;
    var printedTrace = false;
    var isTimeUp = function()
      {
        if (exceededAlready)
        {
          if ((!printedTrace))
          { // && now() - startTime - ms > 300) {
            //console.trace();
            printedTrace = true;
          }
          return true;
        }
        var elapsed = now() - startTime;
        if (elapsed > ms)
        {
          exceededAlready = true;
          //console.debug("time limit hit, before was %d/%d", lastElapsed, ms);
          //console.trace();
          return true;
        }
        else
        {
          lastElapsed = elapsed;
          return false;
        }
      };

    isTimeUp.elapsed = function()
    {
      return now() - startTime;
    };
    return isTimeUp;
  }


  function makeIdleAction(func)
  {
    var scheduledTimeout = null;
    var scheduledTime = 0;

    function unschedule()
    {
      if (scheduledTimeout)
      {
        scheduler.clearTimeout(scheduledTimeout);
        scheduledTimeout = null;
      }
    }

    function reschedule(time)
    {
      unschedule();
      scheduledTime = time;
      var delay = time - now();
      if (delay < 0) delay = 0;
      scheduledTimeout = scheduler.setTimeout(callback, delay);
    }

    function callback()
    {
      scheduledTimeout = null;
      // func may reschedule the action
      func();
    }
    return {
      atMost: function(ms)
      {
        var latestTime = now() + ms;
        if ((!scheduledTimeout) || scheduledTime > latestTime)
        {
          reschedule(latestTime);
        }
      },
      // atLeast(ms) will schedule the action if not scheduled yet.
      // In other words, "infinity" is replaced by ms, even though
      // it is technically larger.
      atLeast: function(ms)
      {
        var earliestTime = now() + ms;
        if ((!scheduledTimeout) || scheduledTime < earliestTime)
        {
          reschedule(earliestTime);
        }
      },
      never: function()
      {
        unschedule();
      }
    };
  }

  function fastIncorp(n)
  {
    // normalize but don't do any lexing or anything
    incorporateUserChanges(newTimeLimit(0));
  }
  editorInfo.ace_fastIncorp = fastIncorp;

  var idleWorkTimer = makeIdleAction(function()
  {

    //if (! top.BEFORE) top.BEFORE = [];
    //top.BEFORE.push(magicdom.root.dom.innerHTML);
    //if (! isEditable) return; // and don't reschedule
    if (inInternationalComposition)
    {
      // don't do idle input incorporation during international input composition
      idleWorkTimer.atLeast(500);
      return;
    }

    inCallStackIfNecessary("idleWorkTimer", function()
    {

      var isTimeUp = newTimeLimit(250);

      //console.time("idlework");
      var finishedImportantWork = false;
      var finishedWork = false;

      try
      {

        // isTimeUp() is a soft constraint for incorporateUserChanges,
        // which always renormalizes the DOM, no matter how long it takes,
        // but doesn't necessarily lex and highlight it
        incorporateUserChanges(isTimeUp);

        if (isTimeUp()) return;

        updateLineNumbers(); // update line numbers if any time left
        if (isTimeUp()) return;

        var visibleRange = scroll.getVisibleCharRange(rep);
        var docRange = [0, rep.lines.totalWidth()];
        //console.log("%o %o", docRange, visibleRange);
        finishedImportantWork = true;
        finishedWork = true;
      }
      finally
      {
        //console.timeEnd("idlework");
        if (finishedWork)
        {
          idleWorkTimer.atMost(1000);
        }
        else if (finishedImportantWork)
        {
          // if we've finished highlighting the view area,
          // more highlighting could be counter-productive,
          // e.g. if the user just opened a triple-quote and will soon close it.
          idleWorkTimer.atMost(500);
        }
        else
        {
          var timeToWait = Math.round(isTimeUp.elapsed() / 2);
          if (timeToWait < 100) timeToWait = 100;
          idleWorkTimer.atMost(timeToWait);
        }
      }
    });

    //if (! top.AFTER) top.AFTER = [];
    //top.AFTER.push(magicdom.root.dom.innerHTML);
  });

  var _nextId = 1;

  function uniqueId(n)
  {
    // not actually guaranteed to be unique, e.g. if user copy-pastes
    // nodes with ids
    var nid = n.id;
    if (nid) return nid;
    return (n.id = "magicdomid" + (_nextId++));
  }


  function recolorLinesInRange(startChar, endChar, isTimeUp, optModFunc)
  {
    if (endChar <= startChar) return;
    if (startChar < 0 || startChar >= rep.lines.totalWidth()) return;
    var lineEntry = rep.lines.atOffset(startChar); // rounds down to line boundary
    var lineStart = rep.lines.offsetOfEntry(lineEntry);
    var lineIndex = rep.lines.indexOfEntry(lineEntry);
    var selectionNeedsResetting = false;
    var firstLine = null;
    var lastLine = null;
    isTimeUp = (isTimeUp || noop);

    // tokenFunc function; accesses current value of lineEntry and curDocChar,
    // also mutates curDocChar
    var curDocChar;
    var tokenFunc = function(tokenText, tokenClass)
      {
        lineEntry.domInfo.appendSpan(tokenText, tokenClass);
        };
    if (optModFunc)
    {
      var f = tokenFunc;
      tokenFunc = function(tokenText, tokenClass)
      {
        optModFunc(tokenText, tokenClass, f, curDocChar);
        curDocChar += tokenText.length;
      };
    }

    while (lineEntry && lineStart < endChar && !isTimeUp())
    {
      //var timer = newTimeLimit(200);
      var lineEnd = lineStart + lineEntry.width;

      curDocChar = lineStart;
      lineEntry.domInfo.clearSpans();
      getSpansForLine(lineEntry, tokenFunc, lineStart);
      lineEntry.domInfo.finishUpdate();

      markNodeClean(lineEntry.lineNode);

      if (rep.selStart && rep.selStart[0] == lineIndex || rep.selEnd && rep.selEnd[0] == lineIndex)
      {
        selectionNeedsResetting = true;
      }

      //if (timer()) console.dirxml(lineEntry.lineNode.dom);
      if (firstLine === null) firstLine = lineIndex;
      lastLine = lineIndex;
      lineStart = lineEnd;
      lineEntry = rep.lines.next(lineEntry);
      lineIndex++;
    }
    if (selectionNeedsResetting)
    {
      currentCallStack.selectionAffected = true;
    }
    //console.debug("Recolored line range %d-%d", firstLine, lastLine);
  }

  // like getSpansForRange, but for a line, and the func takes (text,class)
  // instead of (width,class); excludes the trailing '\n' from
  // consideration by func


  function getSpansForLine(lineEntry, textAndClassFunc, lineEntryOffsetHint)
  {
    var lineEntryOffset = lineEntryOffsetHint;
    if ((typeof lineEntryOffset) != "number")
    {
      lineEntryOffset = rep.lines.offsetOfEntry(lineEntry);
    }
    var text = lineEntry.text;
    var width = lineEntry.width; // text.length+1
    if (text.length === 0)
    {
      // allow getLineStyleFilter to set line-div styles
      var func = linestylefilter.getLineStyleFilter(
      0, '', textAndClassFunc, rep.apool);
      func('', '');
    }
    else
    {
      var offsetIntoLine = 0;
      var filteredFunc = linestylefilter.getFilterStack(text, textAndClassFunc, browser);
      var lineNum = rep.lines.indexOfEntry(lineEntry);
      var aline = rep.alines[lineNum];
      filteredFunc = linestylefilter.getLineStyleFilter(
      text.length, aline, filteredFunc, rep.apool);
      filteredFunc(text, '');
    }
  }

  var observedChanges;

  function clearObservedChanges()
  {
    observedChanges = {
      cleanNodesNearChanges: {}
    };
  }
  clearObservedChanges();

  function getCleanNodeByKey(key)
  {
    var p = PROFILER("getCleanNodeByKey", false);
    p.extra = 0;
    var n = doc.getElementById(key);
    // copying and pasting can lead to duplicate ids
    while (n && isNodeDirty(n))
    {
      p.extra++;
      n.id = "";
      n = doc.getElementById(key);
    }
    p.literal(p.extra, "extra");
    p.end();
    return n;
  }

  function observeChangesAroundNode(node)
  {
    // Around this top-level DOM node, look for changes to the document
    // (from how it looks in our representation) and record them in a way
    // that can be used to "normalize" the document (apply the changes to our
    // representation, and put the DOM in a canonical form).
    // top.console.log("observeChangesAroundNode(%o)", node);
    var cleanNode;
    var hasAdjacentDirtyness;
    if (!isNodeDirty(node))
    {
      cleanNode = node;
      var prevSib = cleanNode.previousSibling;
      var nextSib = cleanNode.nextSibling;
      hasAdjacentDirtyness = ((prevSib && isNodeDirty(prevSib)) || (nextSib && isNodeDirty(nextSib)));
    }
    else
    {
      // node is dirty, look for clean node above
      var upNode = node.previousSibling;
      while (upNode && isNodeDirty(upNode))
      {
        upNode = upNode.previousSibling;
      }
      if (upNode)
      {
        cleanNode = upNode;
      }
      else
      {
        var downNode = node.nextSibling;
        while (downNode && isNodeDirty(downNode))
        {
          downNode = downNode.nextSibling;
        }
        if (downNode)
        {
          cleanNode = downNode;
        }
      }
      if (!cleanNode)
      {
        // Couldn't find any adjacent clean nodes!
        // Since top and bottom of doc is dirty, the dirty area will be detected.
        return;
      }
      hasAdjacentDirtyness = true;
    }

    if (hasAdjacentDirtyness)
    {
      // previous or next line is dirty
      observedChanges.cleanNodesNearChanges['$' + uniqueId(cleanNode)] = true;
    }
    else
    {
      // next and prev lines are clean (if they exist)
      var lineKey = uniqueId(cleanNode);
      var prevSib = cleanNode.previousSibling;
      var nextSib = cleanNode.nextSibling;
      var actualPrevKey = ((prevSib && uniqueId(prevSib)) || null);
      var actualNextKey = ((nextSib && uniqueId(nextSib)) || null);
      var repPrevEntry = rep.lines.prev(rep.lines.atKey(lineKey));
      var repNextEntry = rep.lines.next(rep.lines.atKey(lineKey));
      var repPrevKey = ((repPrevEntry && repPrevEntry.key) || null);
      var repNextKey = ((repNextEntry && repNextEntry.key) || null);
      if (actualPrevKey != repPrevKey || actualNextKey != repNextKey)
      {
        observedChanges.cleanNodesNearChanges['$' + uniqueId(cleanNode)] = true;
      }
    }
  }

  function observeChangesAroundSelection()
  {
    if (currentCallStack.observedSelection) return;
    currentCallStack.observedSelection = true;

    var p = PROFILER("getSelection", false);
    var selection = getSelection();
    p.end();

    if (selection)
    {
      var node1 = topLevel(selection.startPoint.node);
      var node2 = topLevel(selection.endPoint.node);
      if (node1) observeChangesAroundNode(node1);
      if (node2 && node1 != node2)
      {
        observeChangesAroundNode(node2);
      }
    }
  }

  function observeSuspiciousNodes()
  {
    // inspired by Firefox bug #473255, where pasting formatted text
    // causes the cursor to jump away, making the new HTML never found.
    if (root.getElementsByTagName)
    {
      var nds = root.getElementsByTagName("style");
      for (var i = 0; i < nds.length; i++)
      {
        var n = topLevel(nds[i]);
        if (n && n.parentNode == root)
        {
          observeChangesAroundNode(n);
        }
      }
    }
  }

  function incorporateUserChanges(isTimeUp)
  {

    if (currentCallStack.domClean) return false;

    currentCallStack.isUserChange = true;

    isTimeUp = (isTimeUp ||
    function()
    {
      return false;
    });

    if (DEBUG && window.DONT_INCORP || window.DEBUG_DONT_INCORP) return false;

    var p = PROFILER("incorp", false);

    //if (doc.body.innerHTML.indexOf("AppJet") >= 0)
    //dmesg(htmlPrettyEscape(doc.body.innerHTML));
    //if (top.RECORD) top.RECORD.push(doc.body.innerHTML);
    // returns true if dom changes were made
    if (!root.firstChild)
    {
      root.innerHTML = "<div><!-- --></div>";
    }

    p.mark("obs");
    observeChangesAroundSelection();
    observeSuspiciousNodes();
    p.mark("dirty");
    var dirtyRanges = getDirtyRanges();
    //console.log("dirtyRanges: "+toSource(dirtyRanges));
    var dirtyRangesCheckOut = true;
    var j = 0;
    var a, b;
    while (j < dirtyRanges.length)
    {
      a = dirtyRanges[j][0];
      b = dirtyRanges[j][1];
      if (!((a === 0 || getCleanNodeByKey(rep.lines.atIndex(a - 1).key)) && (b == rep.lines.length() || getCleanNodeByKey(rep.lines.atIndex(b).key))))
      {
        dirtyRangesCheckOut = false;
        break;
      }
      j++;
    }
    if (!dirtyRangesCheckOut)
    {
      var numBodyNodes = root.childNodes.length;
      for (var k = 0; k < numBodyNodes; k++)
      {
        var bodyNode = root.childNodes.item(k);
        if ((bodyNode.tagName) && ((!bodyNode.id) || (!rep.lines.containsKey(bodyNode.id))))
        {
          observeChangesAroundNode(bodyNode);
        }
      }
      dirtyRanges = getDirtyRanges();
    }

    clearObservedChanges();

    p.mark("getsel");
    var selection = getSelection();

    //console.log(magicdom.root.dom.innerHTML);
    //console.log("got selection: %o", selection);
    var selStart, selEnd; // each one, if truthy, has [line,char] needed to set selection
    var i = 0;
    var splicesToDo = [];
    var netNumLinesChangeSoFar = 0;
    var toDeleteAtEnd = [];
    p.mark("ranges");
    p.literal(dirtyRanges.length, "numdirt");
    var domInsertsNeeded = []; // each entry is [nodeToInsertAfter, [info1, info2, ...]]
    while (i < dirtyRanges.length)
    {
      var range = dirtyRanges[i];
      a = range[0];
      b = range[1];
      var firstDirtyNode = (((a === 0) && root.firstChild) || getCleanNodeByKey(rep.lines.atIndex(a - 1).key).nextSibling);
      firstDirtyNode = (firstDirtyNode && isNodeDirty(firstDirtyNode) && firstDirtyNode);
      var lastDirtyNode = (((b == rep.lines.length()) && root.lastChild) || getCleanNodeByKey(rep.lines.atIndex(b).key).previousSibling);
      lastDirtyNode = (lastDirtyNode && isNodeDirty(lastDirtyNode) && lastDirtyNode);
      if (firstDirtyNode && lastDirtyNode)
      {
        var cc = makeContentCollector(isStyled, browser, rep.apool, null, className2Author);
        cc.notifySelection(selection);
        var dirtyNodes = [];
        for (var n = firstDirtyNode; n && !(n.previousSibling && n.previousSibling == lastDirtyNode);
        n = n.nextSibling)
        {
          if (browser.msie)
          {
            // try to undo IE's pesky and overzealous linkification
            try
            {
              n.createTextRange().execCommand("unlink", false, null);
            }
            catch (e)
            {}
          }
          cc.collectContent(n);
          dirtyNodes.push(n);
        }
        cc.notifyNextNode(lastDirtyNode.nextSibling);
        var lines = cc.getLines();
        if ((lines.length <= 1 || lines[lines.length - 1] !== "") && lastDirtyNode.nextSibling)
        {
          // dirty region doesn't currently end a line, even taking the following node
          // (or lack of node) into account, so include the following clean node.
          // It could be SPAN or a DIV; basically this is any case where the contentCollector
          // decides it isn't done.
          // Note that this clean node might need to be there for the next dirty range.
          //console.log("inclusive of "+lastDirtyNode.next().dom.tagName);
          b++;
          var cleanLine = lastDirtyNode.nextSibling;
          cc.collectContent(cleanLine);
          toDeleteAtEnd.push(cleanLine);
          cc.notifyNextNode(cleanLine.nextSibling);
        }

        var ccData = cc.finish();
        var ss = ccData.selStart;
        var se = ccData.selEnd;
        lines = ccData.lines;
        var lineAttribs = ccData.lineAttribs;
        var linesWrapped = ccData.linesWrapped;
        var scrollToTheLeftNeeded = false;

        if (linesWrapped > 0)
        {
          if(!browser.msie){
            // chrome decides in it's infinite wisdom that its okay to put the browsers visisble window in the middle of the span
            // an outcome of this is that the first chars of the string are no longer visible to the user..  Yay chrome..
            // Move the browsers visible area to the left hand side of the span
            // Firefox isn't quite so bad, but it's still pretty quirky.
            var scrollToTheLeftNeeded = true;
          }
          // console.log("Editor warning: " + linesWrapped + " long line" + (linesWrapped == 1 ? " was" : "s were") + " hard-wrapped into " + ccData.numLinesAfter + " lines.");
        }

        if (ss[0] >= 0) selStart = [ss[0] + a + netNumLinesChangeSoFar, ss[1]];
        if (se[0] >= 0) selEnd = [se[0] + a + netNumLinesChangeSoFar, se[1]];

        var entries = [];
        var nodeToAddAfter = lastDirtyNode;
        var lineNodeInfos = new Array(lines.length);
        for (var k = 0; k < lines.length; k++)
        {
          var lineString = lines[k];
          var newEntry = createDomLineEntry(lineString);
          entries.push(newEntry);
          lineNodeInfos[k] = newEntry.domInfo;
        }
        //var fragment = magicdom.wrapDom(document.createDocumentFragment());
        domInsertsNeeded.push([nodeToAddAfter, lineNodeInfos]);
        _.each(dirtyNodes,function(n){
          toDeleteAtEnd.push(n);
        });
        var spliceHints = {};
        if (selStart) spliceHints.selStart = selStart;
        if (selEnd) spliceHints.selEnd = selEnd;
        splicesToDo.push([a + netNumLinesChangeSoFar, b - a, entries, lineAttribs, spliceHints]);
        netNumLinesChangeSoFar += (lines.length - (b - a));
      }
      else if (b > a)
      {
        splicesToDo.push([a + netNumLinesChangeSoFar, b - a, [],
          []
        ]);
      }
      i++;
    }

    var domChanges = (splicesToDo.length > 0);

    // update the representation
    p.mark("splice");
    _.each(splicesToDo, function(splice)
    {
      doIncorpLineSplice(splice[0], splice[1], splice[2], splice[3], splice[4]);
    });

    //p.mark("relex");
    //rep.lexer.lexCharRange(scroll.getVisibleCharRange(rep), function() { return false; });
    //var isTimeUp = newTimeLimit(100);
    // do DOM inserts
    p.mark("insert");
    _.each(domInsertsNeeded,function(ins)
    {
      insertDomLines(ins[0], ins[1], isTimeUp);
    });

    p.mark("del");
    // delete old dom nodes
    _.each(toDeleteAtEnd,function(n)
    {
      //var id = n.uniqueId();
      // parent of n may not be "root" in IE due to non-tree-shaped DOM (wtf)
      if(n.parentNode) n.parentNode.removeChild(n);

      //dmesg(htmlPrettyEscape(htmlForRemovedChild(n)));
      //console.log("removed: "+id);
    });

    if(scrollToTheLeftNeeded){ // needed to stop chrome from breaking the ui when long strings without spaces are pasted
      $("#innerdocbody").scrollLeft(0);
    }

    p.mark("findsel");
    // if the nodes that define the selection weren't encountered during
    // content collection, figure out where those nodes are now.
    if (selection && !selStart)
    {
      //if (domChanges) dmesg("selection not collected");
      var selStartFromHook = hooks.callAll('aceStartLineAndCharForPoint', {
        callstack: currentCallStack,
        editorInfo: editorInfo,
        rep: rep,
        root:root,
        point:selection.startPoint,
        documentAttributeManager: documentAttributeManager
      });
      selStart = (selStartFromHook==null||selStartFromHook.length==0)?getLineAndCharForPoint(selection.startPoint):selStartFromHook;
    }
    if (selection && !selEnd)
    {
      var selEndFromHook = hooks.callAll('aceEndLineAndCharForPoint', {
        callstack: currentCallStack,
        editorInfo: editorInfo,
        rep: rep,
        root:root,
        point:selection.endPoint,
        documentAttributeManager: documentAttributeManager
      });
      selEnd = (selEndFromHook==null||selEndFromHook.length==0)?getLineAndCharForPoint(selection.endPoint):selEndFromHook;
    }

    // selection from content collection can, in various ways, extend past final
    // BR in firefox DOM, so cap the line
    var numLines = rep.lines.length();
    if (selStart && selStart[0] >= numLines)
    {
      selStart[0] = numLines - 1;
      selStart[1] = rep.lines.atIndex(selStart[0]).text.length;
    }
    if (selEnd && selEnd[0] >= numLines)
    {
      selEnd[0] = numLines - 1;
      selEnd[1] = rep.lines.atIndex(selEnd[0]).text.length;
    }

    p.mark("repsel");
    // update rep if we have a new selection
    // NOTE: IE loses the selection when you click stuff in e.g. the
    // editbar, so removing the selection when it's lost is not a good
    // idea.
    if (selection) repSelectionChange(selStart, selEnd, selection && selection.focusAtStart);
    // update browser selection
    p.mark("browsel");
    if (selection && (domChanges || isCaret()))
    {
      // if no DOM changes (not this case), want to treat range selection delicately,
      // e.g. in IE not lose which end of the selection is the focus/anchor;
      // on the other hand, we may have just noticed a press of PageUp/PageDown
      currentCallStack.selectionAffected = true;
    }

    currentCallStack.domClean = true;

    p.mark("fixview");

    fixView();

    p.end("END");

    return domChanges;
  }

  var STYLE_ATTRIBS = {
    bold: true,
    italic: true,
    underline: true,
    strikethrough: true,
    list: true
  };
  var OTHER_INCORPED_ATTRIBS = {
    insertorder: true,
    author: true
  };

  function isStyleAttribute(aname)
  {
    return !!STYLE_ATTRIBS[aname];
  }

  function isOtherIncorpedAttribute(aname)
  {
    return !!OTHER_INCORPED_ATTRIBS[aname];
  }

  function insertDomLines(nodeToAddAfter, infoStructs, isTimeUp)
  {
    isTimeUp = (isTimeUp ||
    function()
    {
      return false;
    });

    var lastEntry;
    var lineStartOffset;
    if (infoStructs.length < 1) return;
    var startEntry = rep.lines.atKey(uniqueId(infoStructs[0].node));
    var endEntry = rep.lines.atKey(uniqueId(infoStructs[infoStructs.length - 1].node));
    var charStart = rep.lines.offsetOfEntry(startEntry);
    var charEnd = rep.lines.offsetOfEntry(endEntry) + endEntry.width;

    //rep.lexer.lexCharRange([charStart, charEnd], isTimeUp);
    _.each(infoStructs, function(info)
    {
      var p2 = PROFILER("insertLine", false);
      var node = info.node;
      var key = uniqueId(node);
      var entry;
      p2.mark("findEntry");
      if (lastEntry)
      {
        // optimization to avoid recalculation
        var next = rep.lines.next(lastEntry);
        if (next && next.key == key)
        {
          entry = next;
          lineStartOffset += lastEntry.width;
        }
      }
      if (!entry)
      {
        p2.literal(1, "nonopt");
        entry = rep.lines.atKey(key);
        lineStartOffset = rep.lines.offsetOfKey(key);
      }
      else p2.literal(0, "nonopt");
      lastEntry = entry;
      p2.mark("spans");
      getSpansForLine(entry, function(tokenText, tokenClass)
      {
        info.appendSpan(tokenText, tokenClass);
      }, lineStartOffset, isTimeUp());
      //else if (entry.text.length > 0) {
      //info.appendSpan(entry.text, 'dirty');
      //}
      p2.mark("addLine");
      info.prepareForAdd();
      entry.lineMarker = info.lineMarker;
      if (!nodeToAddAfter)
      {
        root.insertBefore(node, root.firstChild);
      }
      else
      {
        root.insertBefore(node, nodeToAddAfter.nextSibling);
      }
      nodeToAddAfter = node;
      info.notifyAdded();
      p2.mark("markClean");
      markNodeClean(node);
      p2.end();
    });
  }

  function isCaret()
  {
    return (rep.selStart && rep.selEnd && rep.selStart[0] == rep.selEnd[0] && rep.selStart[1] == rep.selEnd[1]);
  }
  editorInfo.ace_isCaret = isCaret;

  // prereq: isCaret()


  function caretLine()
  {
    return rep.selStart[0];
  }
  editorInfo.ace_caretLine = caretLine;

  function caretColumn()
  {
    return rep.selStart[1];
  }
  editorInfo.ace_caretColumn = caretColumn;

  function caretDocChar()
  {
    return rep.lines.offsetOfIndex(caretLine()) + caretColumn();
  }
  editorInfo.ace_caretDocChar = caretDocChar;

  function handleReturnIndentation()
  {
    // on return, indent to level of previous line
    if (isCaret() && caretColumn() === 0 && caretLine() > 0)
    {
      var lineNum = caretLine();
      var thisLine = rep.lines.atIndex(lineNum);
      var prevLine = rep.lines.prev(thisLine);
      var prevLineText = prevLine.text;
      var theIndent = /^ *(?:)/.exec(prevLineText)[0];
      var shouldIndent = parent.parent.clientVars.indentationOnNewLine;
      if (shouldIndent && /[\[\(\:\{]\s*$/.exec(prevLineText))
      {
        theIndent += THE_TAB;
      }
      var cs = Changeset.builder(rep.lines.totalWidth()).keep(
      rep.lines.offsetOfIndex(lineNum), lineNum).insert(
      theIndent, [
        ['author', thisAuthor]
      ], rep.apool).toString();
      performDocumentApplyChangeset(cs);
      performSelectionChange([lineNum, theIndent.length], [lineNum, theIndent.length]);
    }
  }

  function getPointForLineAndChar(lineAndChar)
  {
    var line = lineAndChar[0];
    var charsLeft = lineAndChar[1];
    //console.log("line: %d, key: %s, node: %o", line, rep.lines.atIndex(line).key,
    //getCleanNodeByKey(rep.lines.atIndex(line).key));
    var lineEntry = rep.lines.atIndex(line);
    charsLeft -= lineEntry.lineMarker;
    if (charsLeft < 0)
    {
      charsLeft = 0;
    }
    var lineNode = lineEntry.lineNode;
    var n = lineNode;
    var after = false;
    if (charsLeft === 0)
    {
      var index = 0;

      if (browser.msie && parseInt(browser.version) >= 11) {
        browser.msie = false; // Temp fix to resolve enter and backspace issues..
        // Note that this makes MSIE behave like modern browsers..
      }
      if (browser.msie && line == (rep.lines.length() - 1) && lineNode.childNodes.length === 0)
      {
        // best to stay at end of last empty div in IE
        index = 1;
      }
      return {
        node: lineNode,
        index: index,
        maxIndex: 1
      };
    }
    while (!(n == lineNode && after))
    {
      if (after)
      {
        if (n.nextSibling)
        {
          n = n.nextSibling;
          after = false;
        }
        else n = n.parentNode;
      }
      else
      {
        if (isNodeText(n))
        {
          var len = n.nodeValue.length;
          if (charsLeft <= len)
          {
            return {
              node: n,
              index: charsLeft,
              maxIndex: len
            };
          }
          charsLeft -= len;
          after = true;
        }
        else
        {
          if (n.firstChild) n = n.firstChild;
          else after = true;
        }
      }
    }
    return {
      node: lineNode,
      index: 1,
      maxIndex: 1
    };
  }

  function nodeText(n)
  {
      if (browser.msie) {
	  return n.innerText;
      } else {
	  return n.textContent || n.nodeValue || '';
      }
  }

  function getLineAndCharForPoint(point)
  {
    // Turn DOM node selection into [line,char] selection.
    // This method has to work when the DOM is not pristine,
    // assuming the point is not in a dirty node.
    if (point.node == root)
    {
      if (point.index === 0)
      {
        return [0, 0];
      }
      else
      {
        var N = rep.lines.length();
        var ln = rep.lines.atIndex(N - 1);
        return [N - 1, ln.text.length];
      }
    }
    else
    {
      var n = point.node;
      var col = 0;
      // if this part fails, it probably means the selection node
      // was dirty, and we didn't see it when collecting dirty nodes.
      if (isNodeText(n))
      {
        col = point.index;
      }
      else if (point.index > 0)
      {
        col = nodeText(n).length;
      }
      var parNode, prevSib;
      while ((parNode = n.parentNode) != root)
      {
        if ((prevSib = n.previousSibling))
        {
          n = prevSib;
          col += nodeText(n).length;
        }
        else
        {
          n = parNode;
        }
      }
      if (n.id === "") console.debug("BAD");
      if (n.firstChild && isBlockElement(n.firstChild))
      {
        col += 1; // lineMarker
      }
      var lineEntry = rep.lines.atKey(n.id);
      var lineNum = rep.lines.indexOfEntry(lineEntry);
      return [lineNum, col];
    }
  }
  editorInfo.ace_getLineAndCharForPoint = getLineAndCharForPoint;

  function createDomLineEntry(lineString)
  {
    var info = doCreateDomLine(lineString.length > 0);
    var newNode = info.node;
    return {
      key: uniqueId(newNode),
      text: lineString,
      lineNode: newNode,
      domInfo: info,
      lineMarker: 0
    };
  }

  function canApplyChangesetToDocument(changes)
  {
    return Changeset.oldLen(changes) == rep.alltext.length;
  }

  function performDocumentApplyChangeset(changes, insertsAfterSelection)
  {
    doRepApplyChangeset(changes, insertsAfterSelection);

    var requiredSelectionSetting = null;
    if (rep.selStart && rep.selEnd)
    {
      var selStartChar = rep.lines.offsetOfIndex(rep.selStart[0]) + rep.selStart[1];
      var selEndChar = rep.lines.offsetOfIndex(rep.selEnd[0]) + rep.selEnd[1];
      var result = Changeset.characterRangeFollow(changes, selStartChar, selEndChar, insertsAfterSelection);
      requiredSelectionSetting = [result[0], result[1], rep.selFocusAtStart];
    }

    var linesMutatee = {
      splice: function(start, numRemoved, newLinesVA)
      {
        var args = Array.prototype.slice.call(arguments, 2);
        domAndRepSplice(start, numRemoved, _.map(args, function(s){ return s.slice(0, -1); }), null);
      },
      get: function(i)
      {
        return rep.lines.atIndex(i).text + '\n';
      },
      length: function()
      {
        return rep.lines.length();
      },
      slice_notused: function(start, end)
      {
        return _.map(rep.lines.slice(start, end), function(e)
        {
          return e.text + '\n';
        });
      }
    };

    Changeset.mutateTextLines(changes, linesMutatee);

    checkALines();

    if (requiredSelectionSetting)
    {
      performSelectionChange(lineAndColumnFromChar(requiredSelectionSetting[0]), lineAndColumnFromChar(requiredSelectionSetting[1]), requiredSelectionSetting[2]);
    }

    function domAndRepSplice(startLine, deleteCount, newLineStrings, isTimeUp)
    {
      // dgreensp 3/2009: the spliced lines may be in the middle of a dirty region,
      // so if no explicit time limit, don't spend a lot of time highlighting
      isTimeUp = (isTimeUp || newTimeLimit(50));

      var keysToDelete = [];
      if (deleteCount > 0)
      {
        var entryToDelete = rep.lines.atIndex(startLine);
        for (var i = 0; i < deleteCount; i++)
        {
          keysToDelete.push(entryToDelete.key);
          entryToDelete = rep.lines.next(entryToDelete);
        }
      }

      var lineEntries = _.map(newLineStrings, createDomLineEntry);

      doRepLineSplice(startLine, deleteCount, lineEntries);

      var nodeToAddAfter;
      if (startLine > 0)
      {
        nodeToAddAfter = getCleanNodeByKey(rep.lines.atIndex(startLine - 1).key);
      }
      else nodeToAddAfter = null;

      insertDomLines(nodeToAddAfter, _.map(lineEntries, function(entry)
      {
        return entry.domInfo;
      }), isTimeUp);

      _.each(keysToDelete, function(k)
      {
        var n = doc.getElementById(k);
        n.parentNode.removeChild(n);
      });

      if ((rep.selStart && rep.selStart[0] >= startLine && rep.selStart[0] <= startLine + deleteCount) || (rep.selEnd && rep.selEnd[0] >= startLine && rep.selEnd[0] <= startLine + deleteCount))
      {
        currentCallStack.selectionAffected = true;
      }
    }
  }

  function checkChangesetLineInformationAgainstRep(changes)
  {
    return true; // disable for speed
    var opIter = Changeset.opIterator(Changeset.unpack(changes).ops);
    var curOffset = 0;
    var curLine = 0;
    var curCol = 0;
    while (opIter.hasNext())
    {
      var o = opIter.next();
      if (o.opcode == '-' || o.opcode == '=')
      {
        curOffset += o.chars;
        if (o.lines)
        {
          curLine += o.lines;
          curCol = 0;
        }
        else
        {
          curCol += o.chars;
        }
      }
      var calcLine = rep.lines.indexOfOffset(curOffset);
      var calcLineStart = rep.lines.offsetOfIndex(calcLine);
      var calcCol = curOffset - calcLineStart;
      if (calcCol != curCol || calcLine != curLine)
      {
        return false;
      }
    }
    return true;
  }

  function doRepApplyChangeset(changes, insertsAfterSelection)
  {
    Changeset.checkRep(changes);

    if (Changeset.oldLen(changes) != rep.alltext.length) throw new Error("doRepApplyChangeset length mismatch: " + Changeset.oldLen(changes) + "/" + rep.alltext.length);

    if (!checkChangesetLineInformationAgainstRep(changes))
    {
      throw new Error("doRepApplyChangeset line break mismatch");
    }

    (function doRecordUndoInformation(changes)
    {
      var editEvent = currentCallStack.editEvent;
      if (editEvent.eventType == "nonundoable")
      {
        if (!editEvent.changeset)
        {
          editEvent.changeset = changes;
        }
        else
        {
          editEvent.changeset = Changeset.compose(editEvent.changeset, changes, rep.apool);
        }
      }
      else
      {
        var inverseChangeset = Changeset.inverse(changes, {
          get: function(i)
          {
            return rep.lines.atIndex(i).text + '\n';
          },
          length: function()
          {
            return rep.lines.length();
          }
        }, rep.alines, rep.apool);

        if (!editEvent.backset)
        {
          editEvent.backset = inverseChangeset;
        }
        else
        {
          editEvent.backset = Changeset.compose(inverseChangeset, editEvent.backset, rep.apool);
        }
      }
    })(changes);

    //rep.alltext = Changeset.applyToText(changes, rep.alltext);
    Changeset.mutateAttributionLines(changes, rep.alines, rep.apool);

    if (changesetTracker.isTracking())
    {
      changesetTracker.composeUserChangeset(changes);
    }

  }

  /*
    Converts the position of a char (index in String) into a [row, col] tuple
  */
  function lineAndColumnFromChar(x)
  {
    var lineEntry = rep.lines.atOffset(x);
    var lineStart = rep.lines.offsetOfEntry(lineEntry);
    var lineNum = rep.lines.indexOfEntry(lineEntry);
    return [lineNum, x - lineStart];
  }

  function performDocumentReplaceCharRange(startChar, endChar, newText)
  {
    if (startChar == endChar && newText.length === 0)
    {
      return;
    }
    // Requires that the replacement preserve the property that the
    // internal document text ends in a newline.  Given this, we
    // rewrite the splice so that it doesn't touch the very last
    // char of the document.
    if (endChar == rep.alltext.length)
    {
      if (startChar == endChar)
      {
        // an insert at end
        startChar--;
        endChar--;
        newText = '\n' + newText.substring(0, newText.length - 1);
      }
      else if (newText.length === 0)
      {
        // a delete at end
        startChar--;
        endChar--;
      }
      else
      {
        // a replace at end
        endChar--;
        newText = newText.substring(0, newText.length - 1);
      }
    }
    performDocumentReplaceRange(lineAndColumnFromChar(startChar), lineAndColumnFromChar(endChar), newText);
  }

  function performDocumentReplaceRange(start, end, newText)
  {
    if (start === undefined) start = rep.selStart;
    if (end === undefined) end = rep.selEnd;

    //dmesg(String([start.toSource(),end.toSource(),newText.toSource()]));
    // start[0]: <--- start[1] --->CCCCCCCCCCC\n
    //           CCCCCCCCCCCCCCCCCCCC\n
    //           CCCC\n
    // end[0]:   <CCC end[1] CCC>-------\n
    var builder = Changeset.builder(rep.lines.totalWidth());
    ChangesetUtils.buildKeepToStartOfRange(rep, builder, start);
    ChangesetUtils.buildRemoveRange(rep, builder, start, end);
    builder.insert(newText, [
      ['author', thisAuthor]
    ], rep.apool);
    var cs = builder.toString();

    performDocumentApplyChangeset(cs);
  }

  function performDocumentApplyAttributesToCharRange(start, end, attribs)
  {
    end = Math.min(end, rep.alltext.length - 1);
    documentAttributeManager.setAttributesOnRange(lineAndColumnFromChar(start), lineAndColumnFromChar(end), attribs);
  }
  editorInfo.ace_performDocumentApplyAttributesToCharRange = performDocumentApplyAttributesToCharRange;


  function setAttributeOnSelection(attributeName, attributeValue)
  {
    if (!(rep.selStart && rep.selEnd)) return;

    documentAttributeManager.setAttributesOnRange(rep.selStart, rep.selEnd, [
      [attributeName, attributeValue]
    ]);
  }
  editorInfo.ace_setAttributeOnSelection = setAttributeOnSelection;


  function getAttributeOnSelection(attributeName, prevChar){
    if (!(rep.selStart && rep.selEnd)) return
    var isNotSelection = (rep.selStart[0] == rep.selEnd[0] && rep.selEnd[1] === rep.selStart[1]);
    if(isNotSelection){
      if(prevChar){
        // If it's not the start of the line
        if(rep.selStart[1] !== 0){
          rep.selStart[1]--;
        }
      }
    }

    var withIt = Changeset.makeAttribsString('+', [
      [attributeName, 'true']
    ], rep.apool);
    var withItRegex = new RegExp(withIt.replace(/\*/g, '\\*') + "(\\*|$)");
    function hasIt(attribs)
    {
      return withItRegex.test(attribs);
    }

    return rangeHasAttrib(rep.selStart, rep.selEnd)

    function rangeHasAttrib(selStart, selEnd) {
      // if range is collapsed -> no attribs in range
      if(selStart[1] == selEnd[1] && selStart[0] == selEnd[0]) return false

      if(selStart[0] != selEnd[0]) { // -> More than one line selected
        var hasAttrib = true

        // from selStart to the end of the first line
        hasAttrib = hasAttrib && rangeHasAttrib(selStart, [selStart[0], rep.lines.atIndex(selStart[0]).text.length])

        // for all lines in between
        for(var n=selStart[0]+1; n < selEnd[0]; n++) {
          hasAttrib = hasAttrib && rangeHasAttrib([n, 0], [n, rep.lines.atIndex(n).text.length])
        }

        // for the last, potentially partial, line
        hasAttrib = hasAttrib && rangeHasAttrib([selEnd[0], 0], [selEnd[0], selEnd[1]])

        return hasAttrib
      }

      // Logic tells us we now have a range on a single line

      var lineNum = selStart[0]
        , start = selStart[1]
        , end = selEnd[1]
        , hasAttrib = true

      // Iterate over attribs on this line

      var opIter = Changeset.opIterator(rep.alines[lineNum])
        , indexIntoLine = 0

      while (opIter.hasNext()) {
        var op = opIter.next();
        var opStartInLine = indexIntoLine;
        var opEndInLine = opStartInLine + op.chars;
        if (!hasIt(op.attribs)) {
          // does op overlap selection?
          if (!(opEndInLine <= start || opStartInLine >= end)) {
            hasAttrib = false; // since it's overlapping but hasn't got the attrib -> range hasn't got it
            break;
          }
        }
        indexIntoLine = opEndInLine;
      }

      return hasAttrib
    }
  }

  editorInfo.ace_getAttributeOnSelection = getAttributeOnSelection;

  function toggleAttributeOnSelection(attributeName)
  {
    if (!(rep.selStart && rep.selEnd)) return;

    var selectionAllHasIt = true;
    var withIt = Changeset.makeAttribsString('+', [
      [attributeName, 'true']
    ], rep.apool);
    var withItRegex = new RegExp(withIt.replace(/\*/g, '\\*') + "(\\*|$)");

    function hasIt(attribs)
    {
      return withItRegex.test(attribs);
    }

    var selStartLine = rep.selStart[0];
    var selEndLine = rep.selEnd[0];
    for (var n = selStartLine; n <= selEndLine; n++)
    {
      var opIter = Changeset.opIterator(rep.alines[n]);
      var indexIntoLine = 0;
      var selectionStartInLine = 0;
      if (documentAttributeManager.lineHasMarker(n)) {
        selectionStartInLine = 1; // ignore "*" used as line marker
      }
      var selectionEndInLine = rep.lines.atIndex(n).text.length; // exclude newline
      if (n == selStartLine)
      {
        selectionStartInLine = rep.selStart[1];
      }
      if (n == selEndLine)
      {
        selectionEndInLine = rep.selEnd[1];
      }
      while (opIter.hasNext())
      {
        var op = opIter.next();
        var opStartInLine = indexIntoLine;
        var opEndInLine = opStartInLine + op.chars;
        if (!hasIt(op.attribs))
        {
          // does op overlap selection?
          if (!(opEndInLine <= selectionStartInLine || opStartInLine >= selectionEndInLine))
          {
            selectionAllHasIt = false;
            break;
          }
        }
        indexIntoLine = opEndInLine;
      }
      if (!selectionAllHasIt)
      {
        break;
      }
    }


    var attributeValue = selectionAllHasIt ? '' : 'true';
    documentAttributeManager.setAttributesOnRange(rep.selStart, rep.selEnd, [[attributeName, attributeValue]]);
    if (attribIsFormattingStyle(attributeName)) {
      updateStyleButtonState(attributeName, !selectionAllHasIt); // italic, bold, ...
    }
  }
  editorInfo.ace_toggleAttributeOnSelection = toggleAttributeOnSelection;

  function performDocumentReplaceSelection(newText)
  {
    if (!(rep.selStart && rep.selEnd)) return;
    performDocumentReplaceRange(rep.selStart, rep.selEnd, newText);
  }

  // Change the abstract representation of the document to have a different set of lines.
  // Must be called after rep.alltext is set.


  function doRepLineSplice(startLine, deleteCount, newLineEntries)
  {

    _.each(newLineEntries, function(entry)
    {
      entry.width = entry.text.length + 1;
    });

    var startOldChar = rep.lines.offsetOfIndex(startLine);
    var endOldChar = rep.lines.offsetOfIndex(startLine + deleteCount);

    var oldRegionStart = rep.lines.offsetOfIndex(startLine);
    var oldRegionEnd = rep.lines.offsetOfIndex(startLine + deleteCount);
    rep.lines.splice(startLine, deleteCount, newLineEntries);
    currentCallStack.docTextChanged = true;
    currentCallStack.repChanged = true;
    var newRegionEnd = rep.lines.offsetOfIndex(startLine + newLineEntries.length);

    var newText = _.map(newLineEntries, function(e)
    {
      return e.text + '\n';
    }).join('');

    rep.alltext = rep.alltext.substring(0, startOldChar) + newText + rep.alltext.substring(endOldChar, rep.alltext.length);

    //var newTotalLength = rep.alltext.length;
    //rep.lexer.updateBuffer(rep.alltext, oldRegionStart, oldRegionEnd - oldRegionStart,
    //newRegionEnd - oldRegionStart);
  }

  function doIncorpLineSplice(startLine, deleteCount, newLineEntries, lineAttribs, hints)
  {
    var startOldChar = rep.lines.offsetOfIndex(startLine);
    var endOldChar = rep.lines.offsetOfIndex(startLine + deleteCount);

    var oldRegionStart = rep.lines.offsetOfIndex(startLine);

    var selStartHintChar, selEndHintChar;
    if (hints && hints.selStart)
    {
      selStartHintChar = rep.lines.offsetOfIndex(hints.selStart[0]) + hints.selStart[1] - oldRegionStart;
    }
    if (hints && hints.selEnd)
    {
      selEndHintChar = rep.lines.offsetOfIndex(hints.selEnd[0]) + hints.selEnd[1] - oldRegionStart;
    }

    var newText = _.map(newLineEntries, function(e)
    {
      return e.text + '\n';
    }).join('');
    var oldText = rep.alltext.substring(startOldChar, endOldChar);
    var oldAttribs = rep.alines.slice(startLine, startLine + deleteCount).join('');
    var newAttribs = lineAttribs.join('|1+1') + '|1+1'; // not valid in a changeset
    var analysis = analyzeChange(oldText, newText, oldAttribs, newAttribs, selStartHintChar, selEndHintChar);
    var commonStart = analysis[0];
    var commonEnd = analysis[1];
    var shortOldText = oldText.substring(commonStart, oldText.length - commonEnd);
    var shortNewText = newText.substring(commonStart, newText.length - commonEnd);
    var spliceStart = startOldChar + commonStart;
    var spliceEnd = endOldChar - commonEnd;
    var shiftFinalNewlineToBeforeNewText = false;

    // adjust the splice to not involve the final newline of the document;
    // be very defensive
    if (shortOldText.charAt(shortOldText.length - 1) == '\n' && shortNewText.charAt(shortNewText.length - 1) == '\n')
    {
      // replacing text that ends in newline with text that also ends in newline
      // (still, after analysis, somehow)
      shortOldText = shortOldText.slice(0, -1);
      shortNewText = shortNewText.slice(0, -1);
      spliceEnd--;
      commonEnd++;
    }
    if (shortOldText.length === 0 && spliceStart == rep.alltext.length && shortNewText.length > 0)
    {
      // inserting after final newline, bad
      spliceStart--;
      spliceEnd--;
      shortNewText = '\n' + shortNewText.slice(0, -1);
      shiftFinalNewlineToBeforeNewText = true;
    }
    if (spliceEnd == rep.alltext.length && shortOldText.length > 0 && shortNewText.length === 0)
    {
      // deletion at end of rep.alltext
      if (rep.alltext.charAt(spliceStart - 1) == '\n')
      {
        // (if not then what the heck?  it will definitely lead
        // to a rep.alltext without a final newline)
        spliceStart--;
        spliceEnd--;
      }
    }

    if (!(shortOldText.length === 0 && shortNewText.length === 0))
    {
      var oldDocText = rep.alltext;
      var oldLen = oldDocText.length;

      var spliceStartLine = rep.lines.indexOfOffset(spliceStart);
      var spliceStartLineStart = rep.lines.offsetOfIndex(spliceStartLine);

      var startBuilder = function()
      {
        var builder = Changeset.builder(oldLen);
        builder.keep(spliceStartLineStart, spliceStartLine);
        builder.keep(spliceStart - spliceStartLineStart);
        return builder;
      };

      var eachAttribRun = function(attribs, func /*(startInNewText, endInNewText, attribs)*/ )
      {
        var attribsIter = Changeset.opIterator(attribs);
        var textIndex = 0;
        var newTextStart = commonStart;
        var newTextEnd = newText.length - commonEnd - (shiftFinalNewlineToBeforeNewText ? 1 : 0);
        while (attribsIter.hasNext())
        {
          var op = attribsIter.next();
          var nextIndex = textIndex + op.chars;
          if (!(nextIndex <= newTextStart || textIndex >= newTextEnd))
          {
            func(Math.max(newTextStart, textIndex), Math.min(newTextEnd, nextIndex), op.attribs);
          }
          textIndex = nextIndex;
        }
      };

      var justApplyStyles = (shortNewText == shortOldText);
      var theChangeset;

      if (justApplyStyles)
      {
        // create changeset that clears the incorporated styles on
        // the existing text.  we compose this with the
        // changeset the applies the styles found in the DOM.
        // This allows us to incorporate, e.g., Safari's native "unbold".
        var incorpedAttribClearer = cachedStrFunc(function(oldAtts)
        {
          return Changeset.mapAttribNumbers(oldAtts, function(n)
          {
            var k = rep.apool.getAttribKey(n);
            if (isStyleAttribute(k))
            {
              return rep.apool.putAttrib([k, '']);
            }
            return false;
          });
        });

        var builder1 = startBuilder();
        if (shiftFinalNewlineToBeforeNewText)
        {
          builder1.keep(1, 1);
        }
        eachAttribRun(oldAttribs, function(start, end, attribs)
        {
          builder1.keepText(newText.substring(start, end), incorpedAttribClearer(attribs));
        });
        var clearer = builder1.toString();

        var builder2 = startBuilder();
        if (shiftFinalNewlineToBeforeNewText)
        {
          builder2.keep(1, 1);
        }
        eachAttribRun(newAttribs, function(start, end, attribs)
        {
          builder2.keepText(newText.substring(start, end), attribs);
        });
        var styler = builder2.toString();

        theChangeset = Changeset.compose(clearer, styler, rep.apool);
      }
      else
      {
        var builder = startBuilder();

        var spliceEndLine = rep.lines.indexOfOffset(spliceEnd);
        var spliceEndLineStart = rep.lines.offsetOfIndex(spliceEndLine);
        if (spliceEndLineStart > spliceStart)
        {
          builder.remove(spliceEndLineStart - spliceStart, spliceEndLine - spliceStartLine);
          builder.remove(spliceEnd - spliceEndLineStart);
        }
        else
        {
          builder.remove(spliceEnd - spliceStart);
        }

        var isNewTextMultiauthor = false;
        var authorAtt = Changeset.makeAttribsString('+', (thisAuthor ? [
          ['author', thisAuthor]
        ] : []), rep.apool);
        var authorizer = cachedStrFunc(function(oldAtts)
        {
          if (isNewTextMultiauthor)
          {
            // prefer colors from DOM
            return Changeset.composeAttributes(authorAtt, oldAtts, true, rep.apool);
          }
          else
          {
            // use this author's color
            return Changeset.composeAttributes(oldAtts, authorAtt, true, rep.apool);
          }
        });

        var foundDomAuthor = '';
        eachAttribRun(newAttribs, function(start, end, attribs)
        {
          var a = Changeset.attribsAttributeValue(attribs, 'author', rep.apool);
          if (a && a != foundDomAuthor)
          {
            if (!foundDomAuthor)
            {
              foundDomAuthor = a;
            }
            else
            {
              isNewTextMultiauthor = true; // multiple authors in DOM!
            }
          }
        });

        if (shiftFinalNewlineToBeforeNewText)
        {
          builder.insert('\n', authorizer(''));
        }

        eachAttribRun(newAttribs, function(start, end, attribs)
        {
          builder.insert(newText.substring(start, end), authorizer(attribs));
        });
        theChangeset = builder.toString();
      }

      //dmesg(htmlPrettyEscape(theChangeset));
      doRepApplyChangeset(theChangeset);
    }

    // do this no matter what, because we need to get the right
    // line keys into the rep.
    doRepLineSplice(startLine, deleteCount, newLineEntries);

    checkALines();
  }

  function cachedStrFunc(func)
  {
    var cache = {};
    return function(s)
    {
      if (!cache[s])
      {
        cache[s] = func(s);
      }
      return cache[s];
    };
  }

  function analyzeChange(oldText, newText, oldAttribs, newAttribs, optSelStartHint, optSelEndHint)
  {
    function incorpedAttribFilter(anum)
    {
      return !isOtherIncorpedAttribute(rep.apool.getAttribKey(anum));
    }

    function attribRuns(attribs)
    {
      var lengs = [];
      var atts = [];
      var iter = Changeset.opIterator(attribs);
      while (iter.hasNext())
      {
        var op = iter.next();
        lengs.push(op.chars);
        atts.push(op.attribs);
      }
      return [lengs, atts];
    }

    function attribIterator(runs, backward)
    {
      var lengs = runs[0];
      var atts = runs[1];
      var i = (backward ? lengs.length - 1 : 0);
      var j = 0;
      return function next()
      {
        while (j >= lengs[i])
        {
          if (backward) i--;
          else i++;
          j = 0;
        }
        var a = atts[i];
        j++;
        return a;
      };
    }

    var oldLen = oldText.length;
    var newLen = newText.length;
    var minLen = Math.min(oldLen, newLen);

    var oldARuns = attribRuns(Changeset.filterAttribNumbers(oldAttribs, incorpedAttribFilter));
    var newARuns = attribRuns(Changeset.filterAttribNumbers(newAttribs, incorpedAttribFilter));

    var commonStart = 0;
    var oldStartIter = attribIterator(oldARuns, false);
    var newStartIter = attribIterator(newARuns, false);
    while (commonStart < minLen)
    {
      if (oldText.charAt(commonStart) == newText.charAt(commonStart) && oldStartIter() == newStartIter())
      {
        commonStart++;
      }
      else break;
    }

    var commonEnd = 0;
    var oldEndIter = attribIterator(oldARuns, true);
    var newEndIter = attribIterator(newARuns, true);
    while (commonEnd < minLen)
    {
      if (commonEnd === 0)
      {
        // assume newline in common
        oldEndIter();
        newEndIter();
        commonEnd++;
      }
      else if (oldText.charAt(oldLen - 1 - commonEnd) == newText.charAt(newLen - 1 - commonEnd) && oldEndIter() == newEndIter())
      {
        commonEnd++;
      }
      else break;
    }

    var hintedCommonEnd = -1;
    if ((typeof optSelEndHint) == "number")
    {
      hintedCommonEnd = newLen - optSelEndHint;
    }


    if (commonStart + commonEnd > oldLen)
    {
      // ambiguous insertion
      var minCommonEnd = oldLen - commonStart;
      var maxCommonEnd = commonEnd;
      if (hintedCommonEnd >= minCommonEnd && hintedCommonEnd <= maxCommonEnd)
      {
        commonEnd = hintedCommonEnd;
      }
      else
      {
        commonEnd = minCommonEnd;
      }
      commonStart = oldLen - commonEnd;
    }
    if (commonStart + commonEnd > newLen)
    {
      // ambiguous deletion
      var minCommonEnd = newLen - commonStart;
      var maxCommonEnd = commonEnd;
      if (hintedCommonEnd >= minCommonEnd && hintedCommonEnd <= maxCommonEnd)
      {
        commonEnd = hintedCommonEnd;
      }
      else
      {
        commonEnd = minCommonEnd;
      }
      commonStart = newLen - commonEnd;
    }

    return [commonStart, commonEnd];
  }

  function equalLineAndChars(a, b)
  {
    if (!a) return !b;
    if (!b) return !a;
    return (a[0] == b[0] && a[1] == b[1]);
  }

  function performSelectionChange(selectStart, selectEnd, focusAtStart)
  {
    if (repSelectionChange(selectStart, selectEnd, focusAtStart))
    {
      currentCallStack.selectionAffected = true;
    }
  }
  editorInfo.ace_performSelectionChange = performSelectionChange;

  // Change the abstract representation of the document to have a different selection.
  // Should not rely on the line representation.  Should not affect the DOM.


  function repSelectionChange(selectStart, selectEnd, focusAtStart)
  {
    focusAtStart = !! focusAtStart;

    var newSelFocusAtStart = (focusAtStart && ((!selectStart) || (!selectEnd) || (selectStart[0] != selectEnd[0]) || (selectStart[1] != selectEnd[1])));

    if ((!equalLineAndChars(rep.selStart, selectStart)) || (!equalLineAndChars(rep.selEnd, selectEnd)) || (rep.selFocusAtStart != newSelFocusAtStart))
    {
      rep.selStart = selectStart;
      rep.selEnd = selectEnd;
      rep.selFocusAtStart = newSelFocusAtStart;
      currentCallStack.repChanged = true;

      // select the formatting buttons when there is the style applied on selection
      selectFormattingButtonIfLineHasStyleApplied(rep);

      hooks.callAll('aceSelectionChanged', {
        rep: rep,
        callstack: currentCallStack,
        documentAttributeManager: documentAttributeManager,
      });

      // we scroll when user places the caret at the last line of the pad
      // when this settings is enabled
      var docTextChanged = currentCallStack.docTextChanged;
      if(!docTextChanged){
       var isScrollableEvent = !isPadLoading(currentCallStack.type) && isScrollableEditEvent(currentCallStack.type);
       var innerHeight = getInnerHeight();
       scroll.scrollWhenCaretIsInTheLastLineOfViewportWhenNecessary(rep, isScrollableEvent, innerHeight);
      }

      return true;
      //console.log("selStart: %o, selEnd: %o, focusAtStart: %s", rep.selStart, rep.selEnd,
      //String(!!rep.selFocusAtStart));
    }
    return false;
    //console.log("%o %o %s", rep.selStart, rep.selEnd, rep.selFocusAtStart);
  }

  function isPadLoading(eventType)
  {
    return (eventType === 'setup') || (eventType === 'setBaseText') || (eventType === 'importText');
  }

  function updateStyleButtonState(attribName, hasStyleOnRepSelection) {
    var $formattingButton = parent.parent.$('[data-key="' + attribName + '"]').find('a');
    $formattingButton.toggleClass(SELECT_BUTTON_CLASS, hasStyleOnRepSelection);
  }

  function attribIsFormattingStyle(attributeName) {
    return _.contains(FORMATTING_STYLES, attributeName);
  }

  function selectFormattingButtonIfLineHasStyleApplied (rep) {
    _.each(FORMATTING_STYLES, function (style) {
      var hasStyleOnRepSelection = documentAttributeManager.hasAttributeOnSelectionOrCaretPosition(style);
      updateStyleButtonState(style, hasStyleOnRepSelection);
    })
  }

  function doCreateDomLine(nonEmpty)
  {
    if (browser.msie && (!nonEmpty))
    {
      var result = {
        node: null,
        appendSpan: noop,
        prepareForAdd: noop,
        notifyAdded: noop,
        clearSpans: noop,
        finishUpdate: noop,
        lineMarker: 0
      };

      var lineElem = doc.createElement("div");
      result.node = lineElem;

      result.notifyAdded = function()
      {
        // magic -- settng an empty div's innerHTML to the empty string
        // keeps it from collapsing.  Apparently innerHTML must be set *after*
        // adding the node to the DOM.
        // Such a div is what IE 6 creates naturally when you make a blank line
        // in a document of divs.  However, when copy-and-pasted the div will
        // contain a space, so we note its emptiness with a property.
        lineElem.innerHTML = " "; // Frist we set a value that isnt blank
        // a primitive-valued property survives copy-and-paste
        setAssoc(lineElem, "shouldBeEmpty", true);
        // an object property doesn't
        setAssoc(lineElem, "unpasted", {});
        lineElem.innerHTML = ""; // Then we make it blank..  New line and no space = Awesome :)
      };
      var lineClass = 'ace-line';
      result.appendSpan = function(txt, cls)
      {
        if ((!txt) && cls)
        {
          // gain a whole-line style (currently to show insertion point in CSS)
          lineClass = domline.addToLineClass(lineClass, cls);
        }
        // otherwise, ignore appendSpan, this is an empty line
      };
      result.clearSpans = function()
      {
        lineClass = ''; // non-null to cause update
      };

      var writeClass = function()
      {
        if (lineClass !== null) lineElem.className = lineClass;
      };

      result.prepareForAdd = writeClass;
      result.finishUpdate = writeClass;
      result.getInnerHTML = function()
      {
        return "";
      };
      return result;
    }
    else
    {
      return domline.createDomLine(nonEmpty, doesWrap, browser, doc);
    }
  }

  function textify(str)
  {
    return str.replace(/[\n\r ]/g, ' ').replace(/\xa0/g, ' ').replace(/\t/g, '        ');
  }

  var _blockElems = {
    "div": 1,
    "p": 1,
    "pre": 1,
    "li": 1,
    "ol": 1,
    "ul": 1
  };

  _.each(hooks.callAll('aceRegisterBlockElements'), function(element){
      _blockElems[element] = 1;
  });

  function isBlockElement(n)
  {
    return !!_blockElems[(n.tagName || "").toLowerCase()];
  }

  function getDirtyRanges()
  {
    // based on observedChanges, return a list of ranges of original lines
    // that need to be removed or replaced with new user content to incorporate
    // the user's changes into the line representation.  ranges may be zero-length,
    // indicating inserted content.  for example, [0,0] means content was inserted
    // at the top of the document, while [3,4] means line 3 was deleted, modified,
    // or replaced with one or more new lines of content. ranges do not touch.
    var p = PROFILER("getDirtyRanges", false);
    p.forIndices = 0;
    p.consecutives = 0;
    p.corrections = 0;

    var cleanNodeForIndexCache = {};
    var N = rep.lines.length(); // old number of lines


    function cleanNodeForIndex(i)
    {
      // if line (i) in the un-updated line representation maps to a clean node
      // in the document, return that node.
      // if (i) is out of bounds, return true. else return false.
      if (cleanNodeForIndexCache[i] === undefined)
      {
        p.forIndices++;
        var result;
        if (i < 0 || i >= N)
        {
          result = true; // truthy, but no actual node
        }
        else
        {
          var key = rep.lines.atIndex(i).key;
          result = (getCleanNodeByKey(key) || false);
        }
        cleanNodeForIndexCache[i] = result;
      }
      return cleanNodeForIndexCache[i];
    }
    var isConsecutiveCache = {};

    function isConsecutive(i)
    {
      if (isConsecutiveCache[i] === undefined)
      {
        p.consecutives++;
        isConsecutiveCache[i] = (function()
        {
          // returns whether line (i) and line (i-1), assumed to be map to clean DOM nodes,
          // or document boundaries, are consecutive in the changed DOM
          var a = cleanNodeForIndex(i - 1);
          var b = cleanNodeForIndex(i);
          if ((!a) || (!b)) return false; // violates precondition
          if ((a === true) && (b === true)) return !root.firstChild;
          if ((a === true) && b.previousSibling) return false;
          if ((b === true) && a.nextSibling) return false;
          if ((a === true) || (b === true)) return true;
          return a.nextSibling == b;
        })();
      }
      return isConsecutiveCache[i];
    }

    function isClean(i)
    {
      // returns whether line (i) in the un-updated representation maps to a clean node,
      // or is outside the bounds of the document
      return !!cleanNodeForIndex(i);
    }
    // list of pairs, each representing a range of lines that is clean and consecutive
    // in the changed DOM.  lines (-1) and (N) are always clean, but may or may not
    // be consecutive with lines in the document.  pairs are in sorted order.
    var cleanRanges = [
      [-1, N + 1]
    ];

    function rangeForLine(i)
    {
      // returns index of cleanRange containing i, or -1 if none
      var answer = -1;
      _.each(cleanRanges ,function(r, idx)
      {
        if (i >= r[1]) return false; // keep looking
        if (i < r[0]) return true; // not found, stop looking
        answer = idx;
        return true; // found, stop looking
      });
      return answer;
    }

    function removeLineFromRange(rng, line)
    {
      // rng is index into cleanRanges, line is line number
      // precond: line is in rng
      var a = cleanRanges[rng][0];
      var b = cleanRanges[rng][1];
      if ((a + 1) == b) cleanRanges.splice(rng, 1);
      else if (line == a) cleanRanges[rng][0]++;
      else if (line == (b - 1)) cleanRanges[rng][1]--;
      else cleanRanges.splice(rng, 1, [a, line], [line + 1, b]);
    }

    function splitRange(rng, pt)
    {
      // precond: pt splits cleanRanges[rng] into two non-empty ranges
      var a = cleanRanges[rng][0];
      var b = cleanRanges[rng][1];
      cleanRanges.splice(rng, 1, [a, pt], [pt, b]);
    }
    var correctedLines = {};

    function correctlyAssignLine(line)
    {
      if (correctedLines[line]) return true;
      p.corrections++;
      correctedLines[line] = true;
      // "line" is an index of a line in the un-updated rep.
      // returns whether line was already correctly assigned (i.e. correctly
      // clean or dirty, according to cleanRanges, and if clean, correctly
      // attached or not attached (i.e. in the same range as) the prev and next lines).
      //console.log("correctly assigning: %d", line);
      var rng = rangeForLine(line);
      var lineClean = isClean(line);
      if (rng < 0)
      {
        if (lineClean)
        {
          console.debug("somehow lost clean line");
        }
        return true;
      }
      if (!lineClean)
      {
        // a clean-range includes this dirty line, fix it
        removeLineFromRange(rng, line);
        return false;
      }
      else
      {
        // line is clean, but could be wrongly connected to a clean line
        // above or below
        var a = cleanRanges[rng][0];
        var b = cleanRanges[rng][1];
        var didSomething = false;
        // we'll leave non-clean adjacent nodes in the clean range for the caller to
        // detect and deal with.  we deal with whether the range should be split
        // just above or just below this line.
        if (a < line && isClean(line - 1) && !isConsecutive(line))
        {
          splitRange(rng, line);
          didSomething = true;
        }
        if (b > (line + 1) && isClean(line + 1) && !isConsecutive(line + 1))
        {
          splitRange(rng, line + 1);
          didSomething = true;
        }
        return !didSomething;
      }
    }

    function detectChangesAroundLine(line, reqInARow)
    {
      // make sure cleanRanges is correct about line number "line" and the surrounding
      // lines; only stops checking at end of document or after no changes need
      // making for several consecutive lines. note that iteration is over old lines,
      // so this operation takes time proportional to the number of old lines
      // that are changed or missing, not the number of new lines inserted.
      var correctInARow = 0;
      var currentIndex = line;
      while (correctInARow < reqInARow && currentIndex >= 0)
      {
        if (correctlyAssignLine(currentIndex))
        {
          correctInARow++;
        }
        else correctInARow = 0;
        currentIndex--;
      }
      correctInARow = 0;
      currentIndex = line;
      while (correctInARow < reqInARow && currentIndex < N)
      {
        if (correctlyAssignLine(currentIndex))
        {
          correctInARow++;
        }
        else correctInARow = 0;
        currentIndex++;
      }
    }

    if (N === 0)
    {
      p.cancel();
      if (!isConsecutive(0))
      {
        splitRange(0, 0);
      }
    }
    else
    {
      p.mark("topbot");
      detectChangesAroundLine(0, 1);
      detectChangesAroundLine(N - 1, 1);

      p.mark("obs");
      //console.log("observedChanges: "+toSource(observedChanges));
      for (var k in observedChanges.cleanNodesNearChanges)
      {
        var key = k.substring(1);
        if (rep.lines.containsKey(key))
        {
          var line = rep.lines.indexOfKey(key);
          detectChangesAroundLine(line, 2);
        }
      }
      p.mark("stats&calc");
      p.literal(p.forIndices, "byidx");
      p.literal(p.consecutives, "cons");
      p.literal(p.corrections, "corr");
    }

    var dirtyRanges = [];
    for (var r = 0; r < cleanRanges.length - 1; r++)
    {
      dirtyRanges.push([cleanRanges[r][1], cleanRanges[r + 1][0]]);
    }

    p.end();

    return dirtyRanges;
  }

  function markNodeClean(n)
  {
    // clean nodes have knownHTML that matches their innerHTML
    var dirtiness = {};
    dirtiness.nodeId = uniqueId(n);
    dirtiness.knownHTML = n.innerHTML;
    if (browser.msie)
    {
      // adding a space to an "empty" div in IE designMode doesn't
      // change the innerHTML of the div's parent; also, other
      // browsers don't support innerText
      dirtiness.knownText = n.innerText;
    }
    setAssoc(n, "dirtiness", dirtiness);
  }

  function isNodeDirty(n)
  {
    var p = PROFILER("cleanCheck", false);
    if (n.parentNode != root) return true;
    var data = getAssoc(n, "dirtiness");
    if (!data) return true;
    if (n.id !== data.nodeId) return true;
    if (browser.msie)
    {
      if (n.innerText !== data.knownText) return true;
    }
    if (n.innerHTML !== data.knownHTML) return true;
    p.end();
    return false;
  }

  function getViewPortTopBottom()
  {
    var theTop = scroll.getScrollY();
    var doc = outerWin.document;
    var height = doc.documentElement.clientHeight; // includes padding

    // we have to get the exactly height of the viewport. So it has to subtract all the values which changes
    // the viewport height (E.g. padding, position top)
    var viewportExtraSpacesAndPosition = getEditorPositionTop() + getPaddingTopAddedWhenPageViewIsEnable();
    return {
      top: theTop,
      bottom: (theTop + height - viewportExtraSpacesAndPosition)
    };
  }


  function getEditorPositionTop()
  {
    var editor = parent.document.getElementsByTagName('iframe');
    var editorPositionTop = editor[0].offsetTop;
    return editorPositionTop;
  }

  // ep_page_view adds padding-top, which makes the viewport smaller
  function getPaddingTopAddedWhenPageViewIsEnable()
  {
    var rootDocument = parent.parent.document;
    var aceOuter = rootDocument.getElementsByName("ace_outer");
    var aceOuterPaddingTop = parseInt($(aceOuter).css("padding-top"));
    return aceOuterPaddingTop;
  }

  function handleCut(evt)
  {
    inCallStackIfNecessary("handleCut", function()
    {
      doDeleteKey(evt);
    });
    return true;
  }

  function handleClick(evt)
  {
    inCallStackIfNecessary("handleClick", function()
    {
      idleWorkTimer.atMost(200);
    });

    function isLink(n)
    {
      return (n.tagName || '').toLowerCase() == "a" && n.href;
    }

    // only want to catch left-click
    if ((!evt.ctrlKey) && (evt.button != 2) && (evt.button != 3))
    {
      // find A tag with HREF
      var n = evt.target;
      while (n && n.parentNode && !isLink(n))
      {
        n = n.parentNode;
      }
      if (n && isLink(n))
      {
        try
        {
          var newWindow = window.open(n.href, '_blank');
          newWindow.focus();
        }
        catch (e)
        {
          // absorb "user canceled" error in IE for certain prompts
        }
        evt.preventDefault();
      }
    }

    hideEditBarDropdowns();
  }

  function hideEditBarDropdowns()
  {
    if(window.parent.parent.padeditbar){ // required in case its in an iframe should probably use parent..  See Issue 327 https://github.com/ether/etherpad-lite/issues/327
      window.parent.parent.padeditbar.toggleDropDown("none");
    }
  }

  function doReturnKey()
  {
    if (!(rep.selStart && rep.selEnd))
    {
      return;
    }

    var lineNum = rep.selStart[0];
    var listType = getLineListType(lineNum);

    if (listType)
    {
      var text = rep.lines.atIndex(lineNum).text;
      listType = /([a-z]+)([0-9]+)/.exec(listType);
      var type  = listType[1];
      var level = Number(listType[2]);

      //detect empty list item; exclude indentation
      if(text === '*' && type !== "indent")
      {
        //if not already on the highest level
        if(level > 1)
        {
          setLineListType(lineNum, type+(level-1));//automatically decrease the level
        }
        else
        {
          setLineListType(lineNum, '');//remove the list
          renumberList(lineNum + 1);//trigger renumbering of list that may be right after
        }
      }
      else if (lineNum + 1 <= rep.lines.length())
      {
        performDocumentReplaceSelection('\n');
        setLineListType(lineNum + 1, type+level);
      }
    }
    else
    {
      performDocumentReplaceSelection('\n');
      handleReturnIndentation();
    }
  }

  function doIndentOutdent(isOut)
  {
    if (!((rep.selStart && rep.selEnd) ||
        ((rep.selStart[0] == rep.selEnd[0]) && (rep.selStart[1] == rep.selEnd[1]) &&  rep.selEnd[1] > 1)) &&
        (isOut != true)
       )
    {
      return false;
    }

    var firstLine, lastLine;
    firstLine = rep.selStart[0];
    lastLine = Math.max(firstLine, rep.selEnd[0] - ((rep.selEnd[1] === 0) ? 1 : 0));
    var mods = [];
    for (var n = firstLine; n <= lastLine; n++)
    {
      var listType = getLineListType(n);
      var t = 'indent';
      var level = 0;
      if (listType)
      {
        listType = /([a-z]+)([0-9]+)/.exec(listType);
        if (listType)
        {
          t = listType[1];
          level = Number(listType[2]);
        }
      }
      var newLevel = Math.max(0, Math.min(MAX_LIST_LEVEL, level + (isOut ? -1 : 1)));
      if (level != newLevel)
      {
        mods.push([n, (newLevel > 0) ? t + newLevel : '']);
      }
    }

    _.each(mods, function(mod){
      setLineListType(mod[0], mod[1]);
    });
    return true;
  }
  editorInfo.ace_doIndentOutdent = doIndentOutdent;

  function doTabKey(shiftDown)
  {
    if (!doIndentOutdent(shiftDown))
    {
      performDocumentReplaceSelection(THE_TAB);
    }
  }

  function doDeleteKey(optEvt)
  {
    var evt = optEvt || {};
    var handled = false;
    if (rep.selStart)
    {
      if (isCaret())
      {
        var lineNum = caretLine();
        var col = caretColumn();
        var lineEntry = rep.lines.atIndex(lineNum);
        var lineText = lineEntry.text;
        var lineMarker = lineEntry.lineMarker;
        if (/^ +$/.exec(lineText.substring(lineMarker, col)))
        {
          var col2 = col - lineMarker;
          var tabSize = THE_TAB.length;
          var toDelete = ((col2 - 1) % tabSize) + 1;
          performDocumentReplaceRange([lineNum, col - toDelete], [lineNum, col], '');
          //scrollSelectionIntoView();
          handled = true;
        }
      }
      if (!handled)
      {
        if (isCaret())
        {
          var theLine = caretLine();
          var lineEntry = rep.lines.atIndex(theLine);
          if (caretColumn() <= lineEntry.lineMarker)
          {
            // delete at beginning of line
            var action = 'delete_newline';
            var prevLineListType = (theLine > 0 ? getLineListType(theLine - 1) : '');
            var thisLineListType = getLineListType(theLine);
            var prevLineEntry = (theLine > 0 && rep.lines.atIndex(theLine - 1));
            var prevLineBlank = (prevLineEntry && prevLineEntry.text.length == prevLineEntry.lineMarker);

            var thisLineHasMarker = documentAttributeManager.lineHasMarker(theLine);

            if (thisLineListType)
            {
              // this line is a list
              if (prevLineBlank && !prevLineListType)
              {
                // previous line is blank, remove it
                performDocumentReplaceRange([theLine - 1, prevLineEntry.text.length], [theLine, 0], '');
              }
              else
              {
                // delistify
                performDocumentReplaceRange([theLine, 0], [theLine, lineEntry.lineMarker], '');
              }
            }else if (thisLineHasMarker && prevLineEntry){
              // If the line has any attributes assigned, remove them by removing the marker '*'
              performDocumentReplaceRange([theLine -1 , prevLineEntry.text.length], [theLine, lineEntry.lineMarker], '');
            }
            else if (theLine > 0)
            {
              // remove newline
              performDocumentReplaceRange([theLine - 1, prevLineEntry.text.length], [theLine, 0], '');
            }
          }
          else
          {
            var docChar = caretDocChar();
            if (docChar > 0)
            {
              if (evt.metaKey || evt.ctrlKey || evt.altKey)
              {
                // delete as many unicode "letters or digits" in a row as possible;
                // always delete one char, delete further even if that first char
                // isn't actually a word char.
                var deleteBackTo = docChar - 1;
                while (deleteBackTo > lineEntry.lineMarker && isWordChar(rep.alltext.charAt(deleteBackTo - 1)))
                {
                  deleteBackTo--;
                }
                performDocumentReplaceCharRange(deleteBackTo, docChar, '');
              }
              else
              {
                // normal delete
                performDocumentReplaceCharRange(docChar - 1, docChar, '');
              }
            }
          }
        }
        else
        {
          performDocumentReplaceSelection('');
        }
      }
    }
     //if the list has been removed, it is necessary to renumber
    //starting from the *next* line because the list may have been
    //separated. If it returns null, it means that the list was not cut, try
    //from the current one.
    var line = caretLine();
    if(line != -1 && renumberList(line+1) === null)
    {
      renumberList(line);
    }
  }

  // set of "letter or digit" chars is based on section 20.5.16 of the original Java Language Spec
  var REGEX_WORDCHAR = /[\u0030-\u0039\u0041-\u005A\u0061-\u007A\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\u0100-\u1FFF\u3040-\u9FFF\uF900-\uFDFF\uFE70-\uFEFE\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFDC]/;
  var REGEX_SPACE = /\s/;

  function isWordChar(c)
  {
    return !!REGEX_WORDCHAR.exec(c);
  }
  editorInfo.ace_isWordChar = isWordChar;

  function isSpaceChar(c)
  {
    return !!REGEX_SPACE.exec(c);
  }

  function moveByWordInLine(lineText, initialIndex, forwardNotBack)
  {
    var i = initialIndex;

    function nextChar()
    {
      if (forwardNotBack) return lineText.charAt(i);
      else return lineText.charAt(i - 1);
    }

    function advance()
    {
      if (forwardNotBack) i++;
      else i--;
    }

    function isDone()
    {
      if (forwardNotBack) return i >= lineText.length;
      else return i <= 0;
    }

    // On Mac and Linux, move right moves to end of word and move left moves to start;
    // on Windows, always move to start of word.
    // On Windows, Firefox and IE disagree on whether to stop for punctuation (FF says no).
    if (browser.msie && forwardNotBack)
    {
      while ((!isDone()) && isWordChar(nextChar()))
      {
        advance();
      }
      while ((!isDone()) && !isWordChar(nextChar()))
      {
        advance();
      }
    }
    else
    {
      while ((!isDone()) && !isWordChar(nextChar()))
      {
        advance();
      }
      while ((!isDone()) && isWordChar(nextChar()))
      {
        advance();
      }
    }

    return i;
  }

  function handleKeyEvent(evt)
  {
    // if (DEBUG && window.DONT_INCORP) return;
    if (!isEditable) return;
    var type = evt.type;
    var charCode = evt.charCode;
    var keyCode = evt.keyCode;
    var which = evt.which;
    var altKey = evt.altKey;
    var shiftKey = evt.shiftKey;

    // Is caret potentially hidden by the chat button?
    var myselection = document.getSelection(); // get the current caret selection
    var caretOffsetTop = myselection.focusNode.parentNode.offsetTop | myselection.focusNode.offsetTop; // get the carets selection offset in px IE 214

    if(myselection.focusNode.wholeText){ // Is there any content?  If not lineHeight will report wrong..
      var lineHeight = myselection.focusNode.parentNode.offsetHeight; // line height of populated links
    }else{
      var lineHeight = myselection.focusNode.offsetHeight; // line height of blank lines
    }

    var heightOfChatIcon = parent.parent.$('#chaticon').height(); // height of the chat icon button
    lineHeight = (lineHeight *2) + heightOfChatIcon;
    var viewport = getViewPortTopBottom();
    var viewportHeight = viewport.bottom - viewport.top - lineHeight;
    var relCaretOffsetTop = caretOffsetTop - viewport.top; // relative Caret Offset Top to viewport
    if (viewportHeight < relCaretOffsetTop){
      parent.parent.$("#chaticon").css("opacity",".3"); // make chaticon opacity low when user types near it
    }else{
      parent.parent.$("#chaticon").css("opacity","1"); // make chaticon opacity back to full (so fully visible)
    }

    //dmesg("keyevent type: "+type+", which: "+which);
    // Don't take action based on modifier keys going up and down.
    // Modifier keys do not generate "keypress" events.
    // 224 is the command-key under Mac Firefox.
    // 91 is the Windows key in IE; it is ASCII for open-bracket but isn't the keycode for that key
    // 20 is capslock in IE.
    var isModKey = ((!charCode) && ((type == "keyup") || (type == "keydown")) && (keyCode == 16 || keyCode == 17 || keyCode == 18 || keyCode == 20 || keyCode == 224 || keyCode == 91));
    if (isModKey) return;

    // If the key is a keypress and the browser is opera and the key is enter, do nothign at all as this fires twice.
    if (keyCode == 13 && browser.opera && (type == "keypress")){
      return; // This stops double enters in Opera but double Tabs still show on single tab keypress, adding keyCode == 9 to this doesn't help as the event is fired twice
    }
    var specialHandled = false;
    var isTypeForSpecialKey = ((browser.msie || browser.safari || browser.chrome) ? (type == "keydown") : (type == "keypress"));
    var isTypeForCmdKey = ((browser.msie || browser.safari || browser.chrome) ? (type == "keydown") : (type == "keypress"));
    var stopped = false;

    inCallStackIfNecessary("handleKeyEvent", function()
    {
      if (type == "keypress" || (isTypeForSpecialKey && keyCode == 13 /*return*/ ))
      {
        // in IE, special keys don't send keypress, the keydown does the action
        if (!outsideKeyPress(evt))
        {
          evt.preventDefault();
          stopped = true;
        }
      }
      else if (evt.key === "Dead"){
        // If it's a dead key we don't want to do any Etherpad behavior.
        stopped = true;
        return true;
      }
      else if (type == "keydown")
      {
        outsideKeyDown(evt);
      }
      if (!stopped)
      {
        var specialHandledInHook = hooks.callAll('aceKeyEvent', {
          callstack: currentCallStack,
          editorInfo: editorInfo,
          rep: rep,
          documentAttributeManager: documentAttributeManager,
          evt:evt
        });

        // if any hook returned true, set specialHandled with true
        if (specialHandledInHook) {
          specialHandled = _.contains(specialHandledInHook, true);
        }

        var padShortcutEnabled = parent.parent.clientVars.padShortcutEnabled;
        if ((!specialHandled) && altKey && isTypeForSpecialKey && keyCode == 120 && padShortcutEnabled.altF9){
          // Alt F9 focuses on the File Menu and/or editbar.
          // Note that while most editors use Alt F10 this is not desirable
          // As ubuntu cannot use Alt F10....
          // Focus on the editbar. -- TODO: Move Focus back to previous state (we know it so we can use it)
          var firstEditbarElement = parent.parent.$('#editbar').children("ul").first().children().first().children().first().children().first();
          $(this).blur();
          firstEditbarElement.focus();
          evt.preventDefault();
        }
        if ((!specialHandled) && altKey && keyCode == 67 && type === "keydown" && padShortcutEnabled.altC){
          // Alt c focuses on the Chat window
          $(this).blur();
          parent.parent.chat.show();
          parent.parent.$("#chatinput").focus();
          evt.preventDefault();
        }
        if ((!specialHandled) && evt.ctrlKey && shiftKey && keyCode == 50 && type === "keydown" && padShortcutEnabled.cmdShift2){
          // Control-Shift-2 shows a gritter popup showing a line author
          var lineNumber = rep.selEnd[0];
          var alineAttrs = rep.alines[lineNumber];
          var apool = rep.apool;

          // TODO: support selection ranges
          // TODO: Still work when authorship colors have been cleared
          // TODO: i18n
          // TODO: There appears to be a race condition or so.

          var author = null;
          if (alineAttrs) {
            var authors = [];
            var authorNames = [];
            var opIter = Changeset.opIterator(alineAttrs);

            while (opIter.hasNext()){
              var op = opIter.next();
              authorId = Changeset.opAttributeValue(op, 'author', apool);

              // Only push unique authors and ones with values
              if(authors.indexOf(authorId) === -1 && authorId !== ""){
                authors.push(authorId);
              }

            }

          }

          // No author information is available IE on a new pad.
          if(authors.length === 0){
            var authorString = "No author information is available";
          }
          else{
            // Known authors info, both current and historical
            var padAuthors = parent.parent.pad.userList();
            var authorObj = {};
            authors.forEach(function(authorId){
              padAuthors.forEach(function(padAuthor){
                // If the person doing the lookup is the author..
                if(padAuthor.userId === authorId){
                  if(parent.parent.clientVars.userId === authorId){
                    authorObj = {
                      name: "Me"
                    }
                  }else{
                    authorObj = padAuthor;
                  }
                }
              });
              if(!authorObj){
                author = "Unknown";
                return;
              }
              author = authorObj.name;
              if(!author) author = "Unknown";
              authorNames.push(author);
            })
          }
          if(authors.length === 1){
            var authorString = "The author of this line is " + authorNames;
          }
          if(authors.length > 1){
            var authorString = "The authors of this line are " + authorNames.join(" & ");
	  }

          parent.parent.$.gritter.add({
            // (string | mandatory) the heading of the notification
            title: 'Line Authors',
            // (string | mandatory) the text inside the notification
            text: authorString,
            // (bool | optional) if you want it to fade out on its own or just sit there
            sticky: false,
            // (int | optional) the time you want it to be alive for before fading out
            time: '4000'
          });
        }
        if ((!specialHandled) && isTypeForSpecialKey && keyCode == 8 && padShortcutEnabled.delete)
        {
          // "delete" key; in mozilla, if we're at the beginning of a line, normalize now,
          // or else deleting a blank line can take two delete presses.
          // --
          // we do deletes completely customly now:
          //  - allows consistent (and better) meta-delete behavior
          //  - normalizing and then allowing default behavior confused IE
          //  - probably eliminates a few minor quirks
          fastIncorp(3);
          evt.preventDefault();
          doDeleteKey(evt);
          specialHandled = true;
        }
        if ((!specialHandled) && isTypeForSpecialKey && keyCode == 13 && padShortcutEnabled.return)
        {
          // return key, handle specially;
          // note that in mozilla we need to do an incorporation for proper return behavior anyway.
          fastIncorp(4);
          evt.preventDefault();
          doReturnKey();
          //scrollSelectionIntoView();
          scheduler.setTimeout(function()
          {
            outerWin.scrollBy(-100, 0);
          }, 0);
          specialHandled = true;
        }
        if ((!specialHandled) && isTypeForSpecialKey && keyCode == 27 && padShortcutEnabled.esc)
        {
          // prevent esc key;
          // in mozilla versions 14-19 avoid reconnecting pad.

          fastIncorp(4);
          evt.preventDefault();
          specialHandled = true;
        }
        if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "s" && (evt.metaKey || evt.ctrlKey) && !evt.altKey && padShortcutEnabled.cmdS) /* Do a saved revision on ctrl S */
        {
          evt.preventDefault();
          var originalBackground = parent.parent.$('#revisionlink').css("background")
          parent.parent.$('#revisionlink').css({"background":"lightyellow"});
          scheduler.setTimeout(function(){
            parent.parent.$('#revisionlink').css({"background":originalBackground});
          }, 1000);
          parent.parent.pad.collabClient.sendMessage({"type":"SAVE_REVISION"}); /* The parent.parent part of this is BAD and I feel bad..  It may break something */
          specialHandled = true;
        }
        if ((!specialHandled) && isTypeForSpecialKey && keyCode == 9 && !(evt.metaKey || evt.ctrlKey) && padShortcutEnabled.tab)
        {
          // tab
          fastIncorp(5);
          evt.preventDefault();
          doTabKey(evt.shiftKey);
          //scrollSelectionIntoView();
          specialHandled = true;
        }
        if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "z" && (evt.metaKey || evt.ctrlKey) && !evt.altKey && padShortcutEnabled.cmdZ)
        {
          // cmd-Z (undo)
          fastIncorp(6);
          evt.preventDefault();
          if (evt.shiftKey)
          {
            doUndoRedo("redo");
          }
          else
          {
            doUndoRedo("undo");
          }
          specialHandled = true;
        }
        if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "y" && (evt.metaKey || evt.ctrlKey) && padShortcutEnabled.cmdY)
        {
          // cmd-Y (redo)
          fastIncorp(10);
          evt.preventDefault();
          doUndoRedo("redo");
          specialHandled = true;
        }
        if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "b" && (evt.metaKey || evt.ctrlKey) && padShortcutEnabled.cmdB)
        {
          // cmd-B (bold)
          fastIncorp(13);
          evt.preventDefault();
          toggleAttributeOnSelection('bold');
          specialHandled = true;
        }
        if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "i" && (evt.metaKey || evt.ctrlKey) && padShortcutEnabled.cmdI)
        {
          // cmd-I (italic)
          fastIncorp(14);
          evt.preventDefault();
          toggleAttributeOnSelection('italic');
          specialHandled = true;
        }
        if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "u" && (evt.metaKey || evt.ctrlKey) && padShortcutEnabled.cmdU)
        {
          // cmd-U (underline)
          fastIncorp(15);
          evt.preventDefault();
          toggleAttributeOnSelection('underline');
          specialHandled = true;
        }
        if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "5" && (evt.metaKey || evt.ctrlKey) && evt.altKey !== true && padShortcutEnabled.cmd5)
        {
          // cmd-5 (strikethrough)
          fastIncorp(13);
          evt.preventDefault();
          toggleAttributeOnSelection('strikethrough');
          specialHandled = true;
        }
        if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "l" && (evt.metaKey || evt.ctrlKey) && evt.shiftKey && padShortcutEnabled.cmdShiftL)
        {
          // cmd-shift-L (unorderedlist)
          fastIncorp(9);
          evt.preventDefault();
          doInsertUnorderedList()
          specialHandled = true;
	}
        if ((!specialHandled) && isTypeForCmdKey && ((String.fromCharCode(which).toLowerCase() == "n" && padShortcutEnabled.cmdShiftN) || (String.fromCharCode(which) == 1 && padShortcutEnabled.cmdShift1)) && (evt.metaKey || evt.ctrlKey) && evt.shiftKey)
        {
          // cmd-shift-N and cmd-shift-1 (orderedlist)
          fastIncorp(9);
          evt.preventDefault();
          doInsertOrderedList()
          specialHandled = true;
	}
        if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "c" && (evt.metaKey || evt.ctrlKey) && evt.shiftKey && padShortcutEnabled.cmdShiftC) {
          // cmd-shift-C (clearauthorship)
          fastIncorp(9);
          evt.preventDefault();
          CMDS.clearauthorship();
        }
        if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "h" && (evt.ctrlKey) && padShortcutEnabled.cmdH)
        {
          // cmd-H (backspace)
          fastIncorp(20);
          evt.preventDefault();
          doDeleteKey();
          specialHandled = true;
        }
        if((evt.which == 36 && evt.ctrlKey == true) && padShortcutEnabled.ctrlHome){ scroll.setScrollY(0); } // Control Home send to Y = 0
        if((evt.which == 33 || evt.which == 34) && type == 'keydown' && !evt.ctrlKey){

          evt.preventDefault(); // This is required, browsers will try to do normal default behavior on page up / down and the default behavior SUCKS

          var oldVisibleLineRange = scroll.getVisibleLineRange(rep);
          var topOffset = rep.selStart[0] - oldVisibleLineRange[0];
          if(topOffset < 0 ){
            topOffset = 0;
          }

          var isPageDown = evt.which === 34;
          var isPageUp = evt.which === 33;

          scheduler.setTimeout(function(){
            var newVisibleLineRange = scroll.getVisibleLineRange(rep); // the visible lines IE 1,10
            var linesCount = rep.lines.length(); // total count of lines in pad IE 10
            var numberOfLinesInViewport = newVisibleLineRange[1] - newVisibleLineRange[0]; // How many lines are in the viewport right now?

            if(isPageUp && padShortcutEnabled.pageUp){
              rep.selEnd[0] = rep.selEnd[0] - numberOfLinesInViewport; // move to the bottom line +1 in the viewport (essentially skipping over a page)
              rep.selStart[0] = rep.selStart[0] - numberOfLinesInViewport; // move to the bottom line +1 in the viewport (essentially skipping over a page)
            }

            if(isPageDown && padShortcutEnabled.pageDown){ // if we hit page down
              if(rep.selEnd[0] >= oldVisibleLineRange[0]){ // If the new viewpoint position is actually further than where we are right now
                rep.selStart[0] = oldVisibleLineRange[1] -1; // dont go further in the page down than what's visible IE go from 0 to 50 if 50 is visible on screen but dont go below that else we miss content
                rep.selEnd[0] = oldVisibleLineRange[1] -1; // dont go further in the page down than what's visible IE go from 0 to 50 if 50 is visible on screen but dont go below that else we miss content
              }
            }

            //ensure min and max
            if(rep.selEnd[0] < 0){
              rep.selEnd[0] = 0;
            }
            if(rep.selStart[0] < 0){
              rep.selStart[0] = 0;
            }
            if(rep.selEnd[0] >= linesCount){
              rep.selEnd[0] = linesCount-1;
            }
            updateBrowserSelectionFromRep();
            var myselection = document.getSelection(); // get the current caret selection, can't use rep. here because that only gives us the start position not the current
            var caretOffsetTop = myselection.focusNode.parentNode.offsetTop || myselection.focusNode.offsetTop; // get the carets selection offset in px IE 214

            // sometimes the first selection is -1 which causes problems (Especially with ep_page_view)
            // so use focusNode.offsetTop value.
            if(caretOffsetTop === -1) caretOffsetTop = myselection.focusNode.offsetTop;
            scroll.setScrollY(caretOffsetTop); // set the scrollY offset of the viewport on the document

          }, 200);
        }

        // scroll to viewport when user presses arrow keys and caret is out of the viewport
        if((evt.which == 37 || evt.which == 38 || evt.which == 39 || evt.which == 40)){
          // we use arrowKeyWasReleased to avoid triggering the animation when a key is continuously pressed
          // this makes the scroll smooth
          if(!continuouslyPressingArrowKey(type)){
            // We use getSelection() instead of rep to get the caret position. This avoids errors like when
            // the caret position is not synchronized with the rep. For example, when an user presses arrow
            // down to scroll the pad without releasing the key. When the key is released the rep is not
            // synchronized, so we don't get the right node where caret is.
            var selection = getSelection();

            if(selection){
              var arrowUp = evt.which === 38;
              var innerHeight = getInnerHeight();
              scroll.scrollWhenPressArrowKeys(arrowUp, rep, innerHeight);
            }
          }
        }
      }

      if (type == "keydown")
      {
        idleWorkTimer.atLeast(500);
      }
      else if (type == "keypress")
      {
        if ((!specialHandled) && false /*parenModule.shouldNormalizeOnChar(charCode)*/)
        {
          idleWorkTimer.atMost(0);
        }
        else
        {
          idleWorkTimer.atLeast(500);
        }
      }
      else if (type == "keyup")
      {
        var wait = 0;
        idleWorkTimer.atLeast(wait);
        idleWorkTimer.atMost(wait);
      }

      // Is part of multi-keystroke international character on Firefox Mac
      var isFirefoxHalfCharacter = (browser.firefox && evt.altKey && charCode === 0 && keyCode === 0);

      // Is part of multi-keystroke international character on Safari Mac
      var isSafariHalfCharacter = (browser.safari && evt.altKey && keyCode == 229);

      if (thisKeyDoesntTriggerNormalize || isFirefoxHalfCharacter || isSafariHalfCharacter)
      {
        idleWorkTimer.atLeast(3000); // give user time to type
        // if this is a keydown, e.g., the keyup shouldn't trigger a normalize
        thisKeyDoesntTriggerNormalize = true;
      }

      if ((!specialHandled) && (!thisKeyDoesntTriggerNormalize) && (!inInternationalComposition))
      {
        if (type != "keyup")
        {
          observeChangesAroundSelection();
        }
      }

      if (type == "keyup")
      {
        thisKeyDoesntTriggerNormalize = false;
      }
    });
  }

  var thisKeyDoesntTriggerNormalize = false;

  var arrowKeyWasReleased = true;
  function continuouslyPressingArrowKey(type) {
    var firstTimeKeyIsContinuouslyPressed = false;

    if (type == 'keyup') arrowKeyWasReleased = true;
    else if (type == 'keydown' && arrowKeyWasReleased) {
      firstTimeKeyIsContinuouslyPressed = true;
      arrowKeyWasReleased = false;
    }

    return !firstTimeKeyIsContinuouslyPressed;
  }

  function doUndoRedo(which)
  {
    // precond: normalized DOM
    if (undoModule.enabled)
    {
      var whichMethod;
      if (which == "undo") whichMethod = 'performUndo';
      if (which == "redo") whichMethod = 'performRedo';
      if (whichMethod)
      {
        var oldEventType = currentCallStack.editEvent.eventType;
        currentCallStack.startNewEvent(which);
        undoModule[whichMethod](function(backset, selectionInfo)
        {
          if (backset)
          {
            performDocumentApplyChangeset(backset);
          }
          if (selectionInfo)
          {
            performSelectionChange(lineAndColumnFromChar(selectionInfo.selStart), lineAndColumnFromChar(selectionInfo.selEnd), selectionInfo.selFocusAtStart);
          }
          var oldEvent = currentCallStack.startNewEvent(oldEventType, true);
          return oldEvent;
        });
      }
    }
  }
  editorInfo.ace_doUndoRedo = doUndoRedo;

  function updateBrowserSelectionFromRep()
  {
    // requires normalized DOM!
    var selStart = rep.selStart,
        selEnd = rep.selEnd;

    if (!(selStart && selEnd))
    {
      setSelection(null);
      return;
    }

    var selection = {};

    var ss = [selStart[0], selStart[1]];
    selection.startPoint = getPointForLineAndChar(ss);

    var se = [selEnd[0], selEnd[1]];
    selection.endPoint = getPointForLineAndChar(se);

    selection.focusAtStart = !! rep.selFocusAtStart;
    setSelection(selection);
  }
  editorInfo.ace_updateBrowserSelectionFromRep = updateBrowserSelectionFromRep;

  function nodeMaxIndex(nd)
  {
    if (isNodeText(nd)) return nd.nodeValue.length;
    else return 1;
  }

  function hasIESelection()
  {
    var browserSelection;
    try
    {
      browserSelection = doc.selection;
    }
    catch (e)
    {}
    if (!browserSelection) return false;
    var origSelectionRange;
    try
    {
      origSelectionRange = browserSelection.createRange();
    }
    catch (e)
    {}
    if (!origSelectionRange) return false;
    return true;
  }

  function getSelection()
  {
    // returns null, or a structure containing startPoint and endPoint,
    // each of which has node (a magicdom node), index, and maxIndex.  If the node
    // is a text node, maxIndex is the length of the text; else maxIndex is 1.
    // index is between 0 and maxIndex, inclusive.
    if (browser.msie)
    {
      var browserSelection;
      try
      {
        browserSelection = doc.selection;
      }
      catch (e)
      {}
      if (!browserSelection) return null;
      var origSelectionRange;
      try
      {
        origSelectionRange = browserSelection.createRange();
      }
      catch (e)
      {}
      if (!origSelectionRange) return null;
      var selectionParent = origSelectionRange.parentElement();
      if (selectionParent.ownerDocument != doc) return null;

      var newRange = function()
      {
        return doc.body.createTextRange();
      };

      var rangeForElementNode = function(nd)
      {
        var rng = newRange();
        // doesn't work on text nodes
        rng.moveToElementText(nd);
        return rng;
      };

      var pointFromCollapsedRange = function(rng)
      {
        var parNode = rng.parentElement();
        var elemBelow = -1;
        var elemAbove = parNode.childNodes.length;
        var rangeWithin = rangeForElementNode(parNode);

        if (rng.compareEndPoints("StartToStart", rangeWithin) === 0)
        {
          return {
            node: parNode,
            index: 0,
            maxIndex: 1
          };
        }
        else if (rng.compareEndPoints("EndToEnd", rangeWithin) === 0)
        {
          if (isBlockElement(parNode) && parNode.nextSibling)
          {
            // caret after block is not consistent across browsers
            // (same line vs next) so put caret before next node
            return {
              node: parNode.nextSibling,
              index: 0,
              maxIndex: 1
            };
          }
          return {
            node: parNode,
            index: 1,
            maxIndex: 1
          };
        }
        else if (parNode.childNodes.length === 0)
        {
          return {
            node: parNode,
            index: 0,
            maxIndex: 1
          };
        }

        for (var i = 0; i < parNode.childNodes.length; i++)
        {
          var n = parNode.childNodes.item(i);
          if (!isNodeText(n))
          {
            var nodeRange = rangeForElementNode(n);
            var startComp = rng.compareEndPoints("StartToStart", nodeRange);
            var endComp = rng.compareEndPoints("EndToEnd", nodeRange);
            if (startComp >= 0 && endComp <= 0)
            {
              var index = 0;
              if (startComp > 0)
              {
                index = 1;
              }
              return {
                node: n,
                index: index,
                maxIndex: 1
              };
            }
            else if (endComp > 0)
            {
              if (i > elemBelow)
              {
                elemBelow = i;
                rangeWithin.setEndPoint("StartToEnd", nodeRange);
              }
            }
            else if (startComp < 0)
            {
              if (i < elemAbove)
              {
                elemAbove = i;
                rangeWithin.setEndPoint("EndToStart", nodeRange);
              }
            }
          }
        }
        if ((elemAbove - elemBelow) == 1)
        {
          if (elemBelow >= 0)
          {
            return {
              node: parNode.childNodes.item(elemBelow),
              index: 1,
              maxIndex: 1
            };
          }
          else
          {
            return {
              node: parNode.childNodes.item(elemAbove),
              index: 0,
              maxIndex: 1
            };
          }
        }
        var idx = 0;
        var r = rng.duplicate();
        // infinite stateful binary search! call function for values 0 to inf,
        // expecting the answer to be about 40.  return index of smallest
        // true value.
        var indexIntoRange = binarySearchInfinite(40, function(i)
        {
          // the search algorithm whips the caret back and forth,
          // though it has to be moved relatively and may hit
          // the end of the buffer
          var delta = i - idx;
          var moved = Math.abs(r.move("character", -delta));
          // next line is work-around for fact that when moving left, the beginning
          // of a text node is considered to be after the start of the parent element:
          if (r.move("character", -1)) r.move("character", 1);
          if (delta < 0) idx -= moved;
          else idx += moved;
          return (r.compareEndPoints("StartToStart", rangeWithin) <= 0);
        });
        // iterate over consecutive text nodes, point is in one of them
        var textNode = elemBelow + 1;
        var indexLeft = indexIntoRange;
        while (textNode < elemAbove)
        {
          var tn = parNode.childNodes.item(textNode);
          if (indexLeft <= tn.nodeValue.length)
          {
            return {
              node: tn,
              index: indexLeft,
              maxIndex: tn.nodeValue.length
            };
          }
          indexLeft -= tn.nodeValue.length;
          textNode++;
        }
        var tn = parNode.childNodes.item(textNode - 1);
        return {
          node: tn,
          index: tn.nodeValue.length,
          maxIndex: tn.nodeValue.length
        };
      };

      var selection = {};
      if (origSelectionRange.compareEndPoints("StartToEnd", origSelectionRange) === 0)
      {
        // collapsed
        var pnt = pointFromCollapsedRange(origSelectionRange);
        selection.startPoint = pnt;
        selection.endPoint = {
          node: pnt.node,
          index: pnt.index,
          maxIndex: pnt.maxIndex
        };
      }
      else
      {
        var start = origSelectionRange.duplicate();
        start.collapse(true);
        var end = origSelectionRange.duplicate();
        end.collapse(false);
        selection.startPoint = pointFromCollapsedRange(start);
        selection.endPoint = pointFromCollapsedRange(end);
      }
      return selection;
    }
    else
    {
      // non-IE browser
      var browserSelection = window.getSelection();
      if (browserSelection && browserSelection.type != "None" && browserSelection.rangeCount !== 0)
      {
        var range = browserSelection.getRangeAt(0);

        function isInBody(n)
        {
          while (n && !(n.tagName && n.tagName.toLowerCase() == "body"))
          {
            n = n.parentNode;
          }
          return !!n;
        }

        function pointFromRangeBound(container, offset)
        {
          if (!isInBody(container))
          {
            // command-click in Firefox selects whole document, HEAD and BODY!
            return {
              node: root,
              index: 0,
              maxIndex: 1
            };
          }
          var n = container;
          var childCount = n.childNodes.length;
          if (isNodeText(n))
          {
            return {
              node: n,
              index: offset,
              maxIndex: n.nodeValue.length
            };
          }
          else if (childCount === 0)
          {
            return {
              node: n,
              index: 0,
              maxIndex: 1
            };
          }
          // treat point between two nodes as BEFORE the second (rather than after the first)
          // if possible; this way point at end of a line block-element is treated as
          // at beginning of next line
          else if (offset == childCount)
          {
            var nd = n.childNodes.item(childCount - 1);
            var max = nodeMaxIndex(nd);
            return {
              node: nd,
              index: max,
              maxIndex: max
            };
          }
          else
          {
            var nd = n.childNodes.item(offset);
            var max = nodeMaxIndex(nd);
            return {
              node: nd,
              index: 0,
              maxIndex: max
            };
          }
        }
        var selection = {};
        selection.startPoint = pointFromRangeBound(range.startContainer, range.startOffset);
        selection.endPoint = pointFromRangeBound(range.endContainer, range.endOffset);
        selection.focusAtStart = (((range.startContainer != range.endContainer) || (range.startOffset != range.endOffset)) && browserSelection.anchorNode && (browserSelection.anchorNode == range.endContainer) && (browserSelection.anchorOffset == range.endOffset));

        if(selection.startPoint.node.ownerDocument !== window.document){
          return null;
        }

        return selection;
      }
      else return null;
    }
  }

  function setSelection(selection)
  {
    function copyPoint(pt)
    {
      return {
        node: pt.node,
        index: pt.index,
        maxIndex: pt.maxIndex
      };
    }
    if (browser.msie)
    {
      // Oddly enough, accessing scrollHeight fixes return key handling on IE 8,
      // presumably by forcing some kind of internal DOM update.
      doc.body.scrollHeight;

      function moveToElementText(s, n)
      {
        while (n.firstChild && !isNodeText(n.firstChild))
        {
          n = n.firstChild;
        }
        s.moveToElementText(n);
      }

      function newRange()
      {
        return doc.body.createTextRange();
      }

      function setCollapsedBefore(s, n)
      {
        // s is an IE TextRange, n is a dom node
        if (isNodeText(n))
        {
          // previous node should not also be text, but prevent inf recurs
          if (n.previousSibling && !isNodeText(n.previousSibling))
          {
            setCollapsedAfter(s, n.previousSibling);
          }
          else
          {
            setCollapsedBefore(s, n.parentNode);
          }
        }
        else
        {
          moveToElementText(s, n);
          // work around for issue that caret at beginning of line
          // somehow ends up at end of previous line
          if (s.move('character', 1))
          {
            s.move('character', -1);
          }
          s.collapse(true); // to start
        }
      }

      function setCollapsedAfter(s, n)
      {
        // s is an IE TextRange, n is a magicdom node
        if (isNodeText(n))
        {
          // can't use end of container when no nextSibling (could be on next line),
          // so use previousSibling or start of container and move forward.
          setCollapsedBefore(s, n);
          s.move("character", n.nodeValue.length);
        }
        else
        {
          moveToElementText(s, n);
          s.collapse(false); // to end
        }
      }

      function getPointRange(point)
      {
        var s = newRange();
        var n = point.node;
        if (isNodeText(n))
        {
          setCollapsedBefore(s, n);
          s.move("character", point.index);
        }
        else if (point.index === 0)
        {
          setCollapsedBefore(s, n);
        }
        else
        {
          setCollapsedAfter(s, n);
        }
        return s;
      }

      if (selection)
      {
        if (!hasIESelection())
        {
          return; // don't steal focus
        }

        var startPoint = copyPoint(selection.startPoint);
        var endPoint = copyPoint(selection.endPoint);

        // fix issue where selection can't be extended past end of line
        // with shift-rightarrow or shift-downarrow
        if (endPoint.index == endPoint.maxIndex && endPoint.node.nextSibling)
        {
          endPoint.node = endPoint.node.nextSibling;
          endPoint.index = 0;
          endPoint.maxIndex = nodeMaxIndex(endPoint.node);
        }
        var range = getPointRange(startPoint);
        range.setEndPoint("EndToEnd", getPointRange(endPoint));

        // setting the selection in IE causes everything to scroll
        // so that the selection is visible.  if setting the selection
        // definitely accomplishes nothing, don't do it.


        function isEqualToDocumentSelection(rng)
        {
          var browserSelection;
          try
          {
            browserSelection = doc.selection;
          }
          catch (e)
          {}
          if (!browserSelection) return false;
          var rng2 = browserSelection.createRange();
          if (rng2.parentElement().ownerDocument != doc) return false;
          if (rng.compareEndPoints("StartToStart", rng2) !== 0) return false;
          if (rng.compareEndPoints("EndToEnd", rng2) !== 0) return false;
          return true;
        }
        if (!isEqualToDocumentSelection(range))
        {
          //dmesg(toSource(selection));
          //dmesg(escapeHTML(doc.body.innerHTML));
          range.select();
        }
      }
      else
      {
        try
        {
          doc.selection.empty();
        }
        catch (e)
        {}
      }
    }
    else
    {
      // non-IE browser
      var isCollapsed;

      function pointToRangeBound(pt)
      {
        var p = copyPoint(pt);
        // Make sure Firefox cursor is deep enough; fixes cursor jumping when at top level,
        // and also problem where cut/copy of a whole line selected with fake arrow-keys
        // copies the next line too.
        if (isCollapsed)
        {
          function diveDeep()
          {
            while (p.node.childNodes.length > 0)
            {
              //&& (p.node == root || p.node.parentNode == root)) {
              if (p.index === 0)
              {
                p.node = p.node.firstChild;
                p.maxIndex = nodeMaxIndex(p.node);
              }
              else if (p.index == p.maxIndex)
              {
                p.node = p.node.lastChild;
                p.maxIndex = nodeMaxIndex(p.node);
                p.index = p.maxIndex;
              }
              else break;
            }
          }
          // now fix problem where cursor at end of text node at end of span-like element
          // with background doesn't seem to show up...
          if (isNodeText(p.node) && p.index == p.maxIndex)
          {
            var n = p.node;
            while ((!n.nextSibling) && (n != root) && (n.parentNode != root))
            {
              n = n.parentNode;
            }
            if (n.nextSibling && (!((typeof n.nextSibling.tagName) == "string" && n.nextSibling.tagName.toLowerCase() == "br")) && (n != p.node) && (n != root) && (n.parentNode != root))
            {
              // found a parent, go to next node and dive in
              p.node = n.nextSibling;
              p.maxIndex = nodeMaxIndex(p.node);
              p.index = 0;
              diveDeep();
            }
          }
          // try to make sure insertion point is styled;
          // also fixes other FF problems
          if (!isNodeText(p.node))
          {
            diveDeep();
          }
        }
        if (isNodeText(p.node))
        {
          return {
            container: p.node,
            offset: p.index
          };
        }
        else
        {
          // p.index in {0,1}
          return {
            container: p.node.parentNode,
            offset: childIndex(p.node) + p.index
          };
        }
      }
      var browserSelection = window.getSelection();
      if (browserSelection)
      {
        browserSelection.removeAllRanges();
        if (selection)
        {
          isCollapsed = (selection.startPoint.node === selection.endPoint.node && selection.startPoint.index === selection.endPoint.index);
          var start = pointToRangeBound(selection.startPoint);
          var end = pointToRangeBound(selection.endPoint);

          if ((!isCollapsed) && selection.focusAtStart && browserSelection.collapse && browserSelection.extend)
          {
            // can handle "backwards"-oriented selection, shift-arrow-keys move start
            // of selection
            browserSelection.collapse(end.container, end.offset);
            //console.trace();
            //console.log(htmlPrettyEscape(rep.alltext));
            //console.log("%o %o", rep.selStart, rep.selEnd);
            //console.log("%o %d", start.container, start.offset);
            browserSelection.extend(start.container, start.offset);
          }
          else
          {
            var range = doc.createRange();
            range.setStart(start.container, start.offset);
            range.setEnd(end.container, end.offset);
            browserSelection.removeAllRanges();
            browserSelection.addRange(range);
          }
        }
      }
    }
  }

  function childIndex(n)
  {
    var idx = 0;
    while (n.previousSibling)
    {
      idx++;
      n = n.previousSibling;
    }
    return idx;
  }

  function fixView()
  {
    // calling this method repeatedly should be fast
    if (getInnerWidth() === 0 || getInnerHeight() === 0)
    {
      return;
    }

    function setIfNecessary(obj, prop, value)
    {
      if (obj[prop] != value)
      {
        obj[prop] = value;
      }
    }

    var lineNumberWidth = sideDiv.firstChild.offsetWidth;
    var newSideDivWidth = lineNumberWidth + LINE_NUMBER_PADDING_LEFT;
    if (newSideDivWidth < MIN_LINEDIV_WIDTH) newSideDivWidth = MIN_LINEDIV_WIDTH;
    iframePadLeft = EDIT_BODY_PADDING_LEFT;
    if (hasLineNumbers) iframePadLeft += newSideDivWidth + LINE_NUMBER_PADDING_RIGHT;
    setIfNecessary(iframe.style, "left", iframePadLeft + "px");
    setIfNecessary(sideDiv.style, "width", newSideDivWidth + "px");

    for (var i = 0; i < 2; i++)
    {
      var newHeight = root.clientHeight;
      var newWidth = (browser.msie ? root.createTextRange().boundingWidth : root.clientWidth);
      var viewHeight = getInnerHeight() - iframePadBottom - iframePadTop;
      var viewWidth = getInnerWidth() - iframePadLeft - iframePadRight;
      if (newHeight < viewHeight)
      {
        newHeight = viewHeight;
        if (browser.msie) setIfNecessary(outerWin.document.documentElement.style, 'overflowY', 'auto');
      }
      else
      {
        if (browser.msie) setIfNecessary(outerWin.document.documentElement.style, 'overflowY', 'scroll');
      }
      if (doesWrap)
      {
        newWidth = viewWidth;
      }
      else
      {
        if (newWidth < viewWidth) newWidth = viewWidth;
      }
      setIfNecessary(iframe.style, "height", newHeight + "px");
      setIfNecessary(iframe.style, "width", newWidth + "px");
      setIfNecessary(sideDiv.style, "height", newHeight + "px");
    }
    if (browser.firefox)
    {
      if (!doesWrap)
      {
        // the body:display:table-cell hack makes mozilla do scrolling
        // correctly by shrinking the <body> to fit around its content,
        // but mozilla won't act on clicks below the body.  We keep the
        // style.height property set to the viewport height (editor height
        // not including scrollbar), so it will never shrink so that part of
        // the editor isn't clickable.
        var body = root;
        var styleHeight = viewHeight + "px";
        setIfNecessary(body.style, "height", styleHeight);
      }
      else
      {
        setIfNecessary(root.style, "height", "");
      }
    }
    var win = outerWin;
    var r = 20;

    enforceEditability();

    $(sideDiv).addClass('sidedivdelayed');
  }

  var _teardownActions = [];

  function teardown()
  {
    _.each(_teardownActions, function(a)
    {
      a();
    });
  }

  function setDesignMode(newVal)
  {
    try
    {
      function setIfNecessary(target, prop, val)
      {
        if (String(target[prop]).toLowerCase() != val)
        {
          target[prop] = val;
          return true;
        }
        return false;
      }
      if (browser.msie || browser.safari)
      {
        setIfNecessary(root, 'contentEditable', (newVal ? 'true' : 'false'));
      }
      else
      {
        var wasSet = setIfNecessary(doc, 'designMode', (newVal ? 'on' : 'off'));
        if (wasSet && newVal && browser.opera)
        {
          // turning on designMode clears event handlers
          bindTheEventHandlers();
        }
      }
      return true;
    }
    catch (e)
    {
      return false;
    }
  }

  var iePastedLines = null;

  function handleIEPaste(evt)
  {
    // Pasting in IE loses blank lines in a way that loses information;
    // "one\n\ntwo\nthree" becomes "<p>one</p><p>two</p><p>three</p>",
    // which becomes "one\ntwo\nthree".  We can get the correct text
    // from the clipboard directly, but we still have to let the paste
    // happen to get the style information.
    var clipText = window.clipboardData && window.clipboardData.getData("Text");
    if (clipText && doc.selection)
    {
      // this "paste" event seems to mess with the selection whether we try to
      // stop it or not, so can't really do document-level manipulation now
      // or in an idle call-stack.  instead, use IE native manipulation
      //function escapeLine(txt) {
      //return processSpaces(escapeHTML(textify(txt)));
      //}
      //var newHTML = map(clipText.replace(/\r/g,'').split('\n'), escapeLine).join('<br>');
      //doc.selection.createRange().pasteHTML(newHTML);
      //evt.preventDefault();
      //iePastedLines = map(clipText.replace(/\r/g,'').split('\n'), textify);
    }
  }


  var inInternationalComposition = false;
  function handleCompositionEvent(evt)
  {
    // international input events, fired in FF3, at least;  allow e.g. Japanese input
    if (evt.type == "compositionstart")
    {
      inInternationalComposition = true;
    }
    else if (evt.type == "compositionend")
    {
      inInternationalComposition = false;
    }
  }

  editorInfo.ace_getInInternationalComposition = function ()
  {
    return inInternationalComposition;
  }

  function bindTheEventHandlers()
  {
    $(document).on("keydown", handleKeyEvent);
    $(document).on("keypress", handleKeyEvent);
    $(document).on("keyup", handleKeyEvent);
    $(document).on("click", handleClick);
    // dropdowns on edit bar need to be closed on clicks on both pad inner and pad outer
    $(outerWin.document).on("click", hideEditBarDropdowns);
    // Disabled: https://github.com/ether/etherpad-lite/issues/2546
    // Will break OL re-numbering: https://github.com/ether/etherpad-lite/pull/2533
    // $(document).on("cut", handleCut);

    $(root).on("blur", handleBlur);
    if (browser.msie)
    {
      $(document).on("click", handleIEOuterClick);
    }
    if (browser.msie) $(root).on("paste", handleIEPaste);

    // Don't paste on middle click of links
    $(root).on("paste", function(e){
      // TODO: this breaks pasting strings into URLS when using
      // Control C and Control V -- the Event is never available
      // here.. :(
      if(e.target.a || e.target.localName === "a"){
        e.preventDefault();
      }

      // Call paste hook
      hooks.callAll('acePaste', {
        editorInfo: editorInfo,
        rep: rep,
        documentAttributeManager: documentAttributeManager,
        e: e
      });
    })

    // We reference document here, this is because if we don't this will expose a bug
    // in Google Chrome.  This bug will cause the last character on the last line to
    // not fire an event when dropped into..
    $(document).on("drop", function(e){
      if(e.target.a || e.target.localName === "a"){
        e.preventDefault();
      }

      // Bug fix: when user drags some content and drop it far from its origin, we
      // need to merge the changes into a single changeset. So mark origin with <style>,
      // in order to make content be observed by incorporateUserChanges() (see
      // observeSuspiciousNodes() for more info)
      var selection = getSelection();
      if (selection){
        var firstLineSelected = topLevel(selection.startPoint.node);
        var lastLineSelected  = topLevel(selection.endPoint.node);

        var lineBeforeSelection = firstLineSelected.previousSibling;
        var lineAfterSelection  = lastLineSelected.nextSibling;

        var neighbor = lineBeforeSelection || lineAfterSelection;
        neighbor.appendChild(document.createElement('style'));
      }

      // Call drop hook
      hooks.callAll('aceDrop', {
        editorInfo: editorInfo,
        rep: rep,
        documentAttributeManager: documentAttributeManager,
        e: e
      });
    });

    // CompositionEvent is not implemented below IE version 8
    if ( !(browser.msie && parseInt(browser.version <= 9)) && document.documentElement)
    {
      $(document.documentElement).on("compositionstart", handleCompositionEvent);
      $(document.documentElement).on("compositionend", handleCompositionEvent);
    }
  }

  function topLevel(n)
  {
    if ((!n) || n == root) return null;
    while (n.parentNode != root)
    {
      n = n.parentNode;
    }
    return n;
  }

  function handleIEOuterClick(evt)
  {
    if ((evt.target.tagName || '').toLowerCase() != "html")
    {
      return;
    }
    if (!(evt.pageY > root.clientHeight))
    {
      return;
    }

    // click below the body
    inCallStackIfNecessary("handleOuterClick", function()
    {
      // put caret at bottom of doc
      fastIncorp(11);
      if (isCaret())
      { // don't interfere with drag
        var lastLine = rep.lines.length() - 1;
        var lastCol = rep.lines.atIndex(lastLine).text.length;
        performSelectionChange([lastLine, lastCol], [lastLine, lastCol]);
      }
    });
  }

  function getClassArray(elem, optFilter)
  {
    var bodyClasses = [];
    (elem.className || '').replace(/\S+/g, function(c)
    {
      if ((!optFilter) || (optFilter(c)))
      {
        bodyClasses.push(c);
      }
    });
    return bodyClasses;
  }

  function setClassArray(elem, array)
  {
    elem.className = array.join(' ');
  }

  function setClassPresence(elem, className, present)
  {
    if (present) $(elem).addClass(className);
    else $(elem).removeClass(className);
  }

  function focus()
  {
    window.focus();
  }

  function handleBlur(evt)
  {
    if (browser.msie)
    {
      // a fix: in IE, clicking on a control like a button outside the
      // iframe can "blur" the editor, causing it to stop getting
      // events, though typing still affects it(!).
      setSelection(null);
    }
  }

  function getSelectionPointX(point)
  {
    // doesn't work in wrap-mode
    var node = point.node;
    var index = point.index;

    function leftOf(n)
    {
      return n.offsetLeft;
    }

    function rightOf(n)
    {
      return n.offsetLeft + n.offsetWidth;
    }
    if (!isNodeText(node))
    {
      if (index === 0) return leftOf(node);
      else return rightOf(node);
    }
    else
    {
      // we can get bounds of element nodes, so look for those.
      // allow consecutive text nodes for robustness.
      var charsToLeft = index;
      var charsToRight = node.nodeValue.length - index;
      var n;
      for (n = node.previousSibling; n && isNodeText(n); n = n.previousSibling)
      charsToLeft += n.nodeValue;
      var leftEdge = (n ? rightOf(n) : leftOf(node.parentNode));
      for (n = node.nextSibling; n && isNodeText(n); n = n.nextSibling)
      charsToRight += n.nodeValue;
      var rightEdge = (n ? leftOf(n) : rightOf(node.parentNode));
      var frac = (charsToLeft / (charsToLeft + charsToRight));
      var pixLoc = leftEdge + frac * (rightEdge - leftEdge);
      return Math.round(pixLoc);
    }
  }

  function getPageHeight()
  {
    var win = outerWin;
    var odoc = win.document;
    if (win.innerHeight && win.scrollMaxY) return win.innerHeight + win.scrollMaxY;
    else if (odoc.body.scrollHeight > odoc.body.offsetHeight) return odoc.body.scrollHeight;
    else return odoc.body.offsetHeight;
  }

  function getPageWidth()
  {
    var win = outerWin;
    var odoc = win.document;
    if (win.innerWidth && win.scrollMaxX) return win.innerWidth + win.scrollMaxX;
    else if (odoc.body.scrollWidth > odoc.body.offsetWidth) return odoc.body.scrollWidth;
    else return odoc.body.offsetWidth;
  }

  function getInnerHeight()
  {
    var win = outerWin;
    var odoc = win.document;
    var h;
    if (browser.opera) h = win.innerHeight;
    else h = odoc.documentElement.clientHeight;
    if (h) return h;

    // deal with case where iframe is hidden, hope that
    // style.height of iframe container is set in px
    return Number(editorInfo.frame.parentNode.style.height.replace(/[^0-9]/g, '') || 0);
  }

  function getInnerWidth()
  {
    var win = outerWin;
    var odoc = win.document;
    return odoc.documentElement.clientWidth;
  }

  function scrollXHorizontallyIntoView(pixelX)
  {
    var win = outerWin;
    var odoc = outerWin.document;
    pixelX += iframePadLeft;
    var distInsideLeft = pixelX - win.scrollX;
    var distInsideRight = win.scrollX + getInnerWidth() - pixelX;
    if (distInsideLeft < 0)
    {
      win.scrollBy(distInsideLeft, 0);
    }
    else if (distInsideRight < 0)
    {
      win.scrollBy(-distInsideRight + 1, 0);
    }
  }

  function scrollSelectionIntoView()
  {
    if (!rep.selStart) return;
    fixView();
    var innerHeight = getInnerHeight();
    scroll.scrollNodeVerticallyIntoView(rep, innerHeight);
    if (!doesWrap)
    {
      var browserSelection = getSelection();
      if (browserSelection)
      {
        var focusPoint = (browserSelection.focusAtStart ? browserSelection.startPoint : browserSelection.endPoint);
        var selectionPointX = getSelectionPointX(focusPoint);
        scrollXHorizontallyIntoView(selectionPointX);
        fixView();
      }
    }
  }

  var listAttributeName = 'list';

  function getLineListType(lineNum)
  {
    return documentAttributeManager.getAttributeOnLine(lineNum, listAttributeName)
  }

  function setLineListType(lineNum, listType)
  {
    if(listType == ''){
      documentAttributeManager.removeAttributeOnLine(lineNum, listAttributeName);
      documentAttributeManager.removeAttributeOnLine(lineNum, 'start');
    }else{
      documentAttributeManager.setAttributeOnLine(lineNum, listAttributeName, listType);
    }

    //if the list has been removed, it is necessary to renumber
    //starting from the *next* line because the list may have been
    //separated. If it returns null, it means that the list was not cut, try
    //from the current one.
    if(renumberList(lineNum+1)==null)
    {
      renumberList(lineNum);
    }
  }

  function renumberList(lineNum){
    //1-check we are in a list
    var type = getLineListType(lineNum);
    if(!type)
    {
      return null;
    }
    type = /([a-z]+)[0-9]+/.exec(type);
    if(type[1] == "indent")
    {
      return null;
    }

    //2-find the first line of the list
    while(lineNum-1 >= 0 && (type=getLineListType(lineNum-1)))
    {
      type = /([a-z]+)[0-9]+/.exec(type);
      if(type[1] == "indent")
        break;
      lineNum--;
    }

    //3-renumber every list item of the same level from the beginning, level 1
    //IMPORTANT: never skip a level because there imbrication may be arbitrary
    var builder = Changeset.builder(rep.lines.totalWidth());
    var loc = [0,0];
    function applyNumberList(line, level)
    {
      //init
      var position = 1;
      var curLevel = level;
      var listType;
      //loop over the lines
      while(listType = getLineListType(line))
      {
        //apply new num
        listType = /([a-z]+)([0-9]+)/.exec(listType);
        curLevel = Number(listType[2]);
        if(isNaN(curLevel) || listType[0] == "indent")
        {
          return line;
        }
        else if(curLevel == level)
        {
          ChangesetUtils.buildKeepRange(rep, builder, loc, (loc = [line, 0]));
          ChangesetUtils.buildKeepRange(rep, builder, loc, (loc = [line, 1]), [
            ['start', position]
          ], rep.apool);

          position++;
          line++;
        }
        else if(curLevel < level)
        {
          return line;//back to parent
        }
        else
        {
          line = applyNumberList(line, level+1);//recursive call
        }
      }
      return line;
    }

    applyNumberList(lineNum, 1);
    var cs = builder.toString();
    if (!Changeset.isIdentity(cs))
    {
      performDocumentApplyChangeset(cs);
    }

    //4-apply the modifications


  }


  function doInsertList(type)
  {
    if (!(rep.selStart && rep.selEnd))
    {
      return;
    }

    var firstLine, lastLine;
    firstLine = rep.selStart[0];
    lastLine = Math.max(firstLine, rep.selEnd[0] - ((rep.selEnd[1] === 0) ? 1 : 0));

    var allLinesAreList = true;
    for (var n = firstLine; n <= lastLine; n++)
    {
      var listType = getLineListType(n);
      if (!listType || listType.slice(0, type.length) != type)
      {
        allLinesAreList = false;
        break;
      }
    }

    var mods = [];
    for (var n = firstLine; n <= lastLine; n++)
    {
      var t = '';
      var level = 0;
      var listType = /([a-z]+)([0-9]+)/.exec(getLineListType(n));
      if (listType)
      {
        t = listType[1];
        level = Number(listType[2]);
      }
      var t = getLineListType(n);

      // if already a list, deindent
      if (allLinesAreList && level != 1) { level = level - 1;  }
      // if already indented, then add a level of indentation to the list
      else if (t && !allLinesAreList) { level = level + 1; }

      mods.push([n, allLinesAreList ? 'indent' + level : (t ? type + level : type + '1')]);
    }

    _.each(mods, function(mod){
      setLineListType(mod[0], mod[1]);
    });
  }

  function doInsertUnorderedList(){
    doInsertList('bullet');
  }
  function doInsertOrderedList(){
    doInsertList('number');
  }
  editorInfo.ace_doInsertUnorderedList = doInsertUnorderedList;
  editorInfo.ace_doInsertOrderedList = doInsertOrderedList;

  var lineNumbersShown;
  var sideDivInner;

  function initLineNumbers()
  {
    lineNumbersShown = 1;
    sideDiv.innerHTML = '<table border="0" cellpadding="0" cellspacing="0" align="right"><tr><td id="sidedivinner" class="sidedivinner"><div>1</div></td></tr></table>';
    sideDivInner = outerWin.document.getElementById("sidedivinner");
    $(sideDiv).addClass("sidediv");
  }

  function updateLineNumbers()
  {
    var newNumLines = rep.lines.length();
    if (newNumLines < 1) newNumLines = 1;
    //update height of all current line numbers

    var a = sideDivInner.firstChild;
    var b = doc.body.firstChild;
    var n = 0;

    if (currentCallStack && currentCallStack.domClean)
    {

      while (a && b)
      {
        if(n > lineNumbersShown) //all updated, break
        break;
        var h = (b.clientHeight || b.offsetHeight);
        if (b.nextSibling)
        {
          // when text is zoomed in mozilla, divs have fractional
          // heights (though the properties are always integers)
          // and the line-numbers don't line up unless we pay
          // attention to where the divs are actually placed...
          // (also: padding on TTs/SPANs in IE...)
          if (b === doc.body.firstChild) {
            // It's the first line. For line number alignment purposes, its
            // height is taken to be the top offset of the next line. If we
            // didn't do this special case, we would miss out on any top margin
            // included on the first line. The default stylesheet doesn't add
            // extra margins/padding, but plugins might.
            h = b.nextSibling.offsetTop - window.getComputedStyle(doc.body).getPropertyValue("padding-top");
          } else {
            h = b.nextSibling.offsetTop - b.offsetTop;
          }
        }
        if (h)
        {
          var hpx = h + "px";
          if (a.style.height != hpx) {
            a.style.height = hpx;
          }
        }
        a = a.nextSibling;
        b = b.nextSibling;
        n++;
      }
    }

    if (newNumLines != lineNumbersShown)
    {
      var container = sideDivInner;
      var odoc = outerWin.document;
      var fragment = odoc.createDocumentFragment();
      while (lineNumbersShown < newNumLines)
      {
        lineNumbersShown++;
        var n = lineNumbersShown;
        var div = odoc.createElement("DIV");
        //calculate height for new line number
        if(b){
          var h = (b.clientHeight || b.offsetHeight);

          if (b.nextSibling){
            h = b.nextSibling.offsetTop - b.offsetTop;
          }
        }

        if(h){ // apply style to div
          div.style.height = h +"px";
        }

        div.appendChild(odoc.createTextNode(String(n)));
        fragment.appendChild(div);
        if(b){
          b = b.nextSibling;
        }
      }

      container.appendChild(fragment);
      while (lineNumbersShown > newNumLines)
      {
        container.removeChild(container.lastChild);
        lineNumbersShown--;
      }
    }
  }


  // Init documentAttributeManager
  documentAttributeManager = new AttributeManager(rep, performDocumentApplyChangeset);
  editorInfo.ace_performDocumentApplyAttributesToRange = function () {
    return documentAttributeManager.setAttributesOnRange.apply(documentAttributeManager, arguments);
  };

  this.init = function () {
    $(document).ready(function(){
      doc = document; // defined as a var in scope outside
      inCallStack("setup", function()
      {
        var body = doc.getElementById("innerdocbody");
        root = body; // defined as a var in scope outside
        if (browser.firefox) $(root).addClass("mozilla");
        if (browser.safari) $(root).addClass("safari");
        if (browser.msie) $(root).addClass("msie");
        setClassPresence(root, "authorColors", true);
        setClassPresence(root, "doesWrap", doesWrap);

        initDynamicCSS();

        enforceEditability();

        // set up dom and rep
        while (root.firstChild) root.removeChild(root.firstChild);
        var oneEntry = createDomLineEntry("");
        doRepLineSplice(0, rep.lines.length(), [oneEntry]);
        insertDomLines(null, [oneEntry.domInfo], null);
        rep.alines = Changeset.splitAttributionLines(
        Changeset.makeAttribution("\n"), "\n");

        bindTheEventHandlers();

      });

      hooks.callAll('aceInitialized', {
        editorInfo: editorInfo,
        rep: rep,
        documentAttributeManager: documentAttributeManager
      });

      scheduler.setTimeout(function()
      {
        parent.readyFunc(); // defined in code that sets up the inner iframe
      }, 0);

      isSetUp = true;
    });
  }

}

exports.init = function () {
  var editor = new Ace2Inner()
  editor.init();
};
