Home | History | Annotate | Download | only in front_end
      1 /*
      2  * Copyright (C) 2009 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 /**
     32  * @constructor
     33  * @param {WebInspector.TextRange} newRange
     34  * @param {string} originalText
     35  * @param {WebInspector.TextRange} originalSelection
     36  */
     37 WebInspector.TextEditorCommand = function(newRange, originalText, originalSelection)
     38 {
     39     this.newRange = newRange;
     40     this.originalText = originalText;
     41     this.originalSelection = originalSelection;
     42 }
     43 
     44 /**
     45  * @constructor
     46  * @extends {WebInspector.Object}
     47  */
     48 WebInspector.TextEditorModel = function()
     49 {
     50     this._lines = [""];
     51     this._attributes = [];
     52     /** @type {Array.<WebInspector.TextEditorCommand>} */
     53     this._undoStack = [];
     54     this._noPunctuationRegex = /[^ !%&()*+,-.:;<=>?\[\]\^{|}~]+/;
     55     this._lineBreak = "\n";
     56 }
     57 
     58 WebInspector.TextEditorModel.Events = {
     59     TextChanged: "TextChanged"
     60 }
     61 
     62 WebInspector.TextEditorModel.endsWithBracketRegex = /[{(\[]\s*$/;
     63 
     64 WebInspector.TextEditorModel.prototype = {
     65     /**
     66      * @return {boolean}
     67      */
     68     isClean: function()
     69     {
     70         return this._cleanState === this._undoStack.length;
     71     },
     72 
     73     markClean: function()
     74     {
     75         this._cleanState = this._undoStack.length;
     76     },
     77 
     78     /**
     79      * @return {number}
     80      */
     81     get linesCount()
     82     {
     83         return this._lines.length;
     84     },
     85 
     86     /**
     87      * @return {string}
     88      */
     89     text: function()
     90     {
     91         return this._lines.join(this._lineBreak);
     92     },
     93 
     94     /**
     95      * @return {WebInspector.TextRange}
     96      */
     97     range: function()
     98     {
     99         return new WebInspector.TextRange(0, 0, this._lines.length - 1, this._lines[this._lines.length - 1].length);
    100     },
    101 
    102     /**
    103      * @return {string}
    104      */
    105     get lineBreak()
    106     {
    107         return this._lineBreak;
    108     },
    109 
    110     /**
    111      * @param {number} lineNumber
    112      * @return {string}
    113      */
    114     line: function(lineNumber)
    115     {
    116         if (lineNumber >= this._lines.length)
    117             throw "Out of bounds:" + lineNumber;
    118         return this._lines[lineNumber];
    119     },
    120 
    121     /**
    122      * @param {number} lineNumber
    123      * @return {number}
    124      */
    125     lineLength: function(lineNumber)
    126     {
    127         return this._lines[lineNumber].length;
    128     },
    129 
    130     /**
    131      * @param {string} text
    132      */
    133     setText: function(text)
    134     {
    135         this._resetUndoStack();
    136         text = text || "";
    137         this._attributes = [];
    138         var range = this.range();
    139         this._lineBreak = /\r\n/.test(text) ? "\r\n" : "\n";
    140         var newRange = this._innerSetText(range, text);
    141         this.dispatchEventToListeners(WebInspector.TextEditorModel.Events.TextChanged, { oldRange: range, newRange: newRange});
    142     },
    143 
    144     /**
    145      * @param {WebInspector.TextRange} range
    146      * @return {boolean}
    147      */
    148     _rangeHasOneCharacter: function(range)
    149     {
    150         if (range.startLine === range.endLine && range.endColumn - range.startColumn === 1)
    151             return true;
    152         if (range.endLine - range.startLine === 1 && range.endColumn === 0 && range.startColumn === this.lineLength(range.startLine))
    153             return true;
    154         return false;
    155     },
    156 
    157     /**
    158      * @param {WebInspector.TextRange} range
    159      * @param {string} text
    160      * @param {WebInspector.TextRange=} originalSelection
    161      * @return {boolean}
    162      */
    163     _isEditRangeUndoBoundary: function(range, text, originalSelection)
    164     {
    165         if (originalSelection && !originalSelection.isEmpty())
    166             return true;
    167         if (text)
    168             return text.length > 1 || !range.isEmpty();
    169         return !this._rangeHasOneCharacter(range);
    170     },
    171 
    172     /**
    173      * @param {WebInspector.TextRange} range
    174      * @param {string} text
    175      * @return {boolean}
    176      */
    177     _isEditRangeAdjacentToLastCommand: function(range, text)
    178     {
    179         if (!this._lastCommand)
    180             return true;
    181         if (!text) {
    182             // FIXME: Distinguish backspace and delete in lastCommand.
    183             return this._lastCommand.newRange.immediatelyPrecedes(range) || this._lastCommand.newRange.immediatelyFollows(range);
    184         }
    185         return text.indexOf("\n") === -1 && this._lastCommand.newRange.immediatelyPrecedes(range);
    186     },
    187 
    188     /**
    189      * @param {WebInspector.TextRange} range
    190      * @param {string} text
    191      * @param {WebInspector.TextRange=} originalSelection
    192      * @return {WebInspector.TextRange}
    193      */
    194     editRange: function(range, text, originalSelection)
    195     {
    196         var undoBoundary = this._isEditRangeUndoBoundary(range, text, originalSelection);
    197         if (undoBoundary || !this._isEditRangeAdjacentToLastCommand(range, text))
    198             this._markUndoableState();
    199         var newRange = this._innerEditRange(range, text, originalSelection);
    200         if (undoBoundary)
    201             this._markUndoableState();
    202         return newRange;
    203     },
    204 
    205     /**
    206      * @param {WebInspector.TextRange} range
    207      * @param {string} text
    208      * @param {WebInspector.TextRange=} originalSelection
    209      * @return {WebInspector.TextRange}
    210      */
    211     _innerEditRange: function(range, text, originalSelection)
    212     {
    213         var originalText = this.copyRange(range);
    214         var newRange = this._innerSetText(range, text);
    215         this._lastCommand = this._pushUndoableCommand(newRange, originalText, originalSelection || range);
    216         this.dispatchEventToListeners(WebInspector.TextEditorModel.Events.TextChanged, { oldRange: range, newRange: newRange, editRange: true });
    217         return newRange;
    218     },
    219 
    220     /**
    221      * @param {WebInspector.TextRange} range
    222      * @param {string} text
    223      * @return {WebInspector.TextRange}
    224      */
    225     _innerSetText: function(range, text)
    226     {
    227         this._eraseRange(range);
    228         if (text === "")
    229             return new WebInspector.TextRange(range.startLine, range.startColumn, range.startLine, range.startColumn);
    230 
    231         var newLines = text.split(/\r?\n/);
    232 
    233         var prefix = this._lines[range.startLine].substring(0, range.startColumn);
    234         var suffix = this._lines[range.startLine].substring(range.startColumn);
    235 
    236         var postCaret = prefix.length;
    237         // Insert text.
    238         if (newLines.length === 1) {
    239             this._setLine(range.startLine, prefix + newLines[0] + suffix);
    240             postCaret += newLines[0].length;
    241         } else {
    242             this._setLine(range.startLine, prefix + newLines[0]);
    243             this._insertLines(range, newLines);
    244             this._setLine(range.startLine + newLines.length - 1, newLines[newLines.length - 1] + suffix);
    245             postCaret = newLines[newLines.length - 1].length;
    246         }
    247 
    248         return new WebInspector.TextRange(range.startLine, range.startColumn,
    249                                           range.startLine + newLines.length - 1, postCaret);
    250     },
    251 
    252     /**
    253      * @param {WebInspector.TextRange} range
    254      * @param {Array.<string>} newLines
    255      */
    256     _insertLines: function(range, newLines)
    257     {
    258         var lines = new Array(this._lines.length + newLines.length - 1);
    259         for (var i = 0; i <= range.startLine; ++i)
    260             lines[i] = this._lines[i];
    261         // Line at [0] is already set via setLine.
    262         for (var i = 1; i < newLines.length; ++i)
    263             lines[range.startLine + i] = newLines[i];
    264         for (var i = range.startLine + newLines.length; i < lines.length; ++i)
    265             lines[i] = this._lines[i - newLines.length + 1];
    266         this._lines = lines;
    267 
    268         // Adjust attributes, attributes move with the first character of line.
    269         var attributes = new Array(lines.length);
    270         var insertionIndex = range.startColumn ? range.startLine + 1 : range.startLine;
    271         for (var i = 0; i < insertionIndex; ++i)
    272             attributes[i] = this._attributes[i];
    273         for (var i = insertionIndex + newLines.length - 1; i < attributes.length; ++i)
    274             attributes[i] = this._attributes[i - newLines.length + 1];
    275         this._attributes = attributes;
    276     },
    277 
    278     /**
    279      * @param {WebInspector.TextRange} range
    280      */
    281     _eraseRange: function(range)
    282     {
    283         if (range.isEmpty())
    284             return;
    285 
    286         var prefix = this._lines[range.startLine].substring(0, range.startColumn);
    287         var suffix = this._lines[range.endLine].substring(range.endColumn);
    288 
    289         if (range.endLine > range.startLine) {
    290             this._lines.splice(range.startLine + 1, range.endLine - range.startLine);
    291             // Adjust attributes, attributes move with the first character of line.
    292             this._attributes.splice(range.startColumn ? range.startLine + 1 : range.startLine, range.endLine - range.startLine);
    293         }
    294         this._setLine(range.startLine, prefix + suffix);
    295     },
    296 
    297     /**
    298      * @param {number} lineNumber
    299      * @param {string} text
    300      */
    301     _setLine: function(lineNumber, text)
    302     {
    303         this._lines[lineNumber] = text;
    304     },
    305 
    306     /**
    307      * @param {number} lineNumber
    308      * @param {number} column
    309      * @return {WebInspector.TextRange}
    310      */
    311     wordRange: function(lineNumber, column)
    312     {
    313         return new WebInspector.TextRange(lineNumber, this.wordStart(lineNumber, column, true), lineNumber, this.wordEnd(lineNumber, column, true));
    314     },
    315 
    316     /**
    317      * @param {number} lineNumber
    318      * @param {number} column
    319      * @param {boolean} gapless
    320      * @return {number}
    321      */
    322     wordStart: function(lineNumber, column, gapless)
    323     {
    324         var line = this._lines[lineNumber];
    325         var prefix = line.substring(0, column).split("").reverse().join("");
    326         var prefixMatch = this._noPunctuationRegex.exec(prefix);
    327         return prefixMatch && (!gapless || prefixMatch.index === 0) ? column - prefixMatch.index - prefixMatch[0].length : column;
    328     },
    329 
    330     /**
    331      * @param {number} lineNumber
    332      * @param {number} column
    333      * @param {boolean} gapless
    334      * @return {number}
    335      */
    336     wordEnd: function(lineNumber, column, gapless)
    337     {
    338         var line = this._lines[lineNumber];
    339         var suffix = line.substring(column);
    340         var suffixMatch = this._noPunctuationRegex.exec(suffix);
    341         return suffixMatch && (!gapless || suffixMatch.index === 0) ? column + suffixMatch.index + suffixMatch[0].length : column;
    342     },
    343 
    344     /**
    345      * @param {WebInspector.TextRange} range
    346      * @return {string}
    347      */
    348     copyRange: function(range)
    349     {
    350         if (!range)
    351             range = this.range();
    352 
    353         var clip = [];
    354         if (range.startLine === range.endLine) {
    355             clip.push(this._lines[range.startLine].substring(range.startColumn, range.endColumn));
    356             return clip.join(this._lineBreak);
    357         }
    358         clip.push(this._lines[range.startLine].substring(range.startColumn));
    359         for (var i = range.startLine + 1; i < range.endLine; ++i)
    360             clip.push(this._lines[i]);
    361         clip.push(this._lines[range.endLine].substring(0, range.endColumn));
    362         return clip.join(this._lineBreak);
    363     },
    364 
    365     /**
    366      * @param {number} line
    367      * @param {string} name
    368      * @param {Object?} value
    369      */
    370     setAttribute: function(line, name, value)
    371     {
    372         var attrs = this._attributes[line];
    373         if (!attrs) {
    374             attrs = {};
    375             this._attributes[line] = attrs;
    376         }
    377         attrs[name] = value;
    378     },
    379 
    380     /**
    381      * @param {number} line
    382      * @param {string} name
    383      * @return {Object|null} value
    384      */
    385     getAttribute: function(line, name)
    386     {
    387         var attrs = this._attributes[line];
    388         return attrs ? attrs[name] : null;
    389     },
    390 
    391     /**
    392      * @param {number} line
    393      * @param {string} name
    394      */
    395     removeAttribute: function(line, name)
    396     {
    397         var attrs = this._attributes[line];
    398         if (attrs)
    399             delete attrs[name];
    400     },
    401 
    402     /**
    403      * @param {WebInspector.TextRange} newRange
    404      * @param {string} originalText
    405      * @param {WebInspector.TextRange} originalSelection
    406      * @return {WebInspector.TextEditorCommand}
    407      */
    408     _pushUndoableCommand: function(newRange, originalText, originalSelection)
    409     {
    410         var command = new WebInspector.TextEditorCommand(newRange.clone(), originalText, originalSelection);
    411         if (this._inUndo)
    412             this._redoStack.push(command);
    413         else {
    414             if (!this._inRedo) {
    415                 this._redoStack = [];
    416                 if (typeof this._cleanState === "number" && this._cleanState > this._undoStack.length)
    417                     delete this._cleanState;
    418             }
    419             this._undoStack.push(command);
    420         }
    421         return command;
    422     },
    423 
    424     /**
    425      * @return {?WebInspector.TextRange}
    426      */
    427     undo: function()
    428     {
    429         if (!this._undoStack.length)
    430             return null;
    431 
    432         this._markRedoableState();
    433 
    434         this._inUndo = true;
    435         var range = this._doUndo(this._undoStack);
    436         delete this._inUndo;
    437 
    438         return range;
    439     },
    440 
    441     /**
    442      * @return {WebInspector.TextRange}
    443      */
    444     redo: function()
    445     {
    446         if (!this._redoStack || !this._redoStack.length)
    447             return null;
    448         this._markUndoableState();
    449 
    450         this._inRedo = true;
    451         var range = this._doUndo(this._redoStack);
    452         delete this._inRedo;
    453 
    454         return range ? range.collapseToEnd() : null;
    455     },
    456 
    457     /**
    458      * @param {Array.<WebInspector.TextEditorCommand>} stack
    459      * @return {WebInspector.TextRange}
    460      */
    461     _doUndo: function(stack)
    462     {
    463         var range = null;
    464         for (var i = stack.length - 1; i >= 0; --i) {
    465             var command = stack[i];
    466             stack.length = i;
    467             this._innerEditRange(command.newRange, command.originalText);
    468             range = command.originalSelection;
    469             if (i > 0 && stack[i - 1].explicit)
    470                 return range;
    471         }
    472         return range;
    473     },
    474 
    475     _markUndoableState: function()
    476     {
    477         if (this._undoStack.length)
    478             this._undoStack[this._undoStack.length - 1].explicit = true;
    479     },
    480 
    481     _markRedoableState: function()
    482     {
    483         if (this._redoStack.length)
    484             this._redoStack[this._redoStack.length - 1].explicit = true;
    485     },
    486 
    487     _resetUndoStack: function()
    488     {
    489         delete this._cleanState;
    490         this._undoStack = [];
    491         this._redoStack = [];
    492     },
    493 
    494     /**
    495      * @param {WebInspector.TextRange} range
    496      * @return {WebInspector.TextRange}
    497      */
    498     indentLines: function(range)
    499     {
    500         this._markUndoableState();
    501 
    502         var indent = WebInspector.settings.textEditorIndent.get();
    503         var newRange = range.clone();
    504         // Do not change a selection start position when it is at the beginning of a line
    505         if (range.startColumn)
    506             newRange.startColumn += indent.length;
    507 
    508         var indentEndLine = range.endLine;
    509         if (range.endColumn)
    510             newRange.endColumn += indent.length;
    511         else
    512             indentEndLine--;
    513 
    514         for (var lineNumber = range.startLine; lineNumber <= indentEndLine; lineNumber++)
    515             this._innerEditRange(WebInspector.TextRange.createFromLocation(lineNumber, 0), indent);
    516 
    517         return newRange;
    518     },
    519 
    520     /**
    521      * @param {WebInspector.TextRange} range
    522      * @return {WebInspector.TextRange}
    523      */
    524     unindentLines: function(range)
    525     {
    526         this._markUndoableState();
    527 
    528         var indent = WebInspector.settings.textEditorIndent.get();
    529         var indentLength = indent === WebInspector.TextUtils.Indent.TabCharacter ? 4 : indent.length;
    530         var lineIndentRegex = new RegExp("^ {1," + indentLength + "}");
    531         var newRange = range.clone();
    532 
    533         var indentEndLine = range.endLine;
    534         if (!range.endColumn)
    535             indentEndLine--;
    536 
    537         for (var lineNumber = range.startLine; lineNumber <= indentEndLine; lineNumber++) {
    538             var line = this.line(lineNumber);
    539             var firstCharacter = line.charAt(0);
    540             var lineIndentLength;
    541 
    542             if (firstCharacter === " ")
    543                 lineIndentLength = line.match(lineIndentRegex)[0].length;
    544             else if (firstCharacter === "\t")
    545                 lineIndentLength = 1;
    546             else
    547                 continue;
    548 
    549             this._innerEditRange(new WebInspector.TextRange(lineNumber, 0, lineNumber, lineIndentLength), "");
    550 
    551             if (lineNumber === range.startLine)
    552                 newRange.startColumn = Math.max(0, newRange.startColumn - lineIndentLength);
    553             if (lineNumber === range.endLine)
    554                 newRange.endColumn = Math.max(0, newRange.endColumn - lineIndentLength);
    555         }
    556 
    557         return newRange;
    558     },
    559 
    560     /**
    561      * @param {number=} from
    562      * @param {number=} to
    563      * @return {WebInspector.TextEditorModel}
    564      */
    565     slice: function(from, to)
    566     {
    567         var textModel = new WebInspector.TextEditorModel();
    568         textModel._lines = this._lines.slice(from, to);
    569         textModel._lineBreak = this._lineBreak;
    570         return textModel;
    571     },
    572 
    573     /**
    574      * @param {WebInspector.TextRange} range
    575      * @return {WebInspector.TextRange}
    576      */
    577     growRangeLeft: function(range)
    578     {
    579         var result = range.clone();
    580         if (result.startColumn)
    581             --result.startColumn;
    582         else if (result.startLine)
    583             result.startColumn = this.lineLength(--result.startLine);
    584         return result;
    585     },
    586 
    587     /**
    588      * @param {WebInspector.TextRange} range
    589      * @return {WebInspector.TextRange}
    590      */
    591     growRangeRight: function(range)
    592     {
    593         var result = range.clone();
    594         if (result.endColumn < this.lineLength(result.endLine))
    595             ++result.endColumn;
    596         else if (result.endLine < this.linesCount) {
    597             result.endColumn = 0;
    598             ++result.endLine;
    599         }
    600         return result;
    601     },
    602 
    603     __proto__: WebInspector.Object.prototype
    604 }
    605 
    606 /**
    607  * @constructor
    608  * @param {WebInspector.TextEditorModel} textModel
    609  */
    610 WebInspector.TextEditorModel.BraceMatcher = function(textModel)
    611 {
    612     this._textModel = textModel;
    613 }
    614 
    615 WebInspector.TextEditorModel.BraceMatcher.prototype = {
    616     /**
    617      * @param {number} lineNumber
    618      * @return {Array.<{startColumn: number, endColumn: number, token: string}>}
    619      */
    620     _braceRanges: function(lineNumber)
    621     {
    622         if (lineNumber >= this._textModel.linesCount || lineNumber < 0)
    623             return null;
    624 
    625         var attribute = this._textModel.getAttribute(lineNumber, "highlight");
    626         if (!attribute)
    627             return null;
    628         else
    629             return attribute.braces;
    630     },
    631 
    632     /**
    633      * @param {string} braceTokenLeft
    634      * @param {string} braceTokenRight
    635      * @return {boolean}
    636      */
    637     _matches: function(braceTokenLeft, braceTokenRight)
    638     {
    639         return ((braceTokenLeft === "brace-start" && braceTokenRight === "brace-end") || (braceTokenLeft === "block-start" && braceTokenRight === "block-end"));
    640     },
    641 
    642     /**
    643      * @param {number} lineNumber
    644      * @param {number} column
    645      * @param {number=} maxBraceIteration
    646      * @return {?{lineNumber: number, column: number, token: string}}
    647      */
    648     findLeftCandidate: function(lineNumber, column, maxBraceIteration)
    649     {
    650         var braces = this._braceRanges(lineNumber);
    651         if (!braces)
    652             return null;
    653 
    654         var braceIndex = braces.length - 1;
    655         while (braceIndex >= 0 && braces[braceIndex].startColumn > column)
    656             --braceIndex;
    657 
    658         var brace = braceIndex >= 0 ? braces[braceIndex] : null;
    659         if (brace && brace.startColumn === column && (brace.token === "block-end" || brace.token === "brace-end"))
    660             --braceIndex;
    661 
    662         var stack = [];
    663         maxBraceIteration = maxBraceIteration || Number.MAX_VALUE;
    664         while (--maxBraceIteration) {
    665             if (braceIndex < 0) {
    666                 while ((braces = this._braceRanges(--lineNumber)) && !braces.length) {};
    667                 if (!braces)
    668                     return null;
    669                 braceIndex = braces.length - 1;
    670             }
    671             brace = braces[braceIndex];
    672             if (brace.token === "block-end" || brace.token === "brace-end")
    673                 stack.push(brace.token);
    674             else if (stack.length === 0)
    675                 return {
    676                     lineNumber: lineNumber,
    677                     column: brace.startColumn,
    678                     token: brace.token
    679                 };
    680             else if (!this._matches(brace.token, stack.pop()))
    681                 return null;
    682 
    683             --braceIndex;
    684         }
    685         return null;
    686     },
    687 
    688     /**
    689      * @param {number} lineNumber
    690      * @param {number} column
    691      * @param {number=} maxBraceIteration
    692      * @return {?{lineNumber: number, column: number, token: string}}
    693      */
    694     findRightCandidate: function(lineNumber, column, maxBraceIteration)
    695     {
    696         var braces = this._braceRanges(lineNumber);
    697         if (!braces)
    698             return null;
    699 
    700         var braceIndex = 0;
    701         while (braceIndex < braces.length && braces[braceIndex].startColumn < column)
    702             ++braceIndex;
    703 
    704         var brace = braceIndex < braces.length ? braces[braceIndex] : null;
    705         if (brace && brace.startColumn === column && (brace.token === "block-start" || brace.token === "brace-start"))
    706             ++braceIndex;
    707 
    708         var stack = [];
    709         maxBraceIteration = maxBraceIteration || Number.MAX_VALUE;
    710         while (--maxBraceIteration) {
    711             if (braceIndex >= braces.length) {
    712                 while ((braces = this._braceRanges(++lineNumber)) && !braces.length) {};
    713                 if (!braces)
    714                     return null;
    715                 braceIndex = 0;
    716             }
    717             brace = braces[braceIndex];
    718             if (brace.token === "block-start" || brace.token === "brace-start")
    719                 stack.push(brace.token);
    720             else if (stack.length === 0)
    721                 return {
    722                     lineNumber: lineNumber,
    723                     column: brace.startColumn,
    724                     token: brace.token
    725                 };
    726             else if (!this._matches(stack.pop(), brace.token))
    727                 return null;
    728             ++braceIndex;
    729         }
    730         return null;
    731     },
    732 
    733     /**
    734      * @param {number} lineNumber
    735      * @param {number} column
    736      * @param {number=} maxBraceIteration
    737      * @return {?{leftBrace: {lineNumber: number, column: number, token: string}, rightBrace: {lineNumber: number, column: number, token: string}}}
    738      */
    739     enclosingBraces: function(lineNumber, column, maxBraceIteration)
    740     {
    741         var leftBraceLocation = this.findLeftCandidate(lineNumber, column, maxBraceIteration);
    742         if (!leftBraceLocation)
    743             return null;
    744 
    745         var rightBraceLocation = this.findRightCandidate(lineNumber, column, maxBraceIteration);
    746         if (!rightBraceLocation)
    747             return null;
    748 
    749         if (!this._matches(leftBraceLocation.token, rightBraceLocation.token))
    750             return null;
    751 
    752         return {
    753             leftBrace: leftBraceLocation,
    754             rightBrace: rightBraceLocation
    755         };
    756     },
    757 }
    758