/** * 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; 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 '
' + buf.join('
\n
') + '
'; } 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 = "
"; } 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]: -------\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 = '--'; } else { content = htmlPrettyEscape(text); } return '
' + content + '
'; }).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 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 "

one

two

three

", // 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('
'); //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 = '' + '
1
'; 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--; } } }