/** * 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. */ function OUTER(gscope) { var DEBUG=true;//$$ build script replaces the string "var DEBUG=true;//$$" with "var DEBUG=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 && top.console); if (! 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}; }; } function noop() {} function identity(x) { return x; } // "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; function initDynamicCSS() { dynamicCSS = makeCSSManager("dynamicsyntax"); } 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))); } } else { authorInfos[author] = info; if (info.bgcolor) { if (dynamicCSS) { var bgcolor = info.bgcolor; if ((typeof info.fade) == "number") { bgcolor = fadeColor(bgcolor, info.fade); } dynamicCSS.selectorStyle(getAuthorColorClassSelector( getAuthorClassName(author))).backgroundColor = bgcolor; } } } } 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); } 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(); } recolorModule.recolorLines(); 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; } function inCallStackIfNecessary(type, action) { if (! currentCallStack) { inCallStack(type, action); } else { action(); } } 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; } var recolorModule = (function() { var dirtyLineKeys = {}; var module = {}; module.setCharNeedsRecoloring = function(offset) { if (offset >= rep.alltext.length) { offset = rep.alltext.length-1; } dirtyLineKeys[getLineKeyForOffset(offset)] = true; } module.setCharRangeNeedsRecoloring = function(offset1, offset2) { if (offset1 >= rep.alltext.length) { offset1 = rep.alltext.length-1; } if (offset2 >= rep.alltext.length) { offset2 = rep.alltext.length-1; } var firstEntry = rep.lines.atOffset(offset1); var lastKey = rep.lines.atOffset(offset2).key; dirtyLineKeys[lastKey] = true; var entry = firstEntry; while (entry && entry.key != lastKey) { dirtyLineKeys[entry.key] = true; entry = rep.lines.next(entry); } } module.recolorLines = function() { for(var k in dirtyLineKeys) { recolorLineByKey(k); } dirtyLineKeys = {}; } return module; })(); var parenModule = (function() { var module = {}; module.notifyTick = function() { handleFlashing(false); }; module.notifyChange = function() { handleFlashing(true); }; module.shouldNormalizeOnChar = function (c) { if (parenFlashRep.active) { // avoid highlight style from carrying on to typed text return true; } c = String.fromCharCode(c); return !! (bracketMap[c]); } var parenFlashRep = { active: false, whichChars: null, whichLineKeys: null, expireTime: null }; var bracketMap = {'(': 1, ')':-1, '[':2, ']':-2, '{':3, '}':-3}; var bracketRegex = /[{}\[\]()]/g; function handleFlashing(docChanged) { function getSearchRange(aroundLoc) { var rng = getVisibleCharRange(); var d = 100; // minimum radius var e = 3000; // maximum radius; if (rng[0] > aroundLoc-d) rng[0] = aroundLoc-d; if (rng[0] < aroundLoc-e) rng[0] = aroundLoc-e; if (rng[0] < 0) rng[0] = 0; if (rng[1] < aroundLoc+d) rng[1] = aroundLoc+d; if (rng[1] > aroundLoc+e) rng[1] = aroundLoc+e; if (rng[1] > rep.lines.totalWidth()) rng[1] = rep.lines.totalWidth(); return rng; } function findMatchingVisibleBracket(startLoc, forwards) { var rng = getSearchRange(startLoc); var str = rep.alltext.substring(rng[0], rng[1]); var bstr = str.replace(bracketRegex, '('); // handy for searching var loc = startLoc - rng[0]; var bracketState = []; var foundParen = false; var goodParen = false; function nextLoc() { if (loc < 0) return; if (forwards) loc++; else loc--; if (loc < 0 || loc >= str.length) loc = -1; if (loc >= 0) { if (forwards) loc = bstr.indexOf('(', loc); else loc = bstr.lastIndexOf('(', loc); } } while ((! foundParen) && (loc >= 0)) { if (getCharType(loc + rng[0]) == "p") { var b = bracketMap[str.charAt(loc)]; // -1, 1, -2, 2, -3, 3 var into = forwards; var typ = b; if (typ < 0) { into = ! into; typ = -typ; } if (into) bracketState.push(typ); else { var recent = bracketState.pop(); if (recent != typ) { foundParen = true; goodParen = false; } else if (bracketState.length == 0) { foundParen = true; goodParen = true; } } } //console.log(bracketState.toSource()); if ((! foundParen) && (loc >= 0)) nextLoc(); } if (! foundParen) return null; return {chr: (loc + rng[0]), good: goodParen}; } var r = parenFlashRep; var charsToHighlight = null; var linesToUnhighlight = null; if (r.active && (docChanged || (now() > r.expireTime))) { linesToUnhighlight = r.whichLineKeys; r.active = false; } if ((! r.active) && docChanged && isCaret() && caretColumn() > 0) { var caret = caretDocChar(); if (caret > 0 && getCharType(caret-1) == "p") { var charBefore = rep.alltext.charAt(caret-1); if (bracketMap[charBefore]) { var lookForwards = (bracketMap[charBefore] > 0); var findResult = findMatchingVisibleBracket(caret-1, lookForwards); if (findResult) { var mateLoc = findResult.chr; var mateGood = findResult.good; r.active = true; charsToHighlight = {}; charsToHighlight[caret-1] = 'flash'; charsToHighlight[mateLoc] = (mateGood ? 'flash' : 'flashbad'); r.whichLineKeys = []; r.whichLineKeys.push(getLineKeyForOffset(caret-1)); r.whichLineKeys.push(getLineKeyForOffset(mateLoc)); r.expireTime = now() + 4000; newlyActive = true; } } } } if (linesToUnhighlight) { recolorLineByKey(linesToUnhighlight[0]); recolorLineByKey(linesToUnhighlight[1]); } if (r.active && charsToHighlight) { function f(txt, cls, next, ofst) { var flashClass = charsToHighlight[ofst]; if (cls) { next(txt, cls+" "+flashClass); } else next(txt, cls); } for(var c in charsToHighlight) { recolorLinesInRange((+c), (+c)+1, null, f); } } } return module; })(); 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