Home | History | Annotate | Download | only in front_end
      1 /*
      2  * Copyright (C) 2012 Google Inc. All rights reserved.
      3  *
      4  * Redistribution and use in source and binary forms, with or without
      5  * modification, are permitted provided that the following conditions are
      6  * met:
      7  *
      8  *     * Redistributions of source code must retain the above copyright
      9  * notice, this list of conditions and the following disclaimer.
     10  *     * Redistributions in binary form must reproduce the above
     11  * copyright notice, this list of conditions and the following disclaimer
     12  * in the documentation and/or other materials provided with the
     13  * distribution.
     14  *     * Neither the name of Google Inc. nor the names of its
     15  * contributors may be used to endorse or promote products derived from
     16  * this software without specific prior written permission.
     17  *
     18  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
     19  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
     20  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
     21  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
     22  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
     23  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
     24  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
     25  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
     26  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     27  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
     28  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     29  */
     30 
     31 importScript("cm/codemirror.js");
     32 importScript("cm/css.js");
     33 importScript("cm/javascript.js");
     34 importScript("cm/xml.js");
     35 importScript("cm/htmlmixed.js");
     36 
     37 importScript("cm/matchbrackets.js");
     38 importScript("cm/closebrackets.js");
     39 importScript("cm/markselection.js");
     40 importScript("cm/comment.js");
     41 importScript("cm/overlay.js");
     42 
     43 importScript("cm/htmlembedded.js");
     44 importScript("cm/clike.js");
     45 importScript("cm/coffeescript.js");
     46 importScript("cm/php.js");
     47 importScript("cm/python.js");
     48 importScript("cm/shell.js");
     49 
     50 /**
     51  * @constructor
     52  * @extends {WebInspector.View}
     53  * @implements {WebInspector.TextEditor}
     54  * @param {?string} url
     55  * @param {WebInspector.TextEditorDelegate} delegate
     56  */
     57 WebInspector.CodeMirrorTextEditor = function(url, delegate)
     58 {
     59     WebInspector.View.call(this);
     60     this._delegate = delegate;
     61     this._url = url;
     62 
     63     this.registerRequiredCSS("cm/codemirror.css");
     64     this.registerRequiredCSS("cm/cmdevtools.css");
     65 
     66     this._codeMirror = window.CodeMirror(this.element, {
     67         lineNumbers: true,
     68         gutters: ["CodeMirror-linenumbers"],
     69         matchBrackets: true,
     70         smartIndent: false,
     71         styleSelectedText: true,
     72         electricChars: false,
     73         autoCloseBrackets: { explode: false }
     74     });
     75     this._codeMirror._codeMirrorTextEditor = this;
     76 
     77     CodeMirror.keyMap["devtools-common"] = {
     78         "Left": "goCharLeft",
     79         "Right": "goCharRight",
     80         "Up": "goLineUp",
     81         "Down": "goLineDown",
     82         "End": "goLineEnd",
     83         "Home": "goLineStartSmart",
     84         "PageUp": "goPageUp",
     85         "PageDown": "goPageDown",
     86         "Delete": "delCharAfter",
     87         "Backspace": "delCharBefore",
     88         "Tab": "defaultTab",
     89         "Shift-Tab": "indentLess",
     90         "Enter": "smartNewlineAndIndent",
     91         "Ctrl-Space": "autocomplete"
     92     };
     93 
     94     CodeMirror.keyMap["devtools-pc"] = {
     95         "Ctrl-A": "selectAll",
     96         "Ctrl-Z": "undoAndReveal",
     97         "Shift-Ctrl-Z": "redoAndReveal",
     98         "Ctrl-Y": "redo",
     99         "Ctrl-Home": "goDocStart",
    100         "Ctrl-Up": "goDocStart",
    101         "Ctrl-End": "goDocEnd",
    102         "Ctrl-Down": "goDocEnd",
    103         "Ctrl-Left": "goGroupLeft",
    104         "Ctrl-Right": "goGroupRight",
    105         "Alt-Left": "goLineStart",
    106         "Alt-Right": "goLineEnd",
    107         "Ctrl-Backspace": "delGroupBefore",
    108         "Ctrl-Delete": "delGroupAfter",
    109         "Ctrl-/": "toggleComment",
    110         fallthrough: "devtools-common"
    111     };
    112 
    113     CodeMirror.keyMap["devtools-mac"] = {
    114         "Cmd-A" : "selectAll",
    115         "Cmd-Z" : "undoAndReveal",
    116         "Shift-Cmd-Z": "redoAndReveal",
    117         "Cmd-Up": "goDocStart",
    118         "Cmd-Down": "goDocEnd",
    119         "Alt-Left": "goGroupLeft",
    120         "Alt-Right": "goGroupRight",
    121         "Cmd-Left": "goLineStartSmart",
    122         "Cmd-Right": "goLineEnd",
    123         "Alt-Backspace": "delGroupBefore",
    124         "Alt-Delete": "delGroupAfter",
    125         "Cmd-/": "toggleComment",
    126         fallthrough: "devtools-common"
    127     };
    128 
    129     WebInspector.settings.textEditorIndent.addChangeListener(this._updateEditorIndentation, this);
    130     this._updateEditorIndentation();
    131     WebInspector.settings.showWhitespacesInEditor.addChangeListener(this._updateCodeMirrorMode, this);
    132 
    133     this._codeMirror.setOption("keyMap", WebInspector.isMac() ? "devtools-mac" : "devtools-pc");
    134     this._codeMirror.setOption("flattenSpans", false);
    135     this._codeMirror.setOption("maxHighlightLength", 1000);
    136     this._codeMirror.setOption("mode", null);
    137 
    138     this._shouldClearHistory = true;
    139     this._lineSeparator = "\n";
    140 
    141     this._tokenHighlighter = new WebInspector.CodeMirrorTextEditor.TokenHighlighter(this._codeMirror);
    142     this._blockIndentController = new WebInspector.CodeMirrorTextEditor.BlockIndentController(this._codeMirror);
    143     this._fixWordMovement = new WebInspector.CodeMirrorTextEditor.FixWordMovement(this._codeMirror);
    144     this._autocompleteController = new WebInspector.CodeMirrorTextEditor.AutocompleteController(this, this._codeMirror);
    145 
    146     this._codeMirror.on("change", this._change.bind(this));
    147     this._codeMirror.on("beforeChange", this._beforeChange.bind(this));
    148     this._codeMirror.on("gutterClick", this._gutterClick.bind(this));
    149     this._codeMirror.on("cursorActivity", this._cursorActivity.bind(this));
    150     this._codeMirror.on("scroll", this._scroll.bind(this));
    151     this._codeMirror.on("focus", this._focus.bind(this));
    152     this._codeMirror.on("blur", this._blur.bind(this));
    153     this.element.addEventListener("contextmenu", this._contextMenu.bind(this));
    154 
    155     this.element.addStyleClass("fill");
    156     this.element.style.overflow = "hidden";
    157     this.element.firstChild.addStyleClass("source-code");
    158     this.element.firstChild.addStyleClass("fill");
    159     this._elementToWidget = new Map();
    160     this._nestedUpdatesCounter = 0;
    161 
    162     this.element.addEventListener("focus", this._handleElementFocus.bind(this), false);
    163     this.element.addEventListener("keydown", this._handleKeyDown.bind(this), true);
    164     this.element.tabIndex = 0;
    165 
    166     this._overrideModeWithPrefixedTokens("css-base", "css-");
    167     this._overrideModeWithPrefixedTokens("javascript", "js-");
    168     this._overrideModeWithPrefixedTokens("xml", "xml-");
    169 
    170     this._setupSelectionColor();
    171     this._setupWhitespaceHighlight();
    172 }
    173 
    174 WebInspector.CodeMirrorTextEditor.autocompleteCommand = function(codeMirror)
    175 {
    176     codeMirror._codeMirrorTextEditor._autocompleteController.autocomplete();
    177 }
    178 CodeMirror.commands.autocomplete = WebInspector.CodeMirrorTextEditor.autocompleteCommand;
    179 
    180 CodeMirror.commands.smartNewlineAndIndent = function(codeMirror)
    181 {
    182     codeMirror.operation(innerSmartNewlineAndIndent.bind(this, codeMirror));
    183 
    184     function countIndent(line)
    185     {
    186         for(var i = 0; i < line.length; ++i) {
    187             if (!WebInspector.TextUtils.isSpaceChar(line[i]))
    188                 return i;
    189         }
    190         return line.length;
    191     }
    192 
    193     function innerSmartNewlineAndIndent(codeMirror)
    194     {
    195         var cur = codeMirror.getCursor("start");
    196         var line = codeMirror.getLine(cur.line);
    197         var indent = cur.line > 0 ? countIndent(line) : 0;
    198         if (cur.ch <= indent) {
    199             codeMirror.replaceSelection("\n" + line.substring(0, cur.ch), "end", "+input");
    200             codeMirror.setSelection(new CodeMirror.Pos(cur.line + 1, cur.ch));
    201         } else
    202             codeMirror.execCommand("newlineAndIndent");
    203     }
    204 }
    205 
    206 CodeMirror.commands.undoAndReveal = function(codemirror)
    207 {
    208     var scrollInfo = codemirror.getScrollInfo();
    209     codemirror.execCommand("undo");
    210     var cursor = codemirror.getCursor("start");
    211     codemirror._codeMirrorTextEditor._innerRevealLine(cursor.line, scrollInfo);
    212 }
    213 
    214 CodeMirror.commands.redoAndReveal = function(codemirror)
    215 {
    216     var scrollInfo = codemirror.getScrollInfo();
    217     codemirror.execCommand("redo");
    218     var cursor = codemirror.getCursor("start");
    219     codemirror._codeMirrorTextEditor._innerRevealLine(cursor.line, scrollInfo);
    220 }
    221 
    222 WebInspector.CodeMirrorTextEditor.LongLineModeLineLengthThreshold = 2000;
    223 WebInspector.CodeMirrorTextEditor.MaximumNumberOfWhitespacesPerSingleSpan = 16;
    224 
    225 WebInspector.CodeMirrorTextEditor.prototype = {
    226     wasShown: function()
    227     {
    228         this._codeMirror.refresh();
    229     },
    230 
    231     _guessIndentationLevel: function()
    232     {
    233         var tabRegex = /^\t+/;
    234         var tabLines = 0;
    235         var indents = {};
    236         function processLine(lineHandle)
    237         {
    238             var text = lineHandle.text;
    239             if (text.length === 0 || !WebInspector.TextUtils.isSpaceChar(text[0]))
    240                 return;
    241             if (tabRegex.test(text)) {
    242                 ++tabLines;
    243                 return;
    244             }
    245             var i = 0;
    246             while (i < text.length && WebInspector.TextUtils.isSpaceChar(text[i]))
    247                 ++i;
    248             if (i % 2 !== 0)
    249                 return;
    250             indents[i] = 1 + (indents[i] || 0);
    251         }
    252         this._codeMirror.eachLine(processLine);
    253 
    254         var onePercentFilterThreshold = this.linesCount / 100;
    255         if (tabLines && tabLines > onePercentFilterThreshold)
    256             return "\t";
    257         var minimumIndent = Infinity;
    258         for (var i in indents) {
    259             if (indents[i] < onePercentFilterThreshold)
    260                 continue;
    261             var indent = parseInt(i, 10);
    262             if (minimumIndent > indent)
    263                 minimumIndent = indent;
    264         }
    265         if (minimumIndent === Infinity)
    266             return WebInspector.TextUtils.Indent.FourSpaces;
    267         return new Array(minimumIndent + 1).join(" ");
    268     },
    269 
    270     _updateEditorIndentation: function()
    271     {
    272         var extraKeys = {};
    273         var indent = WebInspector.settings.textEditorIndent.get();
    274         if (WebInspector.settings.textEditorAutoDetectIndent.get())
    275             indent = this._guessIndentationLevel();
    276         if (indent === WebInspector.TextUtils.Indent.TabCharacter) {
    277             this._codeMirror.setOption("indentWithTabs", true);
    278             this._codeMirror.setOption("indentUnit", 4);
    279         } else {
    280             this._codeMirror.setOption("indentWithTabs", false);
    281             this._codeMirror.setOption("indentUnit", indent.length);
    282             extraKeys.Tab = function(codeMirror)
    283             {
    284                 if (codeMirror.somethingSelected())
    285                     return CodeMirror.Pass;
    286                 var pos = codeMirror.getCursor("head");
    287                 codeMirror.replaceRange(indent.substring(pos.ch % indent.length), codeMirror.getCursor());
    288             }
    289         }
    290         this._codeMirror.setOption("extraKeys", extraKeys);
    291         this._indentationLevel = indent;
    292     },
    293 
    294     /**
    295      * @return {string}
    296      */
    297     indent: function()
    298     {
    299         return this._indentationLevel;
    300     },
    301 
    302     /**
    303      * @param {!RegExp} regex
    304      * @param {WebInspector.TextRange} range
    305      */
    306     highlightSearchResults: function(regex, range)
    307     {
    308         function innerHighlightRegex()
    309         {
    310             if (range) {
    311                 this.revealLine(range.startLine);
    312                 this.setSelection(WebInspector.TextRange.createFromLocation(range.startLine, range.startColumn));
    313             } else {
    314                 // Collapse selection to end on search start so that we jump to next occurence on the first enter press.
    315                 this.setSelection(this.selection().collapseToEnd());
    316             }
    317             this._tokenHighlighter.highlightSearchResults(regex, range);
    318         }
    319 
    320         this._codeMirror.operation(innerHighlightRegex.bind(this));
    321     },
    322 
    323     cancelSearchResultsHighlight: function()
    324     {
    325         this._codeMirror.operation(this._tokenHighlighter.highlightSelectedTokens.bind(this._tokenHighlighter));
    326     },
    327 
    328     undo: function()
    329     {
    330         this._codeMirror.undo();
    331     },
    332 
    333     redo: function()
    334     {
    335         this._codeMirror.redo();
    336     },
    337 
    338     _setupSelectionColor: function()
    339     {
    340         if (WebInspector.CodeMirrorTextEditor._selectionStyleInjected)
    341             return;
    342         WebInspector.CodeMirrorTextEditor._selectionStyleInjected = true;
    343         var backgroundColor = WebInspector.getSelectionBackgroundColor();
    344         var backgroundColorRule = backgroundColor ? ".CodeMirror .CodeMirror-selected { background-color: " + backgroundColor + ";}" : "";
    345         var foregroundColor = WebInspector.getSelectionForegroundColor();
    346         var foregroundColorRule = foregroundColor ? ".CodeMirror .CodeMirror-selectedtext:not(.CodeMirror-persist-highlight) { color: " + foregroundColor + "!important;}" : "";
    347         if (!foregroundColorRule && !backgroundColorRule)
    348             return;
    349 
    350         var style = document.createElement("style");
    351         style.textContent = backgroundColorRule + foregroundColorRule;
    352         document.head.appendChild(style);
    353     },
    354 
    355     _setupWhitespaceHighlight: function()
    356     {
    357         if (WebInspector.CodeMirrorTextEditor._whitespaceStyleInjected || !WebInspector.settings.showWhitespacesInEditor.get())
    358             return;
    359         WebInspector.CodeMirrorTextEditor._whitespaceStyleInjected = true;
    360         const classBase = ".cm-whitespace-";
    361         const spaceChar = "";
    362         var spaceChars = "";
    363         var rules = "";
    364         for(var i = 1; i <= WebInspector.CodeMirrorTextEditor.MaximumNumberOfWhitespacesPerSingleSpan; ++i) {
    365             spaceChars += spaceChar;
    366             var rule = classBase + i + "::before { content: '" + spaceChars + "';}\n";
    367             rules += rule;
    368         }
    369         rules += ".cm-tab:before { display: block !important; }\n";
    370         var style = document.createElement("style");
    371         style.textContent = rules;
    372         document.head.appendChild(style);
    373     },
    374 
    375     _handleKeyDown: function(e)
    376     {
    377         if (this._autocompleteController.keyDown(e))
    378             e.consume(true);
    379     },
    380 
    381     _shouldProcessWordForAutocompletion: function(word)
    382     {
    383         return word.length && (word[0] < '0' || word[0] > '9');
    384     },
    385 
    386     /**
    387      * @param {string} text
    388      */
    389     _addTextToCompletionDictionary: function(text)
    390     {
    391         var words = WebInspector.TextUtils.textToWords(text);
    392         for(var i = 0; i < words.length; ++i) {
    393             if (this._shouldProcessWordForAutocompletion(words[i]))
    394                 this._dictionary.addWord(words[i]);
    395         }
    396     },
    397 
    398     /**
    399      * @param {string} text
    400      */
    401     _removeTextFromCompletionDictionary: function(text)
    402     {
    403         var words = WebInspector.TextUtils.textToWords(text);
    404         for(var i = 0; i < words.length; ++i) {
    405             if (this._shouldProcessWordForAutocompletion(words[i]))
    406                 this._dictionary.removeWord(words[i]);
    407         }
    408     },
    409 
    410     /**
    411      * @param {WebInspector.CompletionDictionary} dictionary
    412      */
    413     setCompletionDictionary: function(dictionary)
    414     {
    415         this._dictionary = dictionary;
    416         this._addTextToCompletionDictionary(this.text());
    417     },
    418 
    419     /**
    420      * @param {number} lineNumber
    421      * @param {number} column
    422      * @return {?{x: number, y: number, height: number}}
    423      */
    424     cursorPositionToCoordinates: function(lineNumber, column)
    425     {
    426         if (lineNumber >= this._codeMirror.lineCount() || lineNumber < 0 || column < 0 || column > this._codeMirror.getLine(lineNumber).length)
    427             return null;
    428 
    429         var metrics = this._codeMirror.cursorCoords(new CodeMirror.Pos(lineNumber, column));
    430 
    431         return {
    432             x: metrics.left,
    433             y: metrics.top,
    434             height: metrics.bottom - metrics.top
    435         };
    436     },
    437 
    438     /**
    439      * @param {number} x
    440      * @param {number} y
    441      * @return {?WebInspector.TextRange}
    442      */
    443     coordinatesToCursorPosition: function(x, y)
    444     {
    445         var element = document.elementFromPoint(x, y);
    446         if (!element || !element.isSelfOrDescendant(this._codeMirror.getWrapperElement()))
    447             return null;
    448         var gutterBox = this._codeMirror.getGutterElement().boxInWindow();
    449         if (x >= gutterBox.x && x <= gutterBox.x + gutterBox.width &&
    450             y >= gutterBox.y && y <= gutterBox.y + gutterBox.height)
    451             return null;
    452         var coords = this._codeMirror.coordsChar({left: x, top: y});
    453         return this._toRange(coords, coords);
    454     },
    455 
    456     _convertTokenType: function(tokenType)
    457     {
    458         if (tokenType.startsWith("js-variable") || tokenType.startsWith("js-property") || tokenType === "js-def")
    459             return "javascript-ident";
    460         if (tokenType === "js-string-2")
    461             return "javascript-regexp";
    462         if (tokenType === "js-number" || tokenType === "js-comment" || tokenType === "js-string" || tokenType === "js-keyword")
    463             return "javascript-" + tokenType.substring("js-".length);
    464         return null;
    465     },
    466 
    467     /**
    468      * @param {number} lineNumber
    469      * @param {number} column
    470      * @return {?{startColumn: number, endColumn: number, type: string}}
    471      */
    472     tokenAtTextPosition: function(lineNumber, column)
    473     {
    474         if (lineNumber < 0 || lineNumber >= this._codeMirror.lineCount())
    475             return null;
    476         var token = this._codeMirror.getTokenAt(new CodeMirror.Pos(lineNumber, (column || 0) + 1));
    477         if (!token || !token.type)
    478             return null;
    479         var convertedType = this._convertTokenType(token.type);
    480         if (!convertedType)
    481             return null;
    482         return {
    483             startColumn: token.start,
    484             endColumn: token.end - 1,
    485             type: convertedType
    486         };
    487     },
    488 
    489     /**
    490      * @param {WebInspector.TextRange} textRange
    491      * @return {string}
    492      */
    493     copyRange: function(textRange)
    494     {
    495         var pos = this._toPos(textRange);
    496         return this._codeMirror.getRange(pos.start, pos.end);
    497     },
    498 
    499     /**
    500      * @return {boolean}
    501      */
    502     isClean: function()
    503     {
    504         return this._codeMirror.isClean();
    505     },
    506 
    507     markClean: function()
    508     {
    509         this._codeMirror.markClean();
    510     },
    511 
    512     _hasLongLines: function()
    513     {
    514         function lineIterator(lineHandle)
    515         {
    516             if (lineHandle.text.length > WebInspector.CodeMirrorTextEditor.LongLineModeLineLengthThreshold)
    517                 hasLongLines = true;
    518             return hasLongLines;
    519         }
    520         var hasLongLines = false;
    521         this._codeMirror.eachLine(lineIterator);
    522         return hasLongLines;
    523     },
    524 
    525     /**
    526      * @param {string} mimeType
    527      * @return {string}
    528      */
    529     _whitespaceOverlayMode: function(mimeType)
    530     {
    531         var modeName = CodeMirror.mimeModes[mimeType] + "+whitespaces";
    532         if (CodeMirror.modes[modeName])
    533             return modeName;
    534 
    535         function modeConstructor(config, parserConfig)
    536         {
    537             function nextToken(stream)
    538             {
    539                 if (stream.peek() === " ") {
    540                     var spaces = 0;
    541                     while (spaces < WebInspector.CodeMirrorTextEditor.MaximumNumberOfWhitespacesPerSingleSpan && stream.peek() === " ") {
    542                         ++spaces;
    543                         stream.next();
    544                     }
    545                     return "whitespace whitespace-" + spaces;
    546                 }
    547                 while (!stream.eol() && stream.peek() !== " ")
    548                     stream.next();
    549                 return null;
    550             }
    551             var whitespaceMode = {
    552                 token: nextToken
    553             };
    554             return CodeMirror.overlayMode(CodeMirror.getMode(config, mimeType), whitespaceMode, false);
    555         }
    556         CodeMirror.defineMode(modeName, modeConstructor);
    557         return modeName;
    558     },
    559 
    560     /**
    561      * @param {string} modeName
    562      * @param {string} tokenPrefix
    563      */
    564     _overrideModeWithPrefixedTokens: function(modeName, tokenPrefix)
    565     {
    566         var oldModeName = modeName + "-old";
    567         if (CodeMirror.modes[oldModeName])
    568             return;
    569 
    570         CodeMirror.defineMode(oldModeName, CodeMirror.modes[modeName]);
    571         CodeMirror.defineMode(modeName, modeConstructor);
    572 
    573         function modeConstructor(config, parserConfig)
    574         {
    575             var innerConfig = {};
    576             for (var i in parserConfig)
    577                 innerConfig[i] = parserConfig[i];
    578             innerConfig.name = oldModeName;
    579             var codeMirrorMode = CodeMirror.getMode(config, innerConfig);
    580             codeMirrorMode.name = modeName;
    581             codeMirrorMode.token = tokenOverride.bind(this, codeMirrorMode.token);
    582             return codeMirrorMode;
    583         }
    584 
    585         function tokenOverride(superToken, stream, state)
    586         {
    587             var token = superToken(stream, state);
    588             return token ? tokenPrefix + token : token;
    589         }
    590     },
    591 
    592     _enableLongLinesMode: function()
    593     {
    594         this._codeMirror.setOption("styleSelectedText", false);
    595         this._longLinesMode = true;
    596     },
    597 
    598     _disableLongLinesMode: function()
    599     {
    600         this._codeMirror.setOption("styleSelectedText", true);
    601         this._longLinesMode = false;
    602     },
    603 
    604     _updateCodeMirrorMode: function()
    605     {
    606         var showWhitespaces = WebInspector.settings.showWhitespacesInEditor.get();
    607         this._codeMirror.setOption("mode", showWhitespaces ? this._whitespaceOverlayMode(this._mimeType) : this._mimeType);
    608     },
    609 
    610     /**
    611      * @param {string} mimeType
    612      */
    613     setMimeType: function(mimeType)
    614     {
    615         this._mimeType = mimeType;
    616         if (this._hasLongLines())
    617             this._enableLongLinesMode();
    618         else
    619             this._disableLongLinesMode();
    620         this._updateCodeMirrorMode();
    621     },
    622 
    623     /**
    624      * @param {boolean} readOnly
    625      */
    626     setReadOnly: function(readOnly)
    627     {
    628         this.element.enableStyleClass("CodeMirror-readonly", readOnly)
    629         this._codeMirror.setOption("readOnly", readOnly);
    630     },
    631 
    632     /**
    633      * @return {boolean}
    634      */
    635     readOnly: function()
    636     {
    637         return !!this._codeMirror.getOption("readOnly");
    638     },
    639 
    640     /**
    641      * @param {Object} highlightDescriptor
    642      */
    643     removeHighlight: function(highlightDescriptor)
    644     {
    645         highlightDescriptor.clear();
    646     },
    647 
    648     /**
    649      * @param {WebInspector.TextRange} range
    650      * @param {string} cssClass
    651      * @return {Object}
    652      */
    653     highlightRange: function(range, cssClass)
    654     {
    655         cssClass = "CodeMirror-persist-highlight " + cssClass;
    656         var pos = this._toPos(range);
    657         ++pos.end.ch;
    658         return this._codeMirror.markText(pos.start, pos.end, {
    659             className: cssClass,
    660             startStyle: cssClass + "-start",
    661             endStyle: cssClass + "-end"
    662         });
    663     },
    664 
    665     /**
    666      * @param {string} regex
    667      * @param {string} cssClass
    668      * @return {Object}
    669      */
    670     highlightRegex: function(regex, cssClass) { },
    671 
    672     /**
    673      * @return {Element}
    674      */
    675     defaultFocusedElement: function()
    676     {
    677         return this.element;
    678     },
    679 
    680     focus: function()
    681     {
    682         this._codeMirror.focus();
    683     },
    684 
    685     _handleElementFocus: function()
    686     {
    687         this._codeMirror.focus();
    688     },
    689 
    690     beginUpdates: function()
    691     {
    692         ++this._nestedUpdatesCounter;
    693     },
    694 
    695     endUpdates: function()
    696     {
    697         if (!--this._nestedUpdatesCounter)
    698             this._codeMirror.refresh();
    699     },
    700 
    701     /**
    702      * @param {number} lineNumber
    703      */
    704     revealLine: function(lineNumber)
    705     {
    706         this._innerRevealLine(lineNumber, this._codeMirror.getScrollInfo());
    707     },
    708 
    709     /**
    710      * @param {number} lineNumber
    711      * @param {{left: number, top: number, width: number, height: number, clientWidth: number, clientHeight: number}} scrollInfo
    712      */
    713     _innerRevealLine: function(lineNumber, scrollInfo)
    714     {
    715         var topLine = this._codeMirror.lineAtHeight(scrollInfo.top, "local");
    716         var bottomLine = this._codeMirror.lineAtHeight(scrollInfo.top + scrollInfo.clientHeight, "local");
    717         var linesPerScreen = bottomLine - topLine + 1;
    718         if (lineNumber < topLine) {
    719             var topLineToReveal = Math.max(lineNumber - (linesPerScreen / 2) + 1, 0) | 0;
    720             this._codeMirror.scrollIntoView(new CodeMirror.Pos(topLineToReveal, 0));
    721         } else if (lineNumber > bottomLine) {
    722             var bottomLineToReveal = Math.min(lineNumber + (linesPerScreen / 2) - 1, this.linesCount - 1) | 0;
    723             this._codeMirror.scrollIntoView(new CodeMirror.Pos(bottomLineToReveal, 0));
    724         }
    725     },
    726 
    727     _gutterClick: function(instance, lineNumber, gutter, event)
    728     {
    729         this.dispatchEventToListeners(WebInspector.TextEditor.Events.GutterClick, { lineNumber: lineNumber, event: event });
    730     },
    731 
    732     _contextMenu: function(event)
    733     {
    734         var contextMenu = new WebInspector.ContextMenu(event);
    735         var target = event.target.enclosingNodeOrSelfWithClass("CodeMirror-gutter-elt");
    736         if (target)
    737             this._delegate.populateLineGutterContextMenu(contextMenu, parseInt(target.textContent, 10) - 1);
    738         else
    739             this._delegate.populateTextAreaContextMenu(contextMenu, 0);
    740         contextMenu.show();
    741     },
    742 
    743     /**
    744      * @param {number} lineNumber
    745      * @param {boolean} disabled
    746      * @param {boolean} conditional
    747      */
    748     addBreakpoint: function(lineNumber, disabled, conditional)
    749     {
    750         if (lineNumber < 0 || lineNumber >= this._codeMirror.lineCount())
    751             return;
    752         var className = "cm-breakpoint" + (conditional ? " cm-breakpoint-conditional" : "") + (disabled ? " cm-breakpoint-disabled" : "");
    753         this._codeMirror.addLineClass(lineNumber, "wrap", className);
    754     },
    755 
    756     /**
    757      * @param {number} lineNumber
    758      */
    759     removeBreakpoint: function(lineNumber)
    760     {
    761         if (lineNumber < 0 || lineNumber >= this._codeMirror.lineCount())
    762             return;
    763         var wrapClasses = this._codeMirror.getLineHandle(lineNumber).wrapClass;
    764         if (!wrapClasses)
    765             return;
    766         var classes = wrapClasses.split(" ");
    767         for(var i = 0; i < classes.length; ++i) {
    768             if (classes[i].startsWith("cm-breakpoint"))
    769                 this._codeMirror.removeLineClass(lineNumber, "wrap", classes[i]);
    770         }
    771     },
    772 
    773     /**
    774      * @param {number} lineNumber
    775      */
    776     setExecutionLine: function(lineNumber)
    777     {
    778         this._executionLine = this._codeMirror.getLineHandle(lineNumber);
    779         this._codeMirror.addLineClass(this._executionLine, "wrap", "cm-execution-line");
    780     },
    781 
    782     clearExecutionLine: function()
    783     {
    784         if (this._executionLine)
    785             this._codeMirror.removeLineClass(this._executionLine, "wrap", "cm-execution-line");
    786         delete this._executionLine;
    787     },
    788 
    789     /**
    790      * @param {number} lineNumber
    791      * @param {Element} element
    792      */
    793     addDecoration: function(lineNumber, element)
    794     {
    795         var widget = this._codeMirror.addLineWidget(lineNumber, element);
    796         this._elementToWidget.put(element, widget);
    797     },
    798 
    799     /**
    800      * @param {number} lineNumber
    801      * @param {Element} element
    802      */
    803     removeDecoration: function(lineNumber, element)
    804     {
    805         var widget = this._elementToWidget.remove(element);
    806         if (widget)
    807             this._codeMirror.removeLineWidget(widget);
    808     },
    809 
    810     /**
    811      * @param {number} lineNumber
    812      * @param {number=} columnNumber
    813      */
    814     highlightPosition: function(lineNumber, columnNumber)
    815     {
    816         if (lineNumber < 0)
    817             return;
    818         lineNumber = Math.min(lineNumber, this._codeMirror.lineCount() - 1);
    819         if (typeof columnNumber !== "number" || columnNumber < 0 || columnNumber > this._codeMirror.getLine(lineNumber).length)
    820             columnNumber = 0;
    821 
    822         this.clearPositionHighlight();
    823         this._highlightedLine = this._codeMirror.getLineHandle(lineNumber);
    824         if (!this._highlightedLine)
    825           return;
    826         this.revealLine(lineNumber);
    827         this._codeMirror.addLineClass(this._highlightedLine, null, "cm-highlight");
    828         this._clearHighlightTimeout = setTimeout(this.clearPositionHighlight.bind(this), 2000);
    829         if (!this.readOnly())
    830             this._codeMirror.setSelection(new CodeMirror.Pos(lineNumber, columnNumber));
    831     },
    832 
    833     clearPositionHighlight: function()
    834     {
    835         if (this._clearHighlightTimeout)
    836             clearTimeout(this._clearHighlightTimeout);
    837         delete this._clearHighlightTimeout;
    838 
    839          if (this._highlightedLine)
    840             this._codeMirror.removeLineClass(this._highlightedLine, null, "cm-highlight");
    841         delete this._highlightedLine;
    842     },
    843 
    844     /**
    845      * @return {Array.<Element>}
    846      */
    847     elementsToRestoreScrollPositionsFor: function()
    848     {
    849         return [];
    850     },
    851 
    852     /**
    853      * @param {WebInspector.TextEditor} textEditor
    854      */
    855     inheritScrollPositions: function(textEditor)
    856     {
    857     },
    858 
    859     _updatePaddingBottom: function(width, height)
    860     {
    861         var scrollInfo = this._codeMirror.getScrollInfo();
    862         var newPaddingBottom;
    863         var linesElement = this.element.firstChild.querySelector(".CodeMirror-lines");
    864         var lineCount = this._codeMirror.lineCount();
    865         if (lineCount <= 1)
    866             newPaddingBottom = 0;
    867         else
    868             newPaddingBottom = Math.max(scrollInfo.clientHeight - this._codeMirror.getLineHandle(this._codeMirror.lastLine()).height, 0);
    869         newPaddingBottom += "px";
    870         linesElement.style.paddingBottom = newPaddingBottom;
    871         this._codeMirror.setSize(width, height);
    872     },
    873 
    874     _resizeEditor: function()
    875     {
    876         var scrollInfo = this._codeMirror.getScrollInfo();
    877         var width = this.element.parentElement.offsetWidth;
    878         var height = this.element.parentElement.offsetHeight;
    879         this._codeMirror.setSize(width, height);
    880         this._updatePaddingBottom(width, height);
    881         this._codeMirror.scrollTo(scrollInfo.left, scrollInfo.top);
    882     },
    883 
    884     onResize: function()
    885     {
    886         if (WebInspector.experimentsSettings.scrollBeyondEndOfFile.isEnabled())
    887             this._resizeEditor();
    888         else {
    889             var width = this.element.parentElement.offsetWidth;
    890             var height = this.element.parentElement.offsetHeight;
    891             this._codeMirror.setSize(width, height);
    892         }
    893     },
    894 
    895     /**
    896      * @param {WebInspector.TextRange} range
    897      * @param {string} text
    898      * @return {WebInspector.TextRange}
    899      */
    900     editRange: function(range, text)
    901     {
    902         var pos = this._toPos(range);
    903         this._codeMirror.replaceRange(text, pos.start, pos.end);
    904         var newRange = this._toRange(pos.start, this._codeMirror.posFromIndex(this._codeMirror.indexFromPos(pos.start) + text.length));
    905         this._delegate.onTextChanged(range, newRange);
    906         if (WebInspector.settings.textEditorAutoDetectIndent.get())
    907             this._updateEditorIndentation();
    908         return newRange;
    909     },
    910 
    911     /**
    912      * @param {number} lineNumber
    913      * @param {number} column
    914      * @param {boolean=} prefixOnly
    915      * @return {?WebInspector.TextRange}
    916      */
    917     _wordRangeForCursorPosition: function(lineNumber, column, prefixOnly)
    918     {
    919         var line = this.line(lineNumber);
    920         if (column === 0 || !WebInspector.TextUtils.isWordChar(line.charAt(column - 1)))
    921             return null;
    922         var wordStart = column - 1;
    923         while(wordStart > 0 && WebInspector.TextUtils.isWordChar(line.charAt(wordStart - 1)))
    924             --wordStart;
    925         if (prefixOnly)
    926             return new WebInspector.TextRange(lineNumber, wordStart, lineNumber, column);
    927         var wordEnd = column;
    928         while(wordEnd < line.length && WebInspector.TextUtils.isWordChar(line.charAt(wordEnd)))
    929             ++wordEnd;
    930         return new WebInspector.TextRange(lineNumber, wordStart, lineNumber, wordEnd);
    931     },
    932 
    933     _beforeChange: function(codeMirror, changeObject)
    934     {
    935         if (!this._dictionary)
    936             return;
    937         this._updatedLines = this._updatedLines || {};
    938         for(var i = changeObject.from.line; i <= changeObject.to.line; ++i)
    939             this._updatedLines[i] = this.line(i);
    940     },
    941 
    942     /**
    943      * @param {CodeMirror} codeMirror
    944      * @param {{origin: string, text: Array.<string>, removed: Array.<string>}} changeObject
    945      */
    946     _change: function(codeMirror, changeObject)
    947     {
    948         if (WebInspector.experimentsSettings.scrollBeyondEndOfFile.isEnabled()) {
    949             var hasOneLine = this._codeMirror.lineCount() === 1;
    950             if (hasOneLine !== this._hasOneLine)
    951                 this._resizeEditor();
    952             this._hasOneLine = hasOneLine;
    953         }
    954         var widgets = this._elementToWidget.values();
    955         for (var i = 0; i < widgets.length; ++i)
    956             this._codeMirror.removeLineWidget(widgets[i]);
    957         this._elementToWidget.clear();
    958 
    959         if (this._updatedLines) {
    960             for(var lineNumber in this._updatedLines)
    961                 this._removeTextFromCompletionDictionary(this._updatedLines[lineNumber]);
    962             delete this._updatedLines;
    963         }
    964 
    965         var linesToUpdate = {};
    966         var singleCharInput = false;
    967         do {
    968             var oldRange = this._toRange(changeObject.from, changeObject.to);
    969             var newRange = oldRange.clone();
    970             var linesAdded = changeObject.text.length;
    971             singleCharInput = (changeObject.origin === "+input" && changeObject.text.length === 1 && changeObject.text[0].length === 1) ||
    972                 (changeObject.origin === "+delete" && changeObject.removed.length === 1 && changeObject.removed[0].length === 1);
    973             if (linesAdded === 0) {
    974                 newRange.endLine = newRange.startLine;
    975                 newRange.endColumn = newRange.startColumn;
    976             } else if (linesAdded === 1) {
    977                 newRange.endLine = newRange.startLine;
    978                 newRange.endColumn = newRange.startColumn + changeObject.text[0].length;
    979             } else {
    980                 newRange.endLine = newRange.startLine + linesAdded - 1;
    981                 newRange.endColumn = changeObject.text[linesAdded - 1].length;
    982             }
    983 
    984             if (!this._muteTextChangedEvent)
    985                 this._delegate.onTextChanged(oldRange, newRange);
    986 
    987             for(var i = newRange.startLine; i <= newRange.endLine; ++i) {
    988                 linesToUpdate[i] = true;
    989             }
    990             if (this._dictionary) {
    991                 for(var i = newRange.startLine; i <= newRange.endLine; ++i)
    992                     linesToUpdate[i] = this.line(i);
    993             }
    994         } while (changeObject = changeObject.next);
    995         if (this._dictionary) {
    996             for(var lineNumber in linesToUpdate)
    997                 this._addTextToCompletionDictionary(linesToUpdate[lineNumber]);
    998         }
    999         if (singleCharInput)
   1000             this._autocompleteController.autocomplete();
   1001     },
   1002 
   1003     _cursorActivity: function()
   1004     {
   1005         var start = this._codeMirror.getCursor("anchor");
   1006         var end = this._codeMirror.getCursor("head");
   1007         this._delegate.selectionChanged(this._toRange(start, end));
   1008         if (!this._tokenHighlighter.highlightedRegex())
   1009             this._codeMirror.operation(this._tokenHighlighter.highlightSelectedTokens.bind(this._tokenHighlighter));
   1010     },
   1011 
   1012     _scroll: function()
   1013     {
   1014         if (this._scrollTimer)
   1015             clearTimeout(this._scrollTimer);
   1016         var topmostLineNumber = this._codeMirror.lineAtHeight(this._codeMirror.getScrollInfo().top, "local");
   1017         this._scrollTimer = setTimeout(this._delegate.scrollChanged.bind(this._delegate, topmostLineNumber), 100);
   1018     },
   1019 
   1020     _focus: function()
   1021     {
   1022         this._delegate.editorFocused();
   1023     },
   1024 
   1025     _blur: function()
   1026     {
   1027         this._autocompleteController.finishAutocomplete();
   1028     },
   1029 
   1030     /**
   1031      * @param {number} lineNumber
   1032      */
   1033     scrollToLine: function(lineNumber)
   1034     {
   1035         var pos = new CodeMirror.Pos(lineNumber, 0);
   1036         var coords = this._codeMirror.charCoords(pos, "local");
   1037         this._codeMirror.scrollTo(0, coords.top);
   1038     },
   1039 
   1040     /**
   1041      * @return {number}
   1042      */
   1043     firstVisibleLine: function()
   1044     {
   1045         return this._codeMirror.lineAtHeight(this._codeMirror.getScrollInfo().top, "local");
   1046     },
   1047 
   1048     /**
   1049      * @return {number}
   1050      */
   1051     lastVisibleLine: function()
   1052     {
   1053         var scrollInfo = this._codeMirror.getScrollInfo();
   1054         return this._codeMirror.lineAtHeight(scrollInfo.top + scrollInfo.clientHeight, "local");
   1055     },
   1056 
   1057     /**
   1058      * @return {WebInspector.TextRange}
   1059      */
   1060     selection: function()
   1061     {
   1062         var start = this._codeMirror.getCursor("anchor");
   1063         var end = this._codeMirror.getCursor("head");
   1064 
   1065         return this._toRange(start, end);
   1066     },
   1067 
   1068     /**
   1069      * @return {WebInspector.TextRange?}
   1070      */
   1071     lastSelection: function()
   1072     {
   1073         return this._lastSelection;
   1074     },
   1075 
   1076     /**
   1077      * @param {WebInspector.TextRange} textRange
   1078      */
   1079     setSelection: function(textRange)
   1080     {
   1081         this._lastSelection = textRange;
   1082         var pos = this._toPos(textRange);
   1083         this._codeMirror.setSelection(pos.start, pos.end);
   1084     },
   1085 
   1086     /**
   1087      * @param {string} text
   1088      */
   1089     _detectLineSeparator: function(text)
   1090     {
   1091         this._lineSeparator = text.indexOf("\r\n") >= 0 ? "\r\n" : "\n";
   1092     },
   1093 
   1094     /**
   1095      * @param {string} text
   1096      */
   1097     setText: function(text)
   1098     {
   1099         this._muteTextChangedEvent = true;
   1100         this._codeMirror.setValue(text);
   1101         this._updateEditorIndentation();
   1102         if (this._shouldClearHistory) {
   1103             this._codeMirror.clearHistory();
   1104             this._shouldClearHistory = false;
   1105         }
   1106         this._detectLineSeparator(text);
   1107         delete this._muteTextChangedEvent;
   1108     },
   1109 
   1110     /**
   1111      * @return {string}
   1112      */
   1113     text: function()
   1114     {
   1115         return this._codeMirror.getValue().replace(/\n/g, this._lineSeparator);
   1116     },
   1117 
   1118     /**
   1119      * @return {WebInspector.TextRange}
   1120      */
   1121     range: function()
   1122     {
   1123         var lineCount = this.linesCount;
   1124         var lastLine = this._codeMirror.getLine(lineCount - 1);
   1125         return this._toRange(new CodeMirror.Pos(0, 0), new CodeMirror.Pos(lineCount - 1, lastLine.length));
   1126     },
   1127 
   1128     /**
   1129      * @param {number} lineNumber
   1130      * @return {string}
   1131      */
   1132     line: function(lineNumber)
   1133     {
   1134         return this._codeMirror.getLine(lineNumber);
   1135     },
   1136 
   1137     /**
   1138      * @return {number}
   1139      */
   1140     get linesCount()
   1141     {
   1142         return this._codeMirror.lineCount();
   1143     },
   1144 
   1145     /**
   1146      * @param {number} line
   1147      * @param {string} name
   1148      * @param {Object?} value
   1149      */
   1150     setAttribute: function(line, name, value)
   1151     {
   1152         if (line < 0 || line >= this._codeMirror.lineCount())
   1153             return;
   1154         var handle = this._codeMirror.getLineHandle(line);
   1155         if (handle.attributes === undefined) handle.attributes = {};
   1156         handle.attributes[name] = value;
   1157     },
   1158 
   1159     /**
   1160      * @param {number} line
   1161      * @param {string} name
   1162      * @return {?Object} value
   1163      */
   1164     getAttribute: function(line, name)
   1165     {
   1166         if (line < 0 || line >= this._codeMirror.lineCount())
   1167             return null;
   1168         var handle = this._codeMirror.getLineHandle(line);
   1169         return handle.attributes && handle.attributes[name] !== undefined ? handle.attributes[name] : null;
   1170     },
   1171 
   1172     /**
   1173      * @param {number} line
   1174      * @param {string} name
   1175      */
   1176     removeAttribute: function(line, name)
   1177     {
   1178         if (line < 0 || line >= this._codeMirror.lineCount())
   1179             return;
   1180         var handle = this._codeMirror.getLineHandle(line);
   1181         if (handle && handle.attributes)
   1182             delete handle.attributes[name];
   1183     },
   1184 
   1185     /**
   1186      * @param {WebInspector.TextRange} range
   1187      * @return {{start: CodeMirror.Pos, end: CodeMirror.Pos}}
   1188      */
   1189     _toPos: function(range)
   1190     {
   1191         return {
   1192             start: new CodeMirror.Pos(range.startLine, range.startColumn),
   1193             end: new CodeMirror.Pos(range.endLine, range.endColumn)
   1194         }
   1195     },
   1196 
   1197     _toRange: function(start, end)
   1198     {
   1199         return new WebInspector.TextRange(start.line, start.ch, end.line, end.ch);
   1200     },
   1201 
   1202     __proto__: WebInspector.View.prototype
   1203 }
   1204 
   1205 /**
   1206  * @constructor
   1207  * @param {CodeMirror} codeMirror
   1208  */
   1209 WebInspector.CodeMirrorTextEditor.TokenHighlighter = function(codeMirror)
   1210 {
   1211     this._codeMirror = codeMirror;
   1212 }
   1213 
   1214 WebInspector.CodeMirrorTextEditor.TokenHighlighter.prototype = {
   1215     /**
   1216      * @param {RegExp} regex
   1217      * @param {WebInspector.TextRange} range
   1218      */
   1219     highlightSearchResults: function(regex, range)
   1220     {
   1221         var oldRegex = this._highlightRegex;
   1222         this._highlightRegex = regex;
   1223         this._highlightRange = range;
   1224         if (this._searchResultMarker) {
   1225             this._searchResultMarker.clear();
   1226             delete this._searchResultMarker;
   1227         }
   1228         if (this._highlightDescriptor && this._highlightDescriptor.selectionStart)
   1229             this._codeMirror.removeLineClass(this._highlightDescriptor.selectionStart.line, "wrap", "cm-line-with-selection");
   1230         var selectionStart = this._highlightRange ? new CodeMirror.Pos(this._highlightRange.startLine, this._highlightRange.startColumn) : null;
   1231         if (selectionStart)
   1232             this._codeMirror.addLineClass(selectionStart.line, "wrap", "cm-line-with-selection");
   1233         if (this._highlightRegex === oldRegex) {
   1234             // Do not re-add overlay mode if regex did not change for better performance.
   1235             if (this._highlightDescriptor)
   1236                 this._highlightDescriptor.selectionStart = selectionStart;
   1237         } else {
   1238             this._removeHighlight();
   1239             this._setHighlighter(this._searchHighlighter.bind(this, this._highlightRegex, this._highlightRange), selectionStart);
   1240         }
   1241         if (selectionStart) {
   1242             var pos = WebInspector.CodeMirrorTextEditor.prototype._toPos(this._highlightRange);
   1243             this._searchResultMarker = this._codeMirror.markText(pos.start, pos.end, {className: "cm-column-with-selection"});
   1244         }
   1245     },
   1246 
   1247     highlightedRegex: function()
   1248     {
   1249         return this._highlightRegex;
   1250     },
   1251 
   1252     highlightSelectedTokens: function()
   1253     {
   1254         delete this._highlightRegex;
   1255         delete this._highlightRange;
   1256 
   1257         if (this._highlightDescriptor && this._highlightDescriptor.selectionStart)
   1258             this._codeMirror.removeLineClass(this._highlightDescriptor.selectionStart.line, "wrap", "cm-line-with-selection");
   1259         this._removeHighlight();
   1260         var selectionStart = this._codeMirror.getCursor("start");
   1261         var selectionEnd = this._codeMirror.getCursor("end");
   1262         if (selectionStart.line !== selectionEnd.line)
   1263             return;
   1264         if (selectionStart.ch === selectionEnd.ch)
   1265             return;
   1266 
   1267         var selectedText = this._codeMirror.getSelection();
   1268         if (this._isWord(selectedText, selectionStart.line, selectionStart.ch, selectionEnd.ch)) {
   1269             if (selectionStart)
   1270                 this._codeMirror.addLineClass(selectionStart.line, "wrap", "cm-line-with-selection")
   1271             this._setHighlighter(this._tokenHighlighter.bind(this, selectedText, selectionStart), selectionStart);
   1272         }
   1273     },
   1274 
   1275     /**
   1276      * @param {string} selectedText
   1277      * @param {number} lineNumber
   1278      * @param {number} startColumn
   1279      * @param {number} endColumn
   1280      */
   1281     _isWord: function(selectedText, lineNumber, startColumn, endColumn)
   1282     {
   1283         var line = this._codeMirror.getLine(lineNumber);
   1284         var leftBound = startColumn === 0 || !WebInspector.TextUtils.isWordChar(line.charAt(startColumn - 1));
   1285         var rightBound = endColumn === line.length || !WebInspector.TextUtils.isWordChar(line.charAt(endColumn));
   1286         return leftBound && rightBound && WebInspector.TextUtils.isWord(selectedText);
   1287     },
   1288 
   1289     _removeHighlight: function()
   1290     {
   1291         if (this._highlightDescriptor) {
   1292             this._codeMirror.removeOverlay(this._highlightDescriptor.overlay);
   1293             delete this._highlightDescriptor;
   1294         }
   1295     },
   1296 
   1297     /**
   1298      * @param {RegExp} regex
   1299      * @param {WebInspector.TextRange} range
   1300      * @param {CodeMirror.StringStream} stream
   1301      */
   1302     _searchHighlighter: function(regex, range, stream)
   1303     {
   1304         if (stream.column() === 0)
   1305             delete this._searchMatchLength;
   1306         if (this._searchMatchLength) {
   1307             if (this._searchMatchLength > 1) {
   1308                 for (var i = 0; i < this._searchMatchLength - 2; ++i)
   1309                     stream.next();
   1310                 this._searchMatchLength = 1;
   1311                 return "search-highlight";
   1312             } else {
   1313                 stream.next();
   1314                 delete this._searchMatchLength;
   1315                 return "search-highlight search-highlight-end";
   1316             }
   1317         }
   1318         var match = stream.match(regex, false);
   1319         if (match) {
   1320             stream.next();
   1321             var matchLength = match[0].length;
   1322             if (matchLength === 1)
   1323                 return "search-highlight search-highlight-full";
   1324             this._searchMatchLength = matchLength;
   1325             return "search-highlight search-highlight-start";
   1326         }
   1327 
   1328         while (!stream.match(regex, false) && stream.next()) {};
   1329     },
   1330 
   1331     /**
   1332      * @param {string} token
   1333      * @param {CodeMirror.Pos} selectionStart
   1334      * @param {CodeMirror.StringStream} stream
   1335      */
   1336     _tokenHighlighter: function(token, selectionStart, stream)
   1337     {
   1338         var tokenFirstChar = token.charAt(0);
   1339         if (stream.match(token) && (stream.eol() || !WebInspector.TextUtils.isWordChar(stream.peek())))
   1340             return stream.column() === selectionStart.ch ? "token-highlight column-with-selection" : "token-highlight";
   1341 
   1342         var eatenChar;
   1343         do {
   1344             eatenChar = stream.next();
   1345         } while (eatenChar && (WebInspector.TextUtils.isWordChar(eatenChar) || stream.peek() !== tokenFirstChar));
   1346     },
   1347 
   1348     /**
   1349      * @param {function(CodeMirror.StringStream)} highlighter
   1350      */
   1351     _setHighlighter: function(highlighter, selectionStart)
   1352     {
   1353         var overlayMode = {
   1354             token: highlighter
   1355         };
   1356         this._codeMirror.addOverlay(overlayMode);
   1357         this._highlightDescriptor = {
   1358             overlay: overlayMode,
   1359             selectionStart: selectionStart
   1360         };
   1361     }
   1362 }
   1363 
   1364 /**
   1365  * @constructor
   1366  * @param {CodeMirror} codeMirror
   1367  */
   1368 WebInspector.CodeMirrorTextEditor.BlockIndentController = function(codeMirror)
   1369 {
   1370     codeMirror.addKeyMap(this);
   1371 }
   1372 
   1373 WebInspector.CodeMirrorTextEditor.BlockIndentController.prototype = {
   1374     name: "blockIndentKeymap",
   1375 
   1376     Enter: function(codeMirror)
   1377     {
   1378         if (codeMirror.somethingSelected())
   1379             return CodeMirror.Pass;
   1380         var cursor = codeMirror.getCursor();
   1381         if (cursor.ch === 0)
   1382             return CodeMirror.Pass;
   1383         var line = codeMirror.getLine(cursor.line);
   1384         if (line.substr(cursor.ch - 1, 2) === "{}") {
   1385             codeMirror.execCommand("newlineAndIndent");
   1386             codeMirror.setCursor(cursor);
   1387             codeMirror.execCommand("newlineAndIndent");
   1388             codeMirror.execCommand("indentMore");
   1389         } else if (line.substr(cursor.ch - 1, 1) === "{") {
   1390             codeMirror.execCommand("newlineAndIndent");
   1391             codeMirror.execCommand("indentMore");
   1392         } else
   1393             return CodeMirror.Pass;
   1394     },
   1395 
   1396     "'}'": function(codeMirror)
   1397     {
   1398         var cursor = codeMirror.getCursor();
   1399         var line = codeMirror.getLine(cursor.line);
   1400         for(var i = 0 ; i < line.length; ++i)
   1401             if (!WebInspector.TextUtils.isSpaceChar(line.charAt(i)))
   1402                 return CodeMirror.Pass;
   1403 
   1404         codeMirror.replaceRange("}", cursor);
   1405         var matchingBracket = codeMirror.findMatchingBracket();
   1406         if (!matchingBracket || !matchingBracket.match)
   1407             return;
   1408 
   1409         line = codeMirror.getLine(matchingBracket.to.line);
   1410         var desiredIndentation = 0;
   1411         while (desiredIndentation < line.length && WebInspector.TextUtils.isSpaceChar(line.charAt(desiredIndentation)))
   1412             ++desiredIndentation;
   1413 
   1414         codeMirror.replaceRange(line.substr(0, desiredIndentation) + "}", new CodeMirror.Pos(cursor.line, 0), new CodeMirror.Pos(cursor.line, cursor.ch + 1));
   1415     }
   1416 }
   1417 
   1418 /**
   1419  * @constructor
   1420  * @param {CodeMirror} codeMirror
   1421  */
   1422 WebInspector.CodeMirrorTextEditor.FixWordMovement = function(codeMirror)
   1423 {
   1424     function moveLeft(shift, codeMirror)
   1425     {
   1426         var cursor = codeMirror.getCursor("head");
   1427         if (cursor.ch !== 0 || cursor.line === 0)
   1428             return CodeMirror.Pass;
   1429         codeMirror.setExtending(shift);
   1430         codeMirror.execCommand("goLineUp");
   1431         codeMirror.execCommand("goLineEnd")
   1432         codeMirror.setExtending(false);
   1433     }
   1434     function moveRight(shift, codeMirror)
   1435     {
   1436         var cursor = codeMirror.getCursor("head");
   1437         var line = codeMirror.getLine(cursor.line);
   1438         if (cursor.ch !== line.length || cursor.line + 1 === codeMirror.lineCount())
   1439             return CodeMirror.Pass;
   1440         codeMirror.setExtending(shift);
   1441         codeMirror.execCommand("goLineDown");
   1442         codeMirror.execCommand("goLineStart");
   1443         codeMirror.setExtending(false);
   1444     }
   1445     function delWordBack(codeMirror)
   1446     {
   1447         if (codeMirror.somethingSelected())
   1448             return CodeMirror.Pass;
   1449         var cursor = codeMirror.getCursor("head");
   1450         if (cursor.ch === 0)
   1451             codeMirror.execCommand("delCharBefore");
   1452         else
   1453             return CodeMirror.Pass;
   1454     }
   1455 
   1456     var modifierKey = WebInspector.isMac() ? "Alt" : "Ctrl";
   1457     var leftKey = modifierKey + "-Left";
   1458     var rightKey = modifierKey + "-Right";
   1459     var keyMap = {};
   1460     keyMap[leftKey] = moveLeft.bind(this, false);
   1461     keyMap[rightKey] = moveRight.bind(this, false);
   1462     keyMap["Shift-" + leftKey] = moveLeft.bind(this, true);
   1463     keyMap["Shift-" + rightKey] = moveRight.bind(this, true);
   1464     keyMap[modifierKey + "-Backspace"] = delWordBack.bind(this);
   1465     codeMirror.addKeyMap(keyMap);
   1466 }
   1467 
   1468 /**
   1469  * @constructor
   1470  * @implements {WebInspector.SuggestBoxDelegate}
   1471  * @param {WebInspector.CodeMirrorTextEditor} textEditor
   1472  * @param {CodeMirror} codeMirror
   1473  */
   1474 WebInspector.CodeMirrorTextEditor.AutocompleteController = function(textEditor, codeMirror)
   1475 {
   1476     this._textEditor = textEditor;
   1477     this._codeMirror = codeMirror;
   1478     this._codeMirror.on("scroll", this._onScroll.bind(this));
   1479     this._codeMirror.on("cursorActivity", this._onCursorActivity.bind(this));
   1480 }
   1481 
   1482 WebInspector.CodeMirrorTextEditor.AutocompleteController.prototype = {
   1483     autocomplete: function()
   1484     {
   1485         var dictionary = this._textEditor._dictionary;
   1486         if (!dictionary || this._codeMirror.somethingSelected()) {
   1487             this.finishAutocomplete();
   1488             return;
   1489         }
   1490 
   1491         var cursor = this._codeMirror.getCursor();
   1492         var substituteRange = this._textEditor._wordRangeForCursorPosition(cursor.line, cursor.ch, false);
   1493         if (!substituteRange || substituteRange.startColumn === cursor.ch) {
   1494             this.finishAutocomplete();
   1495             return;
   1496         }
   1497         var prefixRange = substituteRange.clone();
   1498         prefixRange.endColumn = cursor.ch;
   1499 
   1500         var substituteWord = this._textEditor.copyRange(substituteRange);
   1501         var hasPrefixInDictionary = dictionary.hasWord(substituteWord);
   1502         if (hasPrefixInDictionary)
   1503             dictionary.removeWord(substituteWord);
   1504         var wordsWithPrefix = dictionary.wordsWithPrefix(this._textEditor.copyRange(prefixRange));
   1505         if (hasPrefixInDictionary)
   1506             dictionary.addWord(substituteWord);
   1507 
   1508         function sortSuggestions(a, b)
   1509         {
   1510             return dictionary.wordCount(b) - dictionary.wordCount(a) || a.length - b.length;
   1511         }
   1512 
   1513         wordsWithPrefix.sort(sortSuggestions);
   1514 
   1515         if (!this._suggestBox) {
   1516             this._suggestBox = new WebInspector.SuggestBox(this, this._textEditor.element, "generic-suggest", 6);
   1517             this._anchorBox = this._anchorBoxForPosition(cursor.line, cursor.ch);
   1518         }
   1519         this._suggestBox.updateSuggestions(this._anchorBox, wordsWithPrefix, 0, true, this._textEditor.copyRange(prefixRange));
   1520         this._prefixRange = prefixRange;
   1521         if (!this._suggestBox.visible())
   1522             this.finishAutocomplete();
   1523     },
   1524 
   1525     finishAutocomplete: function()
   1526     {
   1527         if (!this._suggestBox)
   1528             return;
   1529         this._suggestBox.hide();
   1530         this._suggestBox = null;
   1531         this._prefixRange = null;
   1532         this._anchorBox = null;
   1533     },
   1534 
   1535     /**
   1536      * @param {Event} e
   1537      */
   1538     keyDown: function(e)
   1539     {
   1540         if (!this._suggestBox)
   1541             return false;
   1542         if (e.keyCode === WebInspector.KeyboardShortcut.Keys.Esc.code) {
   1543             this.finishAutocomplete();
   1544             return true;
   1545         }
   1546         if (e.keyCode === WebInspector.KeyboardShortcut.Keys.Tab.code) {
   1547             this._suggestBox.acceptSuggestion();
   1548             this.finishAutocomplete();
   1549             return true;
   1550         }
   1551         return this._suggestBox.keyPressed(e);
   1552     },
   1553 
   1554     /**
   1555      * @param {string} suggestion
   1556      * @param {boolean=} isIntermediateSuggestion
   1557      */
   1558     applySuggestion: function(suggestion, isIntermediateSuggestion)
   1559     {
   1560         this._currentSuggestion = suggestion;
   1561     },
   1562 
   1563     acceptSuggestion: function()
   1564     {
   1565         if (this._prefixRange.endColumn - this._prefixRange.startColumn !== this._currentSuggestion.length) {
   1566             var pos = this._textEditor._toPos(this._prefixRange);
   1567             this._codeMirror.replaceRange(this._currentSuggestion, pos.start, pos.end, "+autocomplete");
   1568         }
   1569     },
   1570 
   1571     _onScroll: function()
   1572     {
   1573         if (!this._suggestBox)
   1574             return;
   1575         var cursor = this._codeMirror.getCursor();
   1576         var scrollInfo = this._codeMirror.getScrollInfo();
   1577         var topmostLineNumber = this._codeMirror.lineAtHeight(scrollInfo.top, "local");
   1578         var bottomLine = this._codeMirror.lineAtHeight(scrollInfo.top + scrollInfo.clientHeight, "local");
   1579         if (cursor.line < topmostLineNumber || cursor.line > bottomLine)
   1580             this.finishAutocomplete();
   1581         else {
   1582             this._anchorBox = this._anchorBoxForPosition(cursor.line, cursor.ch);
   1583             this._suggestBox.setPosition(this._anchorBox);
   1584         }
   1585     },
   1586 
   1587     _onCursorActivity: function()
   1588     {
   1589         if (!this._suggestBox)
   1590             return;
   1591         var cursor = this._codeMirror.getCursor();
   1592         if (cursor.line !== this._prefixRange.startLine || cursor.ch > this._prefixRange.endColumn || cursor.ch < this._prefixRange.startColumn)
   1593             this.finishAutocomplete();
   1594     },
   1595 
   1596     /**
   1597      * @param {number} line
   1598      * @param {number} column
   1599      * @return {AnchorBox}
   1600      */
   1601     _anchorBoxForPosition: function(line, column)
   1602     {
   1603         var metrics = this._textEditor.cursorPositionToCoordinates(line, column);
   1604         return metrics ? new AnchorBox(metrics.x, metrics.y, 0, metrics.height) : null;
   1605     },
   1606 }
   1607