mirror of
				https://github.com/ether/etherpad-lite.git
				synced 2025-11-04 10:11:33 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			5659 lines
		
	
	
		
			159 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			5659 lines
		
	
	
		
			159 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/**
 | 
						|
 * 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 Ace2Common = require('/ace2_common');
 | 
						|
 | 
						|
// Extract useful method defined in the other module.
 | 
						|
var isNodeText = Ace2Common.isNodeText;
 | 
						|
var object = Ace2Common.object;
 | 
						|
var extend = Ace2Common.extend;
 | 
						|
var forEach = Ace2Common.forEach;
 | 
						|
var map = Ace2Common.map;
 | 
						|
var filter = Ace2Common.filter;
 | 
						|
var isArray = Ace2Common.isArray;
 | 
						|
var browser = Ace2Common.browser;
 | 
						|
var getAssoc = Ace2Common.getAssoc;
 | 
						|
var setAssoc = Ace2Common.setAssoc;
 | 
						|
var binarySearchInfinite = Ace2Common.binarySearchInfinite;
 | 
						|
var htmlPrettyEscape = Ace2Common.htmlPrettyEscape;
 | 
						|
var map = Ace2Common.map;
 | 
						|
var noop = Ace2Common.noop;
 | 
						|
 | 
						|
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('/AttributePoolFactory').createAttributePool;
 | 
						|
var Changeset = require('/Changeset');
 | 
						|
var linestylefilter = require('/linestylefilter').linestylefilter;
 | 
						|
var newSkipList = require('/skiplist').newSkipList;
 | 
						|
var undoModule = require('/undomodule').undoModule;
 | 
						|
var makeVirtualLineView = require('/virtual_lines').makeVirtualLineView;
 | 
						|
 | 
						|
 | 
						|
function Ace2Inner(){
 | 
						|
  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 = 8;
 | 
						|
 | 
						|
  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 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;
 | 
						|
  var overlaysdiv = lineMetricsDiv.nextSibling;
 | 
						|
  initLineNumbers();
 | 
						|
 | 
						|
  var outsideKeyDown = function(evt)
 | 
						|
    {};
 | 
						|
  var outsideKeyPress = function(evt)
 | 
						|
    {
 | 
						|
      return true;
 | 
						|
      };
 | 
						|
  var outsideNotifyDirty = function()
 | 
						|
    {};
 | 
						|
 | 
						|
  // 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: newSkipList(),
 | 
						|
    selStart: null,
 | 
						|
    selEnd: null,
 | 
						|
    selFocusAtStart: false,
 | 
						|
    alltext: "",
 | 
						|
    alines: [],
 | 
						|
    apool: new AttribPool()
 | 
						|
  };
 | 
						|
  // lines, alltext, alines, and DOM are set up in setup()
 | 
						|
  if (undoModule.enabled)
 | 
						|
  {
 | 
						|
    undoModule.apool = rep.apool;
 | 
						|
  }
 | 
						|
 | 
						|
  var root, doc; // set in setup()
 | 
						|
  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);
 | 
						|
  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]] = function()
 | 
						|
    {};
 | 
						|
    //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;
 | 
						|
 | 
						|
  var textFace = 'monospace';
 | 
						|
  var textSize = 12;
 | 
						|
 | 
						|
  function textLineHeight()
 | 
						|
  {
 | 
						|
    return Math.round(textSize * 4 / 3);
 | 
						|
  }
 | 
						|
 | 
						|
  var dynamicCSS = null;
 | 
						|
  var dynamicCSSTop = null;
 | 
						|
 | 
						|
  function initDynamicCSS()
 | 
						|
  {
 | 
						|
    dynamicCSS = makeCSSManager("dynamicsyntax");
 | 
						|
    dynamicCSSTop = makeCSSManager("dynamicsyntax", true);
 | 
						|
  }
 | 
						|
 | 
						|
  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 setAuthorInfo(author, info)
 | 
						|
  {
 | 
						|
    if ((typeof author) != "string")
 | 
						|
    {
 | 
						|
      throw new Error("setAuthorInfo: author (" + author + ") is not a string");
 | 
						|
    }
 | 
						|
    if (!info)
 | 
						|
    {
 | 
						|
      delete authorInfos[author];
 | 
						|
      if (dynamicCSS)
 | 
						|
      {
 | 
						|
        dynamicCSS.removeSelectorStyle(getAuthorColorClassSelector(getAuthorClassName(author)));
 | 
						|
        dynamicCSSTop.removeSelectorStyle(getAuthorColorClassSelector(getAuthorClassName(author)));
 | 
						|
      }
 | 
						|
    }
 | 
						|
    else
 | 
						|
    {
 | 
						|
      authorInfos[author] = info;
 | 
						|
      if (info.bgcolor)
 | 
						|
      {
 | 
						|
        if (dynamicCSS)
 | 
						|
        {
 | 
						|
          var bgcolor = info.bgcolor;
 | 
						|
          if ((typeof info.fade) == "number")
 | 
						|
          {
 | 
						|
            bgcolor = fadeColor(bgcolor, info.fade);
 | 
						|
          }
 | 
						|
          
 | 
						|
          // Text color
 | 
						|
          var txtcolor = (colorutils.luminosity(colorutils.css2triple(bgcolor)) < 0.5) ? '#ffffff' : '#000000';
 | 
						|
          
 | 
						|
          var authorStyle = dynamicCSS.selectorStyle(getAuthorColorClassSelector(
 | 
						|
          getAuthorClassName(author)));
 | 
						|
          authorStyle.backgroundColor = bgcolor;
 | 
						|
          authorStyle.color = txtcolor;
 | 
						|
          
 | 
						|
          var authorStyleTop = dynamicCSSTop.selectorStyle(getAuthorColorClassSelector(
 | 
						|
          getAuthorClassName(author)));
 | 
						|
          authorStyleTop.backgroundColor = bgcolor;
 | 
						|
          authorStyleTop.color = txtcolor;
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  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 boldColorFromColor(lightColorCSS)
 | 
						|
  {
 | 
						|
    var color = colorutils.css2triple(lightColorCSS);
 | 
						|
 | 
						|
    // amp up the saturation to full
 | 
						|
    color = colorutils.saturate(color);
 | 
						|
 | 
						|
    // normalize brightness based on luminosity
 | 
						|
    color = colorutils.scaleColor(color, 0, 0.5 / colorutils.luminosity(color));
 | 
						|
 | 
						|
    return colorutils.triple2css(color);
 | 
						|
  }
 | 
						|
 | 
						|
  function fadeColor(colorCSS, fadeFrac)
 | 
						|
  {
 | 
						|
    var color = colorutils.css2triple(colorCSS);
 | 
						|
    color = colorutils.blend(color, [1, 1, 1], fadeFrac);
 | 
						|
    return colorutils.triple2css(color);
 | 
						|
  }
 | 
						|
 | 
						|
  function doAlert(str)
 | 
						|
  {
 | 
						|
    scheduler.setTimeout(function()
 | 
						|
    {
 | 
						|
      alert(str);
 | 
						|
    }, 0);
 | 
						|
  }
 | 
						|
 | 
						|
  editorInfo.ace_getRep = function()
 | 
						|
  {
 | 
						|
    return rep;
 | 
						|
  }
 | 
						|
 | 
						|
  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 (evt.eventType == "setup" || evt.eventType == "importText" || evt.eventType == "setBaseText")
 | 
						|
          {
 | 
						|
            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();
 | 
						|
      //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) && cs.type != "applyChangesToBase")
 | 
						|
          {
 | 
						|
            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 recolorLineByKey(key)
 | 
						|
  {
 | 
						|
    if (rep.lines.containsKey(key))
 | 
						|
    {
 | 
						|
      var offset = rep.lines.offsetOfKey(key);
 | 
						|
      var width = rep.lines.atKey(key).width;
 | 
						|
      recolorLinesInRange(offset, offset + width);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  function getLineKeyForOffset(charOffset)
 | 
						|
  {
 | 
						|
    return rep.lines.atOffset(charOffset).key;
 | 
						|
  }
 | 
						|
  
 | 
						|
  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);
 | 
						|
  }
 | 
						|
 | 
						|
  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) || ' ' /*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])
 | 
						|
    {
 | 
						|
      inCallStack(cmd, function()
 | 
						|
      {
 | 
						|
        fastIncorp(9);
 | 
						|
        CMDS[cmd].apply(CMDS, cmdArgs);
 | 
						|
      });
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  function replaceRange(start, end, text)
 | 
						|
  {
 | 
						|
    inCallStack('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_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();
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  editorInfo.ace_setProperty = function(key, value)
 | 
						|
  {
 | 
						|
    var k = key.toLowerCase();
 | 
						|
    if (k == "wraps")
 | 
						|
    {
 | 
						|
      setWraps(value);
 | 
						|
    }
 | 
						|
    else if (k == "showsauthorcolors")
 | 
						|
    {
 | 
						|
      setClassPresence(root, "authorColors", !! value);
 | 
						|
    }
 | 
						|
    else if (k == "showsuserselections")
 | 
						|
    {
 | 
						|
      setClassPresence(root, "userSelections", !! value);
 | 
						|
    }
 | 
						|
    else if (k == "showslinenumbers")
 | 
						|
    {
 | 
						|
      hasLineNumbers = !! value;
 | 
						|
      // disable line numbers on mobile devices
 | 
						|
      if (browser.mobile) hasLineNumbers = false;
 | 
						|
      setClassPresence(sideDiv, "sidedivhidden", !hasLineNumbers);
 | 
						|
      fixView();
 | 
						|
    }
 | 
						|
    else if (k == "grayedout")
 | 
						|
    {
 | 
						|
      setClassPresence(outerWin.document.body, "grayedout", !! value);
 | 
						|
    }
 | 
						|
    else if (k == "dmesg")
 | 
						|
    {
 | 
						|
      dmesg = value;
 | 
						|
      window.dmesg = value;
 | 
						|
    }
 | 
						|
    else if (k == 'userauthor')
 | 
						|
    {
 | 
						|
      thisAuthor = String(value);
 | 
						|
    }
 | 
						|
    else if (k == 'styled')
 | 
						|
    {
 | 
						|
      setStyled(value);
 | 
						|
    }
 | 
						|
    else if (k == 'textface')
 | 
						|
    {
 | 
						|
      setTextFace(value);
 | 
						|
    }
 | 
						|
    else if (k == 'textsize')
 | 
						|
    {
 | 
						|
      setTextSize(value);
 | 
						|
    }
 | 
						|
    else if (k == 'rtlistrue')
 | 
						|
    {
 | 
						|
      setClassPresence(root, "rtl", !! 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_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;
 | 
						|
 | 
						|
  function incorpIfQuick()
 | 
						|
  {
 | 
						|
    var me = incorpIfQuick;
 | 
						|
    var failures = (me.failures || 0);
 | 
						|
    if (failures < 5)
 | 
						|
    {
 | 
						|
      var isTimeUp = newTimeLimit(40);
 | 
						|
      var madeChanges = incorporateUserChanges(isTimeUp);
 | 
						|
      if (isTimeUp())
 | 
						|
      {
 | 
						|
        me.failures = failures + 1;
 | 
						|
      }
 | 
						|
      return true;
 | 
						|
    }
 | 
						|
    else
 | 
						|
    {
 | 
						|
      var skipCount = (me.skipCount || 0);
 | 
						|
      skipCount++;
 | 
						|
      if (skipCount == 20)
 | 
						|
      {
 | 
						|
        skipCount = 0;
 | 
						|
        me.failures = 0;
 | 
						|
      }
 | 
						|
      me.skipCount = skipCount;
 | 
						|
    }
 | 
						|
    return false;
 | 
						|
  }
 | 
						|
 | 
						|
  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;
 | 
						|
    }
 | 
						|
 | 
						|
    inCallStack("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 = getVisibleCharRange();
 | 
						|
        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)
 | 
						|
    {
 | 
						|
      function topLevel(n)
 | 
						|
      {
 | 
						|
        if ((!n) || n == root) return null;
 | 
						|
        while (n.parentNode != root)
 | 
						|
        {
 | 
						|
          n = n.parentNode;
 | 
						|
        }
 | 
						|
        return n;
 | 
						|
      }
 | 
						|
      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 = nds[i];
 | 
						|
        while (n.parentNode && n.parentNode != root)
 | 
						|
        {
 | 
						|
          n = n.parentNode;
 | 
						|
        }
 | 
						|
        if (n.parentNode == root)
 | 
						|
        {
 | 
						|
          observeChangesAroundNode(n);
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  function incorporateUserChanges(isTimeUp)
 | 
						|
  {
 | 
						|
 | 
						|
    if (currentCallStack.domClean) return false;
 | 
						|
 | 
						|
    inInternationalComposition = false; // if we need the document normalized, so be it
 | 
						|
    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;
 | 
						|
 | 
						|
        if (linesWrapped > 0)
 | 
						|
        {
 | 
						|
          doAlert("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]);
 | 
						|
        forEach(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");
 | 
						|
    forEach(splicesToDo, function(splice)
 | 
						|
    {
 | 
						|
      doIncorpLineSplice(splice[0], splice[1], splice[2], splice[3], splice[4]);
 | 
						|
    });
 | 
						|
 | 
						|
    //p.mark("relex");
 | 
						|
    //rep.lexer.lexCharRange(getVisibleCharRange(), function() { return false; });
 | 
						|
    //var isTimeUp = newTimeLimit(100);
 | 
						|
    // do DOM inserts
 | 
						|
    p.mark("insert");
 | 
						|
    forEach(domInsertsNeeded, function(ins)
 | 
						|
    {
 | 
						|
      insertDomLines(ins[0], ins[1], isTimeUp);
 | 
						|
    });
 | 
						|
 | 
						|
    p.mark("del");
 | 
						|
    // delete old dom nodes
 | 
						|
    forEach(toDeleteAtEnd, function(n)
 | 
						|
    {
 | 
						|
      //var id = n.uniqueId();
 | 
						|
      // parent of n may not be "root" in IE due to non-tree-shaped DOM (wtf)
 | 
						|
      n.parentNode.removeChild(n);
 | 
						|
 | 
						|
      //dmesg(htmlPrettyEscape(htmlForRemovedChild(n)));
 | 
						|
      //console.log("removed: "+id);
 | 
						|
    });
 | 
						|
 | 
						|
    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");
 | 
						|
      selStart = getLineAndCharForPoint(selection.startPoint);
 | 
						|
    }
 | 
						|
    if (selection && !selEnd)
 | 
						|
    {
 | 
						|
      selEnd = getLineAndCharForPoint(selection.endPoint);
 | 
						|
    }
 | 
						|
 | 
						|
    // 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;
 | 
						|
  }
 | 
						|
 | 
						|
  function htmlForRemovedChild(n)
 | 
						|
  {
 | 
						|
    var div = doc.createElement("DIV");
 | 
						|
    div.appendChild(n);
 | 
						|
    return div.innerHTML;
 | 
						|
  }
 | 
						|
 | 
						|
  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 isIncorpedAttribute(aname)
 | 
						|
  {
 | 
						|
    return ( !! STYLE_ATTRIBS[aname]) || ( !! 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);
 | 
						|
    forEach(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];
 | 
						|
  }
 | 
						|
 | 
						|
  function caretColumn()
 | 
						|
  {
 | 
						|
    return rep.selStart[1];
 | 
						|
  }
 | 
						|
 | 
						|
  function caretDocChar()
 | 
						|
  {
 | 
						|
    return rep.lines.offsetOfIndex(caretLine()) + caretColumn();
 | 
						|
  }
 | 
						|
 | 
						|
  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];
 | 
						|
      if (/[\[\(\{]\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 setupMozillaCaretHack(lineNum)
 | 
						|
  {
 | 
						|
    // This is really ugly, but by god, it works!
 | 
						|
    // Fixes annoying Firefox caret artifact (observed in 2.0.0.12
 | 
						|
    // and unfixed in Firefox 2 as of now) where mutating the DOM
 | 
						|
    // and then moving the caret to the beginning of a line causes
 | 
						|
    // an image of the caret to be XORed at the top of the iframe.
 | 
						|
    // The previous solution involved remembering to set the selection
 | 
						|
    // later, in response to the next event in the queue, which was hugely
 | 
						|
    // annoying.
 | 
						|
    // This solution: add a space character (0x20) to the beginning of the line.
 | 
						|
    // After setting the selection, remove the space.
 | 
						|
    var lineNode = rep.lines.atIndex(lineNum).lineNode;
 | 
						|
 | 
						|
    var fc = lineNode.firstChild;
 | 
						|
    while (isBlockElement(fc) && fc.firstChild)
 | 
						|
    {
 | 
						|
      fc = fc.firstChild;
 | 
						|
    }
 | 
						|
    var textNode;
 | 
						|
    if (isNodeText(fc))
 | 
						|
    {
 | 
						|
      fc.nodeValue = " " + fc.nodeValue;
 | 
						|
      textNode = fc;
 | 
						|
    }
 | 
						|
    else
 | 
						|
    {
 | 
						|
      textNode = doc.createTextNode(" ");
 | 
						|
      fc.parentNode.insertBefore(textNode, fc);
 | 
						|
    }
 | 
						|
    markNodeClean(lineNode);
 | 
						|
    return {
 | 
						|
      unhack: function()
 | 
						|
      {
 | 
						|
        if (textNode.nodeValue == " ")
 | 
						|
        {
 | 
						|
          textNode.parentNode.removeChild(textNode);
 | 
						|
        }
 | 
						|
        else
 | 
						|
        {
 | 
						|
          textNode.nodeValue = textNode.nodeValue.substring(1);
 | 
						|
        }
 | 
						|
        markNodeClean(lineNode);
 | 
						|
      }
 | 
						|
    };
 | 
						|
  }
 | 
						|
 | 
						|
 | 
						|
  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 && 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)
 | 
						|
  {
 | 
						|
    return n.innerText || 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];
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  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)
 | 
						|
      {
 | 
						|
        domAndRepSplice(start, numRemoved, map(Array.prototype.slice.call(arguments, 2), 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);
 | 
						|
 | 
						|
      forEach(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);
 | 
						|
    }
 | 
						|
 | 
						|
  }
 | 
						|
 | 
						|
  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());
 | 
						|
    buildKeepToStartOfRange(builder, start);
 | 
						|
    buildRemoveRange(builder, start, end);
 | 
						|
    builder.insert(newText, [
 | 
						|
      ['author', thisAuthor]
 | 
						|
    ], rep.apool);
 | 
						|
    var cs = builder.toString();
 | 
						|
 | 
						|
    performDocumentApplyChangeset(cs);
 | 
						|
  }
 | 
						|
 | 
						|
  function performDocumentApplyAttributesToCharRange(start, end, attribs)
 | 
						|
  {
 | 
						|
    if (end >= rep.alltext.length)
 | 
						|
    {
 | 
						|
      end = rep.alltext.length - 1;
 | 
						|
    }
 | 
						|
    performDocumentApplyAttributesToRange(lineAndColumnFromChar(start), lineAndColumnFromChar(end), attribs);
 | 
						|
  }
 | 
						|
  editorInfo.ace_performDocumentApplyAttributesToCharRange = performDocumentApplyAttributesToCharRange;
 | 
						|
 | 
						|
  function performDocumentApplyAttributesToRange(start, end, attribs)
 | 
						|
  {
 | 
						|
    var builder = Changeset.builder(rep.lines.totalWidth());
 | 
						|
    buildKeepToStartOfRange(builder, start);
 | 
						|
    buildKeepRange(builder, start, end, attribs, rep.apool);
 | 
						|
    var cs = builder.toString();
 | 
						|
    performDocumentApplyChangeset(cs);
 | 
						|
  }
 | 
						|
 | 
						|
  function buildKeepToStartOfRange(builder, start)
 | 
						|
  {
 | 
						|
    var startLineOffset = rep.lines.offsetOfIndex(start[0]);
 | 
						|
 | 
						|
    builder.keep(startLineOffset, start[0]);
 | 
						|
    builder.keep(start[1]);
 | 
						|
  }
 | 
						|
 | 
						|
  function buildRemoveRange(builder, start, end)
 | 
						|
  {
 | 
						|
    var startLineOffset = rep.lines.offsetOfIndex(start[0]);
 | 
						|
    var endLineOffset = rep.lines.offsetOfIndex(end[0]);
 | 
						|
 | 
						|
    if (end[0] > start[0])
 | 
						|
    {
 | 
						|
      builder.remove(endLineOffset - startLineOffset - start[1], end[0] - start[0]);
 | 
						|
      builder.remove(end[1]);
 | 
						|
    }
 | 
						|
    else
 | 
						|
    {
 | 
						|
      builder.remove(end[1] - start[1]);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  function buildKeepRange(builder, start, end, attribs, pool)
 | 
						|
  {
 | 
						|
    var startLineOffset = rep.lines.offsetOfIndex(start[0]);
 | 
						|
    var endLineOffset = rep.lines.offsetOfIndex(end[0]);
 | 
						|
 | 
						|
    if (end[0] > start[0])
 | 
						|
    {
 | 
						|
      builder.keep(endLineOffset - startLineOffset - start[1], end[0] - start[0], attribs, pool);
 | 
						|
      builder.keep(end[1], 0, attribs, pool);
 | 
						|
    }
 | 
						|
    else
 | 
						|
    {
 | 
						|
      builder.keep(end[1] - start[1], 0, attribs, pool);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  function setAttributeOnSelection(attributeName, attributeValue)
 | 
						|
  {
 | 
						|
    if (!(rep.selStart && rep.selEnd)) return;
 | 
						|
 | 
						|
    performDocumentApplyAttributesToRange(rep.selStart, rep.selEnd, [
 | 
						|
      [attributeName, attributeValue]
 | 
						|
    ]);
 | 
						|
  }
 | 
						|
  editorInfo.ace_setAttributeOnSelection = setAttributeOnSelection;
 | 
						|
 | 
						|
  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;
 | 
						|
      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;
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    if (selectionAllHasIt)
 | 
						|
    {
 | 
						|
      performDocumentApplyAttributesToRange(rep.selStart, rep.selEnd, [
 | 
						|
        [attributeName, '']
 | 
						|
      ]);
 | 
						|
    }
 | 
						|
    else
 | 
						|
    {
 | 
						|
      performDocumentApplyAttributesToRange(rep.selStart, rep.selEnd, [
 | 
						|
        [attributeName, 'true']
 | 
						|
      ]);
 | 
						|
    }
 | 
						|
  }
 | 
						|
  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)
 | 
						|
  {
 | 
						|
 | 
						|
    forEach(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);
 | 
						|
 | 
						|
      function startBuilder()
 | 
						|
      {
 | 
						|
        var builder = Changeset.builder(oldLen);
 | 
						|
        builder.keep(spliceStartLineStart, spliceStartLine);
 | 
						|
        builder.keep(spliceStart - spliceStartLineStart);
 | 
						|
        return builder;
 | 
						|
      }
 | 
						|
 | 
						|
      function eachAttribRun(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 isStyleAttribute(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;
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  // 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;
 | 
						|
      if (mozillaFakeArrows) mozillaFakeArrows.notifySelectionChanged();
 | 
						|
      currentCallStack.repChanged = true;
 | 
						|
 | 
						|
      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 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
 | 
						|
      };
 | 
						|
 | 
						|
      function writeClass()
 | 
						|
      {
 | 
						|
        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
 | 
						|
  };
 | 
						|
 | 
						|
  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;
 | 
						|
      forEach(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 getLineEntryTopBottom(entry, destObj)
 | 
						|
  {
 | 
						|
    var dom = entry.lineNode;
 | 
						|
    var top = dom.offsetTop;
 | 
						|
    var height = dom.offsetHeight;
 | 
						|
    var obj = (destObj || {});
 | 
						|
    obj.top = top;
 | 
						|
    obj.bottom = (top + height);
 | 
						|
    return obj;
 | 
						|
  }
 | 
						|
 | 
						|
  function getViewPortTopBottom()
 | 
						|
  {
 | 
						|
    var theTop = getScrollY();
 | 
						|
    var doc = outerWin.document;
 | 
						|
    var height = doc.documentElement.clientHeight;
 | 
						|
    return {
 | 
						|
      top: theTop,
 | 
						|
      bottom: (theTop + height)
 | 
						|
    };
 | 
						|
  }
 | 
						|
 | 
						|
  function getVisibleLineRange()
 | 
						|
  {
 | 
						|
    var viewport = getViewPortTopBottom();
 | 
						|
    //console.log("viewport top/bottom: %o", viewport);
 | 
						|
    var obj = {};
 | 
						|
    var start = rep.lines.search(function(e)
 | 
						|
    {
 | 
						|
      return getLineEntryTopBottom(e, obj).bottom > viewport.top;
 | 
						|
    });
 | 
						|
    var end = rep.lines.search(function(e)
 | 
						|
    {
 | 
						|
      return getLineEntryTopBottom(e, obj).top >= viewport.bottom;
 | 
						|
    });
 | 
						|
    if (end < start) end = start; // unlikely
 | 
						|
    //console.log(start+","+end);
 | 
						|
    return [start, end];
 | 
						|
  }
 | 
						|
 | 
						|
  function getVisibleCharRange()
 | 
						|
  {
 | 
						|
    var lineRange = getVisibleLineRange();
 | 
						|
    return [rep.lines.offsetOfIndex(lineRange[0]), rep.lines.offsetOfIndex(lineRange[1])];
 | 
						|
  }
 | 
						|
 | 
						|
  function handleClick(evt)
 | 
						|
  {
 | 
						|
    //hide the dropdowns
 | 
						|
    if(window.top.padeditbar){ // required in case its in an iframe should probably use parent..  See Issue 327 https://github.com/Pita/etherpad-lite/issues/327
 | 
						|
      window.top.padeditbar.toogleDropDown("none");
 | 
						|
    }
 | 
						|
  
 | 
						|
    inCallStack("handleClick", function()
 | 
						|
    {
 | 
						|
      idleWorkTimer.atMost(200);
 | 
						|
    });
 | 
						|
 | 
						|
    // only want to catch left-click
 | 
						|
    if ((!evt.ctrlKey) && (evt.button != 2) && (evt.button != 3))
 | 
						|
    {
 | 
						|
      // find A tag with HREF
 | 
						|
 | 
						|
 | 
						|
      function isLink(n)
 | 
						|
      {
 | 
						|
        return (n.tagName || '').toLowerCase() == "a" && n.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();
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  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]+)([12345678])/.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))
 | 
						|
    {
 | 
						|
      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]+)([12345678])/.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 : '']);
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    if (mods.length > 0)
 | 
						|
    {
 | 
						|
      setLineListTypes(mods);
 | 
						|
    }
 | 
						|
 | 
						|
    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);
 | 
						|
            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 (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);
 | 
						|
  }
 | 
						|
 | 
						|
  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.windows && 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;
 | 
						|
 | 
						|
    //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;
 | 
						|
 | 
						|
    var specialHandled = false;
 | 
						|
    var isTypeForSpecialKey = ((browser.msie || browser.safari) ? (type == "keydown") : (type == "keypress"));
 | 
						|
    var isTypeForCmdKey = ((browser.msie || browser.safari) ? (type == "keydown") : (type == "keypress"));
 | 
						|
 | 
						|
    var stopped = false;
 | 
						|
 | 
						|
    inCallStack("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 (type == "keydown")
 | 
						|
      {
 | 
						|
        outsideKeyDown(evt);
 | 
						|
      }
 | 
						|
 | 
						|
      if (!stopped)
 | 
						|
      {
 | 
						|
        if (isTypeForSpecialKey && keyCode == 8)
 | 
						|
        {
 | 
						|
          // "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)
 | 
						|
        {
 | 
						|
          // 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 == 9 && !(evt.metaKey || evt.ctrlKey))
 | 
						|
        {
 | 
						|
          // 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)
 | 
						|
        {
 | 
						|
          // 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))
 | 
						|
        {
 | 
						|
          // cmd-Y (redo)
 | 
						|
          fastIncorp(10);
 | 
						|
          evt.preventDefault();
 | 
						|
          doUndoRedo("redo");
 | 
						|
          specialHandled = true;
 | 
						|
        }
 | 
						|
        if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "b" && (evt.metaKey || evt.ctrlKey))
 | 
						|
        {
 | 
						|
          // cmd-B (bold)
 | 
						|
          fastIncorp(13);
 | 
						|
          evt.preventDefault();
 | 
						|
          toggleAttributeOnSelection('bold');
 | 
						|
          specialHandled = true;
 | 
						|
        }
 | 
						|
        if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "i" && (evt.metaKey || evt.ctrlKey))
 | 
						|
        {
 | 
						|
          // cmd-I (italic)
 | 
						|
          fastIncorp(14);
 | 
						|
          evt.preventDefault();
 | 
						|
          toggleAttributeOnSelection('italic');
 | 
						|
          specialHandled = true;
 | 
						|
        }
 | 
						|
        if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "u" && (evt.metaKey || evt.ctrlKey))
 | 
						|
        {
 | 
						|
          // cmd-U (underline)
 | 
						|
          fastIncorp(15);
 | 
						|
          evt.preventDefault();
 | 
						|
          toggleAttributeOnSelection('underline');
 | 
						|
          specialHandled = true;
 | 
						|
        }
 | 
						|
        if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "h" && (evt.ctrlKey))
 | 
						|
        {
 | 
						|
          // cmd-H (backspace)
 | 
						|
          fastIncorp(20);
 | 
						|
          evt.preventDefault();
 | 
						|
          doDeleteKey();
 | 
						|
          specialHandled = true;
 | 
						|
        }
 | 
						|
 | 
						|
        if (mozillaFakeArrows && mozillaFakeArrows.handleKeyEvent(evt))
 | 
						|
        {
 | 
						|
          evt.preventDefault();
 | 
						|
          specialHandled = true;
 | 
						|
        }
 | 
						|
      }
 | 
						|
 | 
						|
      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 = 200;
 | 
						|
        idleWorkTimer.atLeast(wait);
 | 
						|
        idleWorkTimer.atMost(wait);
 | 
						|
      }
 | 
						|
 | 
						|
      // Is part of multi-keystroke international character on Firefox Mac
 | 
						|
      var isFirefoxHalfCharacter = (browser.mozilla && 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" || !incorpIfQuick())
 | 
						|
        {
 | 
						|
          observeChangesAroundSelection();
 | 
						|
        }
 | 
						|
      }
 | 
						|
 | 
						|
      if (type == "keyup")
 | 
						|
      {
 | 
						|
        thisKeyDoesntTriggerNormalize = false;
 | 
						|
      }
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  var thisKeyDoesntTriggerNormalize = false;
 | 
						|
 | 
						|
  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 mozillaCaretHack = (false && browser.mozilla && selStart && selEnd && selStart[0] == selEnd[0] && selStart[1] == rep.lines.atIndex(selStart[0]).lineMarker && selEnd[1] == rep.lines.atIndex(selEnd[0]).lineMarker && setupMozillaCaretHack(selStart[0]));
 | 
						|
 | 
						|
    var selection = {};
 | 
						|
 | 
						|
    var ss = [selStart[0], selStart[1]];
 | 
						|
    if (mozillaCaretHack) ss[1] += 1;
 | 
						|
    selection.startPoint = getPointForLineAndChar(ss);
 | 
						|
 | 
						|
    var se = [selEnd[0], selEnd[1]];
 | 
						|
    if (mozillaCaretHack) se[1] += 1;
 | 
						|
    selection.endPoint = getPointForLineAndChar(se);
 | 
						|
 | 
						|
    selection.focusAtStart = !! rep.selFocusAtStart;
 | 
						|
 | 
						|
    setSelection(selection);
 | 
						|
 | 
						|
    if (mozillaCaretHack)
 | 
						|
    {
 | 
						|
      mozillaCaretHack.unhack();
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  function getRepHTML()
 | 
						|
  {
 | 
						|
    return map(rep.lines.slice(), function(entry)
 | 
						|
    {
 | 
						|
      var text = entry.text;
 | 
						|
      var content;
 | 
						|
      if (text.length == 0)
 | 
						|
      {
 | 
						|
        content = '<span style="color: #aaa">--</span>';
 | 
						|
      }
 | 
						|
      else
 | 
						|
      {
 | 
						|
        content = htmlPrettyEscape(text);
 | 
						|
      }
 | 
						|
      return '<div><code>' + content + '</div></code>';
 | 
						|
    }).join('');
 | 
						|
  }
 | 
						|
 | 
						|
  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;
 | 
						|
 | 
						|
      function newRange()
 | 
						|
      {
 | 
						|
        return doc.body.createTextRange();
 | 
						|
      }
 | 
						|
 | 
						|
      function rangeForElementNode(nd)
 | 
						|
      {
 | 
						|
        var rng = newRange();
 | 
						|
        // doesn't work on text nodes
 | 
						|
        rng.moveToElementText(nd);
 | 
						|
        return rng;
 | 
						|
      }
 | 
						|
 | 
						|
      function pointFromCollapsedRange(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);
 | 
						|
/*if ((!selection.startPoint.node.isText) && (!selection.endPoint.node.isText)) {
 | 
						|
	  console.log(selection.startPoint.node.uniqueId()+","+
 | 
						|
		      selection.startPoint.index+" / "+
 | 
						|
		      selection.endPoint.node.uniqueId()+","+
 | 
						|
		      selection.endPoint.index);
 | 
						|
	}*/
 | 
						|
      }
 | 
						|
      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));
 | 
						|
        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.mozilla)
 | 
						|
    {
 | 
						|
      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", "");
 | 
						|
      }
 | 
						|
    }
 | 
						|
    // if near edge, scroll to edge
 | 
						|
    var scrollX = getScrollX();
 | 
						|
    var scrollY = getScrollY();
 | 
						|
    var win = outerWin;
 | 
						|
    var r = 20;
 | 
						|
 | 
						|
    enforceEditability();
 | 
						|
 | 
						|
    addClass(sideDiv, 'sidedivdelayed');
 | 
						|
  }
 | 
						|
 | 
						|
  function getScrollXY()
 | 
						|
  {
 | 
						|
    var win = outerWin;
 | 
						|
    var odoc = outerWin.document;
 | 
						|
    if (typeof(win.pageYOffset) == "number")
 | 
						|
    {
 | 
						|
      return {
 | 
						|
        x: win.pageXOffset,
 | 
						|
        y: win.pageYOffset
 | 
						|
      };
 | 
						|
    }
 | 
						|
    var docel = odoc.documentElement;
 | 
						|
    if (docel && typeof(docel.scrollTop) == "number")
 | 
						|
    {
 | 
						|
      return {
 | 
						|
        x: docel.scrollLeft,
 | 
						|
        y: docel.scrollTop
 | 
						|
      };
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  function getScrollX()
 | 
						|
  {
 | 
						|
    return getScrollXY().x;
 | 
						|
  }
 | 
						|
 | 
						|
  function getScrollY()
 | 
						|
  {
 | 
						|
    return getScrollXY().y;
 | 
						|
  }
 | 
						|
 | 
						|
  function setScrollX(x)
 | 
						|
  {
 | 
						|
    outerWin.scrollTo(x, getScrollY());
 | 
						|
  }
 | 
						|
 | 
						|
  function setScrollY(y)
 | 
						|
  {
 | 
						|
    outerWin.scrollTo(getScrollX(), y);
 | 
						|
  }
 | 
						|
 | 
						|
  function setScrollXY(x, y)
 | 
						|
  {
 | 
						|
    outerWin.scrollTo(x, y);
 | 
						|
  }
 | 
						|
 | 
						|
  var _teardownActions = [];
 | 
						|
 | 
						|
  function teardown()
 | 
						|
  {
 | 
						|
    forEach(_teardownActions, function(a)
 | 
						|
    {
 | 
						|
      a();
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  bindEventHandler(window, "load", setup);
 | 
						|
 | 
						|
  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;
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  function bindTheEventHandlers()
 | 
						|
  {
 | 
						|
    bindEventHandler(window, "unload", teardown);
 | 
						|
    bindEventHandler(document, "keydown", handleKeyEvent);
 | 
						|
    bindEventHandler(document, "keypress", handleKeyEvent);
 | 
						|
    bindEventHandler(document, "keyup", handleKeyEvent);
 | 
						|
    bindEventHandler(document, "click", handleClick);
 | 
						|
    bindEventHandler(root, "blur", handleBlur);
 | 
						|
    if (browser.msie)
 | 
						|
    {
 | 
						|
      bindEventHandler(document, "click", handleIEOuterClick);
 | 
						|
    }
 | 
						|
    if (browser.msie) bindEventHandler(root, "paste", handleIEPaste);
 | 
						|
    if ((!browser.msie) && document.documentElement)
 | 
						|
    {
 | 
						|
      bindEventHandler(document.documentElement, "compositionstart", handleCompositionEvent);
 | 
						|
      bindEventHandler(document.documentElement, "compositionend", handleCompositionEvent);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  function handleIEOuterClick(evt)
 | 
						|
  {
 | 
						|
    if ((evt.target.tagName || '').toLowerCase() != "html")
 | 
						|
    {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    if (!(evt.pageY > root.clientHeight))
 | 
						|
    {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    // click below the body
 | 
						|
    inCallStack("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 addClass(elem, className)
 | 
						|
  {
 | 
						|
    var seen = false;
 | 
						|
    var cc = getClassArray(elem, function(c)
 | 
						|
    {
 | 
						|
      if (c == className) seen = true;
 | 
						|
      return true;
 | 
						|
    });
 | 
						|
    if (!seen)
 | 
						|
    {
 | 
						|
      cc.push(className);
 | 
						|
      setClassArray(elem, cc);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  function removeClass(elem, className)
 | 
						|
  {
 | 
						|
    var seen = false;
 | 
						|
    var cc = getClassArray(elem, function(c)
 | 
						|
    {
 | 
						|
      if (c == className)
 | 
						|
      {
 | 
						|
        seen = true;
 | 
						|
        return false;
 | 
						|
      }
 | 
						|
      return true;
 | 
						|
    });
 | 
						|
    if (seen)
 | 
						|
    {
 | 
						|
      setClassArray(elem, cc);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  function setClassPresence(elem, className, present)
 | 
						|
  {
 | 
						|
    if (present) addClass(elem, className);
 | 
						|
    else removeClass(elem, className);
 | 
						|
  }
 | 
						|
 | 
						|
  function setup()
 | 
						|
  {
 | 
						|
    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.mozilla) addClass(root, "mozilla");
 | 
						|
      if (browser.safari) addClass(root, "safari");
 | 
						|
      if (browser.msie) addClass(root, "msie");
 | 
						|
      if (browser.msie)
 | 
						|
      {
 | 
						|
        // cache CSS background images
 | 
						|
        try
 | 
						|
        {
 | 
						|
          doc.execCommand("BackgroundImageCache", false, true);
 | 
						|
        }
 | 
						|
        catch (e)
 | 
						|
        { /* throws an error in some IE 6 but not others! */
 | 
						|
        }
 | 
						|
      }
 | 
						|
      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();
 | 
						|
 | 
						|
    });
 | 
						|
 | 
						|
    scheduler.setTimeout(function()
 | 
						|
    {
 | 
						|
      parent.readyFunc(); // defined in code that sets up the inner iframe
 | 
						|
    }, 0);
 | 
						|
 | 
						|
    isSetUp = true;
 | 
						|
  }
 | 
						|
 | 
						|
  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 bindEventHandler(target, type, func)
 | 
						|
  {
 | 
						|
    var handler;
 | 
						|
    if ((typeof func._wrapper) != "function")
 | 
						|
    {
 | 
						|
      func._wrapper = function(event)
 | 
						|
      {
 | 
						|
        func(fixEvent(event || window.event || {}));
 | 
						|
      }
 | 
						|
    }
 | 
						|
    var handler = func._wrapper;
 | 
						|
    if (target.addEventListener) target.addEventListener(type, handler, false);
 | 
						|
    else target.attachEvent("on" + type, handler);
 | 
						|
    _teardownActions.push(function()
 | 
						|
    {
 | 
						|
      unbindEventHandler(target, type, func);
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  function unbindEventHandler(target, type, func)
 | 
						|
  {
 | 
						|
    var handler = func._wrapper;
 | 
						|
    if (target.removeEventListener) target.removeEventListener(type, handler, false);
 | 
						|
    else target.detachEvent("on" + type, handler);
 | 
						|
  }
 | 
						|
 | 
						|
  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 scrollNodeVerticallyIntoView(node)
 | 
						|
  {
 | 
						|
    // requires element (non-text) node;
 | 
						|
    // if node extends above top of viewport or below bottom of viewport (or top of scrollbar),
 | 
						|
    // scroll it the minimum distance needed to be completely in view.
 | 
						|
    var win = outerWin;
 | 
						|
    var odoc = outerWin.document;
 | 
						|
    var distBelowTop = node.offsetTop + iframePadTop - win.scrollY;
 | 
						|
    var distAboveBottom = win.scrollY + getInnerHeight() - (node.offsetTop + iframePadTop + node.offsetHeight);
 | 
						|
 | 
						|
    if (distBelowTop < 0)
 | 
						|
    {
 | 
						|
      win.scrollBy(0, distBelowTop);
 | 
						|
    }
 | 
						|
    else if (distAboveBottom < 0)
 | 
						|
    {
 | 
						|
      win.scrollBy(0, -distAboveBottom);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  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 focusLine = (rep.selFocusAtStart ? rep.selStart[0] : rep.selEnd[0]);
 | 
						|
    scrollNodeVerticallyIntoView(rep.lines.atIndex(focusLine).lineNode);
 | 
						|
    if (!doesWrap)
 | 
						|
    {
 | 
						|
      var browserSelection = getSelection();
 | 
						|
      if (browserSelection)
 | 
						|
      {
 | 
						|
        var focusPoint = (browserSelection.focusAtStart ? browserSelection.startPoint : browserSelection.endPoint);
 | 
						|
        var selectionPointX = getSelectionPointX(focusPoint);
 | 
						|
        scrollXHorizontallyIntoView(selectionPointX);
 | 
						|
        fixView();
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  function getLineListType(lineNum)
 | 
						|
  {
 | 
						|
    // get "list" attribute of first char of line
 | 
						|
    var aline = rep.alines[lineNum];
 | 
						|
    if (aline)
 | 
						|
    {
 | 
						|
      var opIter = Changeset.opIterator(aline);
 | 
						|
      if (opIter.hasNext())
 | 
						|
      {
 | 
						|
        return Changeset.opAttributeValue(opIter.next(), 'list', rep.apool) || '';
 | 
						|
      }
 | 
						|
    }
 | 
						|
    return '';
 | 
						|
  }
 | 
						|
 | 
						|
  function setLineListType(lineNum, listType)
 | 
						|
  {
 | 
						|
    setLineListTypes([
 | 
						|
      [lineNum, listType]
 | 
						|
    ]);
 | 
						|
  }
 | 
						|
  
 | 
						|
  function renumberList(lineNum){
 | 
						|
    //1-check we are in a list
 | 
						|
    var type = getLineListType(lineNum);
 | 
						|
    if(!type)
 | 
						|
    {
 | 
						|
      return null;
 | 
						|
    }
 | 
						|
    type = /([a-z]+)[12345678]/.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]+)[12345678]/.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());
 | 
						|
    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]+)([12345678])/.exec(listType);
 | 
						|
        curLevel = Number(listType[2]);
 | 
						|
        if(isNaN(curLevel) || listType[0] == "indent")
 | 
						|
        {
 | 
						|
          return line;
 | 
						|
        }
 | 
						|
        else if(curLevel == level)
 | 
						|
        {
 | 
						|
          buildKeepRange(builder, loc, (loc = [line, 0]));
 | 
						|
          buildKeepRange(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 setLineListTypes(lineNumTypePairsInOrder)
 | 
						|
  {
 | 
						|
    var loc = [0, 0];
 | 
						|
    var builder = Changeset.builder(rep.lines.totalWidth());
 | 
						|
    for (var i = 0; i < lineNumTypePairsInOrder.length; i++)
 | 
						|
    {
 | 
						|
      var pair = lineNumTypePairsInOrder[i];
 | 
						|
      var lineNum = pair[0];
 | 
						|
      var listType = pair[1];
 | 
						|
      buildKeepRange(builder, loc, (loc = [lineNum, 0]));
 | 
						|
      if (getLineListType(lineNum))
 | 
						|
      {
 | 
						|
        // already a line marker
 | 
						|
        if (listType)
 | 
						|
        {
 | 
						|
          // make different list type
 | 
						|
          buildKeepRange(builder, loc, (loc = [lineNum, 1]), [
 | 
						|
            ['list', listType]
 | 
						|
          ], rep.apool);
 | 
						|
        }
 | 
						|
        else
 | 
						|
        {
 | 
						|
          // remove list marker
 | 
						|
          buildRemoveRange(builder, loc, (loc = [lineNum, 1]));
 | 
						|
        }
 | 
						|
      }
 | 
						|
      else
 | 
						|
      {
 | 
						|
        // currently no line marker
 | 
						|
        if (listType)
 | 
						|
        {
 | 
						|
          // add a line marker
 | 
						|
          builder.insert('*', [
 | 
						|
            ['author', thisAuthor],
 | 
						|
            ['insertorder', 'first'],
 | 
						|
            ['list', listType]
 | 
						|
          ], rep.apool);
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    var cs = builder.toString();
 | 
						|
    if (!Changeset.isIdentity(cs))
 | 
						|
    {
 | 
						|
      performDocumentApplyChangeset(cs);
 | 
						|
    }
 | 
						|
    
 | 
						|
    //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 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]+)([12345678])/.exec(getLineListType(n));
 | 
						|
      if (listType)
 | 
						|
      {
 | 
						|
        t = listType[1];
 | 
						|
        level = Number(listType[2]);
 | 
						|
      }
 | 
						|
      var t = getLineListType(n);
 | 
						|
      mods.push([n, allLinesAreList ? 'indent' + level : (t ? type + level : type + '1')]);
 | 
						|
    }
 | 
						|
    setLineListTypes(mods);
 | 
						|
  }
 | 
						|
  
 | 
						|
  function doInsertUnorderedList(){
 | 
						|
    doInsertList('bullet');
 | 
						|
  }
 | 
						|
  function doInsertOrderedList(){
 | 
						|
    doInsertList('number');
 | 
						|
  }
 | 
						|
  editorInfo.ace_doInsertUnorderedList = doInsertUnorderedList;
 | 
						|
  editorInfo.ace_doInsertOrderedList = doInsertOrderedList;
 | 
						|
 | 
						|
  var mozillaFakeArrows = (browser.mozilla && (function()
 | 
						|
  {
 | 
						|
    // In Firefox 2, arrow keys are unstable while DOM-manipulating
 | 
						|
    // operations are going on.  Specifically, if an operation
 | 
						|
    // (computation that ties up the event queue) is going on (in the
 | 
						|
    // call-stack of some event, like a timeout) that at some point
 | 
						|
    // mutates nodes involved in the selection, then the arrow
 | 
						|
    // keypress may (randomly) move the caret to the beginning or end
 | 
						|
    // of the document.  If the operation also mutates the selection
 | 
						|
    // range, the old selection or the new selection may be used, or
 | 
						|
    // neither.
 | 
						|
    // As long as the arrow is pressed during the busy operation, it
 | 
						|
    // doesn't seem to matter that the keydown and keypress events
 | 
						|
    // aren't generated until afterwards, or that the arrow movement
 | 
						|
    // can still be stopped (meaning it hasn't been performed yet);
 | 
						|
    // Firefox must be preserving some old information about the
 | 
						|
    // selection or the DOM from when the key was initially pressed.
 | 
						|
    // However, it also doesn't seem to matter when the key was
 | 
						|
    // actually pressed relative to the time of the mutation within
 | 
						|
    // the prolonged operation.  Also, even in very controlled tests
 | 
						|
    // (like a mutation followed by a long period of busyWaiting), the
 | 
						|
    // problem shows up often but not every time, with no discernable
 | 
						|
    // pattern.  Who knows, it could have something to do with the
 | 
						|
    // caret-blinking timer, or DOM changes not being applied
 | 
						|
    // immediately.
 | 
						|
    // This problem, mercifully, does not show up at all in IE or
 | 
						|
    // Safari.  My solution is to have my own, full-featured arrow-key
 | 
						|
    // implementation for Firefox.
 | 
						|
    // Note that the problem addressed here is potentially very subtle,
 | 
						|
    // especially if the operation is quick and is timed to usually happen
 | 
						|
    // when the user is idle.
 | 
						|
    // features:
 | 
						|
    // - 'up' and 'down' arrows preserve column when passing through shorter lines
 | 
						|
    // - shift-arrows extend the "focus" point, which may be start or end of range
 | 
						|
    // - the focus point is kept horizontally and vertically scrolled into view
 | 
						|
    // - arrows without shift cause caret to move to beginning or end of selection (left,right)
 | 
						|
    //   or move focus point up or down a line (up,down)
 | 
						|
    // - command-(left,right,up,down) on Mac acts like (line-start, line-end, doc-start, doc-end)
 | 
						|
    // - takes wrapping into account when doesWrap is true, i.e. up-arrow and down-arrow move
 | 
						|
    //   between the virtual lines within a wrapped line; this was difficult, and unfortunately
 | 
						|
    //   requires mutating the DOM to get the necessary information
 | 
						|
    var savedFocusColumn = 0; // a value of 0 has no effect
 | 
						|
    var updatingSelectionNow = false;
 | 
						|
 | 
						|
    function getVirtualLineView(lineNum)
 | 
						|
    {
 | 
						|
      var lineNode = rep.lines.atIndex(lineNum).lineNode;
 | 
						|
      while (lineNode.firstChild && isBlockElement(lineNode.firstChild))
 | 
						|
      {
 | 
						|
        lineNode = lineNode.firstChild;
 | 
						|
      }
 | 
						|
      return makeVirtualLineView(lineNode);
 | 
						|
    }
 | 
						|
 | 
						|
    function markerlessLineAndChar(line, chr)
 | 
						|
    {
 | 
						|
      return [line, chr - rep.lines.atIndex(line).lineMarker];
 | 
						|
    }
 | 
						|
 | 
						|
    function markerfulLineAndChar(line, chr)
 | 
						|
    {
 | 
						|
      return [line, chr + rep.lines.atIndex(line).lineMarker];
 | 
						|
    }
 | 
						|
 | 
						|
    return {
 | 
						|
      notifySelectionChanged: function()
 | 
						|
      {
 | 
						|
        if (!updatingSelectionNow)
 | 
						|
        {
 | 
						|
          savedFocusColumn = 0;
 | 
						|
        }
 | 
						|
      },
 | 
						|
      handleKeyEvent: function(evt)
 | 
						|
      {
 | 
						|
        // returns "true" if handled
 | 
						|
        if (evt.type != "keypress") return false;
 | 
						|
        var keyCode = evt.keyCode;
 | 
						|
        if (keyCode < 37 || keyCode > 40) return false;
 | 
						|
        incorporateUserChanges();
 | 
						|
 | 
						|
        if (!(rep.selStart && rep.selEnd)) return true;
 | 
						|
 | 
						|
        // {byWord,toEnd,normal}
 | 
						|
        var moveMode = (evt.altKey ? "byWord" : (evt.ctrlKey ? "byWord" : (evt.metaKey ? "toEnd" : "normal")));
 | 
						|
 | 
						|
        var anchorCaret = markerlessLineAndChar(rep.selStart[0], rep.selStart[1]);
 | 
						|
        var focusCaret = markerlessLineAndChar(rep.selEnd[0], rep.selEnd[1]);
 | 
						|
        var wasCaret = isCaret();
 | 
						|
        if (rep.selFocusAtStart)
 | 
						|
        {
 | 
						|
          var tmp = anchorCaret;
 | 
						|
          anchorCaret = focusCaret;
 | 
						|
          focusCaret = tmp;
 | 
						|
        }
 | 
						|
        var K_UP = 38,
 | 
						|
            K_DOWN = 40,
 | 
						|
            K_LEFT = 37,
 | 
						|
            K_RIGHT = 39;
 | 
						|
        var dontMove = false;
 | 
						|
        if (wasCaret && !evt.shiftKey)
 | 
						|
        {
 | 
						|
          // collapse, will mutate both together
 | 
						|
          anchorCaret = focusCaret;
 | 
						|
        }
 | 
						|
        else if ((!wasCaret) && (!evt.shiftKey))
 | 
						|
        {
 | 
						|
          if (keyCode == K_LEFT)
 | 
						|
          {
 | 
						|
            // place caret at beginning
 | 
						|
            if (rep.selFocusAtStart) anchorCaret = focusCaret;
 | 
						|
            else focusCaret = anchorCaret;
 | 
						|
            if (moveMode == "normal") dontMove = true;
 | 
						|
          }
 | 
						|
          else if (keyCode == K_RIGHT)
 | 
						|
          {
 | 
						|
            // place caret at end
 | 
						|
            if (rep.selFocusAtStart) focusCaret = anchorCaret;
 | 
						|
            else anchorCaret = focusCaret;
 | 
						|
            if (moveMode == "normal") dontMove = true;
 | 
						|
          }
 | 
						|
          else
 | 
						|
          {
 | 
						|
            // collapse, will mutate both together
 | 
						|
            anchorCaret = focusCaret;
 | 
						|
          }
 | 
						|
        }
 | 
						|
        if (!dontMove)
 | 
						|
        {
 | 
						|
          function lineLength(i)
 | 
						|
          {
 | 
						|
            var entry = rep.lines.atIndex(i);
 | 
						|
            return entry.text.length - entry.lineMarker;
 | 
						|
          }
 | 
						|
 | 
						|
          function lineText(i)
 | 
						|
          {
 | 
						|
            var entry = rep.lines.atIndex(i);
 | 
						|
            return entry.text.substring(entry.lineMarker);
 | 
						|
          }
 | 
						|
 | 
						|
          if (keyCode == K_UP || keyCode == K_DOWN)
 | 
						|
          {
 | 
						|
            var up = (keyCode == K_UP);
 | 
						|
            var canChangeLines = ((up && focusCaret[0]) || ((!up) && focusCaret[0] < rep.lines.length() - 1));
 | 
						|
            var virtualLineView, virtualLineSpot, canChangeVirtualLines = false;
 | 
						|
            if (doesWrap)
 | 
						|
            {
 | 
						|
              virtualLineView = getVirtualLineView(focusCaret[0]);
 | 
						|
              virtualLineSpot = virtualLineView.getVLineAndOffsetForChar(focusCaret[1]);
 | 
						|
              canChangeVirtualLines = ((up && virtualLineSpot.vline > 0) || ((!up) && virtualLineSpot.vline < (
 | 
						|
              virtualLineView.getNumVirtualLines() - 1)));
 | 
						|
            }
 | 
						|
            var newColByVirtualLineChange;
 | 
						|
            if (moveMode == "toEnd")
 | 
						|
            {
 | 
						|
              if (up)
 | 
						|
              {
 | 
						|
                focusCaret[0] = 0;
 | 
						|
                focusCaret[1] = 0;
 | 
						|
              }
 | 
						|
              else
 | 
						|
              {
 | 
						|
                focusCaret[0] = rep.lines.length() - 1;
 | 
						|
                focusCaret[1] = lineLength(focusCaret[0]);
 | 
						|
              }
 | 
						|
            }
 | 
						|
            else if (moveMode == "byWord")
 | 
						|
            {
 | 
						|
              // move by "paragraph", a feature that Firefox lacks but IE and Safari both have
 | 
						|
              if (up)
 | 
						|
              {
 | 
						|
                if (focusCaret[1] == 0 && canChangeLines)
 | 
						|
                {
 | 
						|
                  focusCaret[0]--;
 | 
						|
                  focusCaret[1] = 0;
 | 
						|
                }
 | 
						|
                else focusCaret[1] = 0;
 | 
						|
              }
 | 
						|
              else
 | 
						|
              {
 | 
						|
                var lineLen = lineLength(focusCaret[0]);
 | 
						|
                if (browser.windows)
 | 
						|
                {
 | 
						|
                  if (canChangeLines)
 | 
						|
                  {
 | 
						|
                    focusCaret[0]++;
 | 
						|
                    focusCaret[1] = 0;
 | 
						|
                  }
 | 
						|
                  else
 | 
						|
                  {
 | 
						|
                    focusCaret[1] = lineLen;
 | 
						|
                  }
 | 
						|
                }
 | 
						|
                else
 | 
						|
                {
 | 
						|
                  if (focusCaret[1] == lineLen && canChangeLines)
 | 
						|
                  {
 | 
						|
                    focusCaret[0]++;
 | 
						|
                    focusCaret[1] = lineLength(focusCaret[0]);
 | 
						|
                  }
 | 
						|
                  else
 | 
						|
                  {
 | 
						|
                    focusCaret[1] = lineLen;
 | 
						|
                  }
 | 
						|
                }
 | 
						|
              }
 | 
						|
              savedFocusColumn = 0;
 | 
						|
            }
 | 
						|
            else if (canChangeVirtualLines)
 | 
						|
            {
 | 
						|
              var vline = virtualLineSpot.vline;
 | 
						|
              var offset = virtualLineSpot.offset;
 | 
						|
              if (up) vline--;
 | 
						|
              else vline++;
 | 
						|
              if (savedFocusColumn > offset) offset = savedFocusColumn;
 | 
						|
              else
 | 
						|
              {
 | 
						|
                savedFocusColumn = offset;
 | 
						|
              }
 | 
						|
              var newSpot = virtualLineView.getCharForVLineAndOffset(vline, offset);
 | 
						|
              focusCaret[1] = newSpot.lineChar;
 | 
						|
            }
 | 
						|
            else if (canChangeLines)
 | 
						|
            {
 | 
						|
              if (up) focusCaret[0]--;
 | 
						|
              else focusCaret[0]++;
 | 
						|
              var offset = focusCaret[1];
 | 
						|
              if (doesWrap)
 | 
						|
              {
 | 
						|
                offset = virtualLineSpot.offset;
 | 
						|
              }
 | 
						|
              if (savedFocusColumn > offset) offset = savedFocusColumn;
 | 
						|
              else
 | 
						|
              {
 | 
						|
                savedFocusColumn = offset;
 | 
						|
              }
 | 
						|
              if (doesWrap)
 | 
						|
              {
 | 
						|
                var newLineView = getVirtualLineView(focusCaret[0]);
 | 
						|
                var vline = (up ? newLineView.getNumVirtualLines() - 1 : 0);
 | 
						|
                var newSpot = newLineView.getCharForVLineAndOffset(vline, offset);
 | 
						|
                focusCaret[1] = newSpot.lineChar;
 | 
						|
              }
 | 
						|
              else
 | 
						|
              {
 | 
						|
                var lineLen = lineLength(focusCaret[0]);
 | 
						|
                if (offset > lineLen) offset = lineLen;
 | 
						|
                focusCaret[1] = offset;
 | 
						|
              }
 | 
						|
            }
 | 
						|
            else
 | 
						|
            {
 | 
						|
              if (up) focusCaret[1] = 0;
 | 
						|
              else focusCaret[1] = lineLength(focusCaret[0]);
 | 
						|
              savedFocusColumn = 0;
 | 
						|
            }
 | 
						|
          }
 | 
						|
          else if (keyCode == K_LEFT || keyCode == K_RIGHT)
 | 
						|
          {
 | 
						|
            var left = (keyCode == K_LEFT);
 | 
						|
            if (left)
 | 
						|
            {
 | 
						|
              if (moveMode == "toEnd") focusCaret[1] = 0;
 | 
						|
              else if (focusCaret[1] > 0)
 | 
						|
              {
 | 
						|
                if (moveMode == "byWord")
 | 
						|
                {
 | 
						|
                  focusCaret[1] = moveByWordInLine(lineText(focusCaret[0]), focusCaret[1], false);
 | 
						|
                }
 | 
						|
                else
 | 
						|
                {
 | 
						|
                  focusCaret[1]--;
 | 
						|
                }
 | 
						|
              }
 | 
						|
              else if (focusCaret[0] > 0)
 | 
						|
              {
 | 
						|
                focusCaret[0]--;
 | 
						|
                focusCaret[1] = lineLength(focusCaret[0]);
 | 
						|
                if (moveMode == "byWord")
 | 
						|
                {
 | 
						|
                  focusCaret[1] = moveByWordInLine(lineText(focusCaret[0]), focusCaret[1], false);
 | 
						|
                }
 | 
						|
              }
 | 
						|
            }
 | 
						|
            else
 | 
						|
            {
 | 
						|
              var lineLen = lineLength(focusCaret[0]);
 | 
						|
              if (moveMode == "toEnd") focusCaret[1] = lineLen;
 | 
						|
              else if (focusCaret[1] < lineLen)
 | 
						|
              {
 | 
						|
                if (moveMode == "byWord")
 | 
						|
                {
 | 
						|
                  focusCaret[1] = moveByWordInLine(lineText(focusCaret[0]), focusCaret[1], true);
 | 
						|
                }
 | 
						|
                else
 | 
						|
                {
 | 
						|
                  focusCaret[1]++;
 | 
						|
                }
 | 
						|
              }
 | 
						|
              else if (focusCaret[0] < rep.lines.length() - 1)
 | 
						|
              {
 | 
						|
                focusCaret[0]++;
 | 
						|
                focusCaret[1] = 0;
 | 
						|
                if (moveMode == "byWord")
 | 
						|
                {
 | 
						|
                  focusCaret[1] = moveByWordInLine(lineText(focusCaret[0]), focusCaret[1], true);
 | 
						|
                }
 | 
						|
              }
 | 
						|
            }
 | 
						|
            savedFocusColumn = 0;
 | 
						|
          }
 | 
						|
        }
 | 
						|
 | 
						|
        var newSelFocusAtStart = ((focusCaret[0] < anchorCaret[0]) || (focusCaret[0] == anchorCaret[0] && focusCaret[1] < anchorCaret[1]));
 | 
						|
        var newSelStart = (newSelFocusAtStart ? focusCaret : anchorCaret);
 | 
						|
        var newSelEnd = (newSelFocusAtStart ? anchorCaret : focusCaret);
 | 
						|
        updatingSelectionNow = true;
 | 
						|
        performSelectionChange(markerfulLineAndChar(newSelStart[0], newSelStart[1]), markerfulLineAndChar(newSelEnd[0], newSelEnd[1]), newSelFocusAtStart);
 | 
						|
        updatingSelectionNow = false;
 | 
						|
        currentCallStack.userChangedSelection = true;
 | 
						|
        return true;
 | 
						|
      }
 | 
						|
    };
 | 
						|
  })());
 | 
						|
 | 
						|
 | 
						|
  // stolen from jquery-1.2.1
 | 
						|
 | 
						|
 | 
						|
  function fixEvent(event)
 | 
						|
  {
 | 
						|
    // store a copy of the original event object
 | 
						|
    // and clone to set read-only properties
 | 
						|
    var originalEvent = event;
 | 
						|
    event = extend(
 | 
						|
    {}, originalEvent);
 | 
						|
 | 
						|
    // add preventDefault and stopPropagation since
 | 
						|
    // they will not work on the clone
 | 
						|
    event.preventDefault = function()
 | 
						|
    {
 | 
						|
      // if preventDefault exists run it on the original event
 | 
						|
      if (originalEvent.preventDefault) originalEvent.preventDefault();
 | 
						|
      // otherwise set the returnValue property of the original event to false (IE)
 | 
						|
      originalEvent.returnValue = false;
 | 
						|
    };
 | 
						|
    event.stopPropagation = function()
 | 
						|
    {
 | 
						|
      // if stopPropagation exists run it on the original event
 | 
						|
      if (originalEvent.stopPropagation) originalEvent.stopPropagation();
 | 
						|
      // otherwise set the cancelBubble property of the original event to true (IE)
 | 
						|
      originalEvent.cancelBubble = true;
 | 
						|
    };
 | 
						|
 | 
						|
    // Fix target property, if necessary
 | 
						|
    if (!event.target && event.srcElement) event.target = event.srcElement;
 | 
						|
 | 
						|
    // check if target is a textnode (safari)
 | 
						|
    if (browser.safari && event.target.nodeType == 3) event.target = originalEvent.target.parentNode;
 | 
						|
 | 
						|
    // Add relatedTarget, if necessary
 | 
						|
    if (!event.relatedTarget && event.fromElement) event.relatedTarget = event.fromElement == event.target ? event.toElement : event.fromElement;
 | 
						|
 | 
						|
    // Calculate pageX/Y if missing and clientX/Y available
 | 
						|
    if (event.pageX == null && event.clientX != null)
 | 
						|
    {
 | 
						|
      var e = document.documentElement,
 | 
						|
          b = document.body;
 | 
						|
      event.pageX = event.clientX + (e && e.scrollLeft || b.scrollLeft || 0);
 | 
						|
      event.pageY = event.clientY + (e && e.scrollTop || b.scrollTop || 0);
 | 
						|
    }
 | 
						|
 | 
						|
    // Add which for key events
 | 
						|
    if (!event.which && (event.charCode || event.keyCode)) event.which = event.charCode || event.keyCode;
 | 
						|
 | 
						|
    // Add metaKey to non-Mac browsers (use ctrl for PC's and Meta for Macs)
 | 
						|
    if (!event.metaKey && event.ctrlKey) event.metaKey = event.ctrlKey;
 | 
						|
 | 
						|
    // Add which for click: 1 == left; 2 == middle; 3 == right
 | 
						|
    // Note: button is not normalized, so don't use it
 | 
						|
    if (!event.which && event.button) event.which = (event.button & 1 ? 1 : (event.button & 2 ? 3 : (event.button & 4 ? 2 : 0)));
 | 
						|
 | 
						|
    return event;
 | 
						|
  }
 | 
						|
 | 
						|
  var lineNumbersShown;
 | 
						|
  var sideDivInner;
 | 
						|
 | 
						|
  function initLineNumbers()
 | 
						|
  {
 | 
						|
    lineNumbersShown = 1;
 | 
						|
    sideDiv.innerHTML = '<table border="0" cellpadding="0" cellspacing="0" align="right">' + '<tr><td id="sidedivinner"><div>1</div></td></tr></table>';
 | 
						|
    sideDivInner = outerWin.document.getElementById("sidedivinner");
 | 
						|
  }
 | 
						|
 | 
						|
  function updateLineNumbers()
 | 
						|
  {
 | 
						|
    var newNumLines = rep.lines.length();
 | 
						|
    if (newNumLines < 1) newNumLines = 1;
 | 
						|
	//update height of all current line numbers
 | 
						|
    if (currentCallStack && currentCallStack.domClean)
 | 
						|
    {
 | 
						|
      var a = sideDivInner.firstChild;
 | 
						|
      var b = doc.body.firstChild;
 | 
						|
      var n = 0;
 | 
						|
      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...)
 | 
						|
          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
 | 
						|
	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);
 | 
						|
	b = b.nextSibling;
 | 
						|
      }
 | 
						|
      container.appendChild(fragment);
 | 
						|
      while (lineNumbersShown > newNumLines)
 | 
						|
      {
 | 
						|
        container.removeChild(container.lastChild);
 | 
						|
        lineNumbersShown--;
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
  
 | 
						|
};
 | 
						|
 | 
						|
exports.editor = new Ace2Inner(); |