Home | History | Annotate | Download | only in front-end
      1 /*
      2  * Copyright (C) 2011 Google Inc. All rights reserved.
      3  * Copyright (C) 2010 Apple Inc. All rights reserved.
      4  *
      5  * Redistribution and use in source and binary forms, with or without
      6  * modification, are permitted provided that the following conditions are
      7  * met:
      8  *
      9  *     * Redistributions of source code must retain the above copyright
     10  * notice, this list of conditions and the following disclaimer.
     11  *     * Redistributions in binary form must reproduce the above
     12  * copyright notice, this list of conditions and the following disclaimer
     13  * in the documentation and/or other materials provided with the
     14  * distribution.
     15  *     * Neither the name of Google Inc. nor the names of its
     16  * contributors may be used to endorse or promote products derived from
     17  * this software without specific prior written permission.
     18  *
     19  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
     20  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
     21  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
     22  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
     23  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
     24  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
     25  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
     26  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
     27  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     28  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
     29  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     30  */
     31 
     32 WebInspector.TextViewer = function(textModel, platform, url, delegate)
     33 {
     34     WebInspector.View.call(this);
     35 
     36     this._textModel = textModel;
     37     this._textModel.changeListener = this._textChanged.bind(this);
     38     this._textModel.resetUndoStack();
     39     this._delegate = delegate;
     40 
     41     this.element.className = "text-editor monospace";
     42 
     43     var enterTextChangeMode = this._enterInternalTextChangeMode.bind(this);
     44     var exitTextChangeMode = this._exitInternalTextChangeMode.bind(this);
     45     var syncScrollListener = this._syncScroll.bind(this);
     46     var syncDecorationsForLineListener = this._syncDecorationsForLine.bind(this);
     47     this._mainPanel = new WebInspector.TextEditorMainPanel(this._textModel, url, syncScrollListener, syncDecorationsForLineListener, enterTextChangeMode, exitTextChangeMode);
     48     this._gutterPanel = new WebInspector.TextEditorGutterPanel(this._textModel, syncDecorationsForLineListener);
     49     this.element.appendChild(this._mainPanel.element);
     50     this.element.appendChild(this._gutterPanel.element);
     51 
     52     // Forward mouse wheel events from the unscrollable gutter to the main panel.
     53     this._gutterPanel.element.addEventListener("mousewheel", function(e) {
     54         this._mainPanel.element.dispatchEvent(e);
     55     }.bind(this), false);
     56 
     57     this.element.addEventListener("dblclick", this._doubleClick.bind(this), true);
     58     this.element.addEventListener("keydown", this._handleKeyDown.bind(this), false);
     59 
     60     this._registerShortcuts();
     61 }
     62 
     63 WebInspector.TextViewer.prototype = {
     64     set mimeType(mimeType)
     65     {
     66         this._mainPanel.mimeType = mimeType;
     67     },
     68 
     69     set readOnly(readOnly)
     70     {
     71         if (this._mainPanel.readOnly === readOnly)
     72             return;
     73         this._mainPanel.readOnly = readOnly;
     74         this._delegate.readOnlyStateChanged(readOnly);
     75     },
     76 
     77     get readOnly()
     78     {
     79         return this._mainPanel.readOnly;
     80     },
     81 
     82     get textModel()
     83     {
     84         return this._textModel;
     85     },
     86 
     87     revealLine: function(lineNumber)
     88     {
     89         this._mainPanel.revealLine(lineNumber);
     90     },
     91 
     92     addDecoration: function(lineNumber, decoration)
     93     {
     94         this._mainPanel.addDecoration(lineNumber, decoration);
     95         this._gutterPanel.addDecoration(lineNumber, decoration);
     96     },
     97 
     98     removeDecoration: function(lineNumber, decoration)
     99     {
    100         this._mainPanel.removeDecoration(lineNumber, decoration);
    101         this._gutterPanel.removeDecoration(lineNumber, decoration);
    102     },
    103 
    104     markAndRevealRange: function(range)
    105     {
    106         this._mainPanel.markAndRevealRange(range);
    107     },
    108 
    109     highlightLine: function(lineNumber)
    110     {
    111         if (typeof lineNumber !== "number" || lineNumber < 0)
    112             return;
    113 
    114         this._mainPanel.highlightLine(lineNumber);
    115     },
    116 
    117     clearLineHighlight: function()
    118     {
    119         this._mainPanel.clearLineHighlight();
    120     },
    121 
    122     freeCachedElements: function()
    123     {
    124         this._mainPanel.freeCachedElements();
    125         this._gutterPanel.freeCachedElements();
    126     },
    127 
    128     get scrollTop()
    129     {
    130         return this._mainPanel.element.scrollTop;
    131     },
    132 
    133     set scrollTop(scrollTop)
    134     {
    135         this._mainPanel.element.scrollTop = scrollTop;
    136     },
    137 
    138     get scrollLeft()
    139     {
    140         return this._mainPanel.element.scrollLeft;
    141     },
    142 
    143     set scrollLeft(scrollLeft)
    144     {
    145         this._mainPanel.element.scrollLeft = scrollLeft;
    146     },
    147 
    148     beginUpdates: function()
    149     {
    150         this._mainPanel.beginUpdates();
    151         this._gutterPanel.beginUpdates();
    152     },
    153 
    154     endUpdates: function()
    155     {
    156         this._mainPanel.endUpdates();
    157         this._gutterPanel.endUpdates();
    158         this._updatePanelOffsets();
    159     },
    160 
    161     resize: function()
    162     {
    163         this._mainPanel.resize();
    164         this._gutterPanel.resize();
    165         this._updatePanelOffsets();
    166     },
    167 
    168     // WebInspector.TextModel listener
    169     _textChanged: function(oldRange, newRange, oldText, newText)
    170     {
    171         if (!this._internalTextChangeMode)
    172             this._textModel.resetUndoStack();
    173         this._mainPanel.textChanged(oldRange, newRange);
    174         this._gutterPanel.textChanged(oldRange, newRange);
    175         this._updatePanelOffsets();
    176     },
    177 
    178     _enterInternalTextChangeMode: function()
    179     {
    180         this._internalTextChangeMode = true;
    181         this._delegate.startEditing();
    182     },
    183 
    184     _exitInternalTextChangeMode: function(oldRange, newRange)
    185     {
    186         this._internalTextChangeMode = false;
    187         this._delegate.endEditing(oldRange, newRange);
    188     },
    189 
    190     _updatePanelOffsets: function()
    191     {
    192         var lineNumbersWidth = this._gutterPanel.element.offsetWidth;
    193         if (lineNumbersWidth)
    194             this._mainPanel.element.style.setProperty("left", lineNumbersWidth + "px");
    195         else
    196             this._mainPanel.element.style.removeProperty("left"); // Use default value set in CSS.
    197     },
    198 
    199     _syncScroll: function()
    200     {
    201         // Async call due to performance reasons.
    202         setTimeout(function() {
    203             var mainElement = this._mainPanel.element;
    204             var gutterElement = this._gutterPanel.element;
    205             // Handle horizontal scroll bar at the bottom of the main panel.
    206             this._gutterPanel.syncClientHeight(mainElement.clientHeight);
    207             gutterElement.scrollTop = mainElement.scrollTop;
    208         }.bind(this), 0);
    209     },
    210 
    211     _syncDecorationsForLine: function(lineNumber)
    212     {
    213         if (lineNumber >= this._textModel.linesCount)
    214             return;
    215 
    216         var mainChunk = this._mainPanel.chunkForLine(lineNumber);
    217         if (mainChunk.linesCount === 1 && mainChunk.decorated) {
    218             var gutterChunk = this._gutterPanel.makeLineAChunk(lineNumber);
    219             var height = mainChunk.height;
    220             if (height)
    221                 gutterChunk.element.style.setProperty("height", height + "px");
    222             else
    223                 gutterChunk.element.style.removeProperty("height");
    224         } else {
    225             var gutterChunk = this._gutterPanel.chunkForLine(lineNumber);
    226             if (gutterChunk.linesCount === 1)
    227                 gutterChunk.element.style.removeProperty("height");
    228         }
    229     },
    230 
    231     _doubleClick: function(event)
    232     {
    233         if (!this.readOnly || this._commitEditingInProgress)
    234             return;
    235 
    236         var lineRow = event.target.enclosingNodeOrSelfWithClass("webkit-line-content");
    237         if (!lineRow)
    238             return;  // Do not trigger editing from line numbers.
    239 
    240         if (!this._delegate.isContentEditable())
    241             return;
    242 
    243         this.readOnly = false;
    244         window.getSelection().collapseToStart();
    245     },
    246 
    247     _registerShortcuts: function()
    248     {
    249         var keys = WebInspector.KeyboardShortcut.Keys;
    250         var modifiers = WebInspector.KeyboardShortcut.Modifiers;
    251 
    252         this._shortcuts = {};
    253         var commitEditing = this._commitEditing.bind(this);
    254         var cancelEditing = this._cancelEditing.bind(this);
    255         this._shortcuts[WebInspector.KeyboardShortcut.makeKey("s", modifiers.CtrlOrMeta)] = commitEditing;
    256         this._shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Enter.code, modifiers.CtrlOrMeta)] = commitEditing;
    257         this._shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Esc.code)] = cancelEditing;
    258 
    259         var handleUndo = this._mainPanel.handleUndoRedo.bind(this._mainPanel, false);
    260         var handleRedo = this._mainPanel.handleUndoRedo.bind(this._mainPanel, true);
    261         this._shortcuts[WebInspector.KeyboardShortcut.makeKey("z", modifiers.CtrlOrMeta)] = handleUndo;
    262         this._shortcuts[WebInspector.KeyboardShortcut.makeKey("z", modifiers.Shift | modifiers.CtrlOrMeta)] = handleRedo;
    263 
    264         var handleTabKey = this._mainPanel.handleTabKeyPress.bind(this._mainPanel, false);
    265         var handleShiftTabKey = this._mainPanel.handleTabKeyPress.bind(this._mainPanel, true);
    266         this._shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Tab.code)] = handleTabKey;
    267         this._shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Tab.code, modifiers.Shift)] = handleShiftTabKey;
    268     },
    269 
    270     _handleKeyDown: function(e)
    271     {
    272         var shortcutKey = WebInspector.KeyboardShortcut.makeKeyFromEvent(e);
    273         var handler = this._shortcuts[shortcutKey];
    274         if (handler && handler.call(this)) {
    275             e.preventDefault();
    276             e.stopPropagation();
    277         }
    278     },
    279 
    280     _commitEditing: function()
    281     {
    282         if (this.readOnly)
    283             return false;
    284 
    285         this.readOnly = true;
    286         function didCommitEditing(error)
    287         {
    288             this._commitEditingInProgress = false;
    289             if (error)
    290                 this.readOnly = false;
    291         }
    292         this._commitEditingInProgress = true;
    293         this._delegate.commitEditing(didCommitEditing.bind(this));
    294         return true;
    295     },
    296 
    297     _cancelEditing: function()
    298     {
    299         if (this.readOnly)
    300             return false;
    301 
    302         this.readOnly = true;
    303         this._delegate.cancelEditing();
    304         return true;
    305     }
    306 }
    307 
    308 WebInspector.TextViewer.prototype.__proto__ = WebInspector.View.prototype;
    309 
    310 WebInspector.TextViewerDelegate = function()
    311 {
    312 }
    313 
    314 WebInspector.TextViewerDelegate.prototype = {
    315     isContentEditable: function()
    316     {
    317         // Should be implemented by subclasses.
    318     },
    319 
    320     readOnlyStateChanged: function(readOnly)
    321     {
    322         // Should be implemented by subclasses.
    323     },
    324 
    325     startEditing: function()
    326     {
    327         // Should be implemented by subclasses.
    328     },
    329 
    330     endEditing: function(oldRange, newRange)
    331     {
    332         // Should be implemented by subclasses.
    333     },
    334 
    335     commitEditing: function()
    336     {
    337         // Should be implemented by subclasses.
    338     },
    339 
    340     cancelEditing: function()
    341     {
    342         // Should be implemented by subclasses.
    343     }
    344 }
    345 
    346 WebInspector.TextViewerDelegate.prototype.__proto__ = WebInspector.Object.prototype;
    347 
    348 WebInspector.TextEditorChunkedPanel = function(textModel)
    349 {
    350     this._textModel = textModel;
    351 
    352     this._defaultChunkSize = 50;
    353     this._paintCoalescingLevel = 0;
    354     this._domUpdateCoalescingLevel = 0;
    355 }
    356 
    357 WebInspector.TextEditorChunkedPanel.prototype = {
    358     get textModel()
    359     {
    360         return this._textModel;
    361     },
    362 
    363     revealLine: function(lineNumber)
    364     {
    365         if (lineNumber >= this._textModel.linesCount)
    366             return;
    367 
    368         var chunk = this.makeLineAChunk(lineNumber);
    369         chunk.element.scrollIntoViewIfNeeded();
    370     },
    371 
    372     addDecoration: function(lineNumber, decoration)
    373     {
    374         if (lineNumber >= this._textModel.linesCount)
    375             return;
    376 
    377         var chunk = this.makeLineAChunk(lineNumber);
    378         chunk.addDecoration(decoration);
    379     },
    380 
    381     removeDecoration: function(lineNumber, decoration)
    382     {
    383         if (lineNumber >= this._textModel.linesCount)
    384             return;
    385 
    386         var chunk = this.chunkForLine(lineNumber);
    387         chunk.removeDecoration(decoration);
    388     },
    389 
    390     _buildChunks: function()
    391     {
    392         this.beginDomUpdates();
    393 
    394         this._container.removeChildren();
    395 
    396         this._textChunks = [];
    397         for (var i = 0; i < this._textModel.linesCount; i += this._defaultChunkSize) {
    398             var chunk = this._createNewChunk(i, i + this._defaultChunkSize);
    399             this._textChunks.push(chunk);
    400             this._container.appendChild(chunk.element);
    401         }
    402 
    403         this._repaintAll();
    404 
    405         this.endDomUpdates();
    406     },
    407 
    408     makeLineAChunk: function(lineNumber)
    409     {
    410         var chunkNumber = this._chunkNumberForLine(lineNumber);
    411         var oldChunk = this._textChunks[chunkNumber];
    412 
    413         if (!oldChunk) {
    414             console.error("No chunk for line number: " + lineNumber);
    415             return;
    416         }
    417 
    418         if (oldChunk.linesCount === 1)
    419             return oldChunk;
    420 
    421         return this._splitChunkOnALine(lineNumber, chunkNumber);
    422     },
    423 
    424     _splitChunkOnALine: function(lineNumber, chunkNumber)
    425     {
    426         this.beginDomUpdates();
    427 
    428         var oldChunk = this._textChunks[chunkNumber];
    429         var wasExpanded = oldChunk.expanded;
    430         oldChunk.expanded = false;
    431 
    432         var insertIndex = chunkNumber + 1;
    433 
    434         // Prefix chunk.
    435         if (lineNumber > oldChunk.startLine) {
    436             var prefixChunk = this._createNewChunk(oldChunk.startLine, lineNumber);
    437             this._textChunks.splice(insertIndex++, 0, prefixChunk);
    438             this._container.insertBefore(prefixChunk.element, oldChunk.element);
    439         }
    440 
    441         // Line chunk.
    442         var lineChunk = this._createNewChunk(lineNumber, lineNumber + 1);
    443         this._textChunks.splice(insertIndex++, 0, lineChunk);
    444         this._container.insertBefore(lineChunk.element, oldChunk.element);
    445 
    446         // Suffix chunk.
    447         if (oldChunk.startLine + oldChunk.linesCount > lineNumber + 1) {
    448             var suffixChunk = this._createNewChunk(lineNumber + 1, oldChunk.startLine + oldChunk.linesCount);
    449             this._textChunks.splice(insertIndex, 0, suffixChunk);
    450             this._container.insertBefore(suffixChunk.element, oldChunk.element);
    451         }
    452 
    453         // Remove enclosing chunk.
    454         this._textChunks.splice(chunkNumber, 1);
    455         this._container.removeChild(oldChunk.element);
    456 
    457         if (wasExpanded) {
    458             if (prefixChunk)
    459                 prefixChunk.expanded = true;
    460             lineChunk.expanded = true;
    461             if (suffixChunk)
    462                 suffixChunk.expanded = true;
    463         }
    464 
    465         this.endDomUpdates();
    466 
    467         return lineChunk;
    468     },
    469 
    470     _scroll: function()
    471     {
    472         // FIXME: Replace the "2" with the padding-left value from CSS.
    473         if (this.element.scrollLeft <= 2)
    474             this.element.scrollLeft = 0;
    475 
    476         this._scheduleRepaintAll();
    477         if (this._syncScrollListener)
    478             this._syncScrollListener();
    479     },
    480 
    481     _scheduleRepaintAll: function()
    482     {
    483         if (this._repaintAllTimer)
    484             clearTimeout(this._repaintAllTimer);
    485         this._repaintAllTimer = setTimeout(this._repaintAll.bind(this), 50);
    486     },
    487 
    488     beginUpdates: function()
    489     {
    490         this._paintCoalescingLevel++;
    491     },
    492 
    493     endUpdates: function()
    494     {
    495         this._paintCoalescingLevel--;
    496         if (!this._paintCoalescingLevel)
    497             this._repaintAll();
    498     },
    499 
    500     beginDomUpdates: function()
    501     {
    502         this._domUpdateCoalescingLevel++;
    503     },
    504 
    505     endDomUpdates: function()
    506     {
    507         this._domUpdateCoalescingLevel--;
    508     },
    509 
    510     _chunkNumberForLine: function(lineNumber)
    511     {
    512         function compareLineNumbers(value, chunk)
    513         {
    514             return value < chunk.startLine ? -1 : 1;
    515         }
    516         var insertBefore = insertionIndexForObjectInListSortedByFunction(lineNumber, this._textChunks, compareLineNumbers);
    517         return insertBefore - 1;
    518     },
    519 
    520     chunkForLine: function(lineNumber)
    521     {
    522         return this._textChunks[this._chunkNumberForLine(lineNumber)];
    523     },
    524 
    525     _findFirstVisibleChunkNumber: function(visibleFrom)
    526     {
    527         function compareOffsetTops(value, chunk)
    528         {
    529             return value < chunk.offsetTop ? -1 : 1;
    530         }
    531         var insertBefore = insertionIndexForObjectInListSortedByFunction(visibleFrom, this._textChunks, compareOffsetTops);
    532         return insertBefore - 1;
    533     },
    534 
    535     _findVisibleChunks: function(visibleFrom, visibleTo)
    536     {
    537         var from = this._findFirstVisibleChunkNumber(visibleFrom);
    538         for (var to = from + 1; to < this._textChunks.length; ++to) {
    539             if (this._textChunks[to].offsetTop >= visibleTo)
    540                 break;
    541         }
    542         return { start: from, end: to };
    543     },
    544 
    545     _findFirstVisibleLineNumber: function(visibleFrom)
    546     {
    547         var chunk = this._textChunks[this._findFirstVisibleChunkNumber(visibleFrom)];
    548         if (!chunk.expanded)
    549             return chunk.startLine;
    550 
    551         var lineNumbers = [];
    552         for (var i = 0; i < chunk.linesCount; ++i) {
    553             lineNumbers.push(chunk.startLine + i);
    554         }
    555 
    556         function compareLineRowOffsetTops(value, lineNumber)
    557         {
    558             var lineRow = chunk.getExpandedLineRow(lineNumber);
    559             return value < lineRow.offsetTop ? -1 : 1;
    560         }
    561         var insertBefore = insertionIndexForObjectInListSortedByFunction(visibleFrom, lineNumbers, compareLineRowOffsetTops);
    562         return lineNumbers[insertBefore - 1];
    563     },
    564 
    565     _repaintAll: function()
    566     {
    567         delete this._repaintAllTimer;
    568 
    569         if (this._paintCoalescingLevel || this._dirtyLines)
    570             return;
    571 
    572         var visibleFrom = this.element.scrollTop;
    573         var visibleTo = this.element.scrollTop + this.element.clientHeight;
    574 
    575         if (visibleTo) {
    576             var result = this._findVisibleChunks(visibleFrom, visibleTo);
    577             this._expandChunks(result.start, result.end);
    578         }
    579     },
    580 
    581     _expandChunks: function(fromIndex, toIndex)
    582     {
    583         // First collapse chunks to collect the DOM elements into a cache to reuse them later.
    584         for (var i = 0; i < fromIndex; ++i)
    585             this._textChunks[i].expanded = false;
    586         for (var i = toIndex; i < this._textChunks.length; ++i)
    587             this._textChunks[i].expanded = false;
    588         for (var i = fromIndex; i < toIndex; ++i)
    589             this._textChunks[i].expanded = true;
    590     },
    591 
    592     _totalHeight: function(firstElement, lastElement)
    593     {
    594         lastElement = (lastElement || firstElement).nextElementSibling;
    595         if (lastElement)
    596             return lastElement.offsetTop - firstElement.offsetTop;
    597 
    598         var offsetParent = firstElement.offsetParent;
    599         if (offsetParent && offsetParent.scrollHeight > offsetParent.clientHeight)
    600             return offsetParent.scrollHeight - firstElement.offsetTop;
    601 
    602         var total = 0;
    603         while (firstElement && firstElement !== lastElement) {
    604             total += firstElement.offsetHeight;
    605             firstElement = firstElement.nextElementSibling;
    606         }
    607         return total;
    608     },
    609 
    610     resize: function()
    611     {
    612         this._repaintAll();
    613     }
    614 }
    615 
    616 WebInspector.TextEditorGutterPanel = function(textModel, syncDecorationsForLineListener)
    617 {
    618     WebInspector.TextEditorChunkedPanel.call(this, textModel);
    619 
    620     this._syncDecorationsForLineListener = syncDecorationsForLineListener;
    621 
    622     this.element = document.createElement("div");
    623     this.element.className = "text-editor-lines";
    624 
    625     this._container = document.createElement("div");
    626     this._container.className = "inner-container";
    627     this.element.appendChild(this._container);
    628 
    629     this.element.addEventListener("scroll", this._scroll.bind(this), false);
    630 
    631     this.freeCachedElements();
    632     this._buildChunks();
    633 }
    634 
    635 WebInspector.TextEditorGutterPanel.prototype = {
    636     freeCachedElements: function()
    637     {
    638         this._cachedRows = [];
    639     },
    640 
    641     _createNewChunk: function(startLine, endLine)
    642     {
    643         return new WebInspector.TextEditorGutterChunk(this, startLine, endLine);
    644     },
    645 
    646     textChanged: function(oldRange, newRange)
    647     {
    648         this.beginDomUpdates();
    649 
    650         var linesDiff = newRange.linesCount - oldRange.linesCount;
    651         if (linesDiff) {
    652             // Remove old chunks (if needed).
    653             for (var chunkNumber = this._textChunks.length - 1; chunkNumber >= 0 ; --chunkNumber) {
    654                 var chunk = this._textChunks[chunkNumber];
    655                 if (chunk.startLine + chunk.linesCount <= this._textModel.linesCount)
    656                     break;
    657                 chunk.expanded = false;
    658                 this._container.removeChild(chunk.element);
    659             }
    660             this._textChunks.length = chunkNumber + 1;
    661 
    662             // Add new chunks (if needed).
    663             var totalLines = 0;
    664             if (this._textChunks.length) {
    665                 var lastChunk = this._textChunks[this._textChunks.length - 1];
    666                 totalLines = lastChunk.startLine + lastChunk.linesCount;
    667             }
    668             for (var i = totalLines; i < this._textModel.linesCount; i += this._defaultChunkSize) {
    669                 var chunk = this._createNewChunk(i, i + this._defaultChunkSize);
    670                 this._textChunks.push(chunk);
    671                 this._container.appendChild(chunk.element);
    672             }
    673             this._repaintAll();
    674         } else {
    675             // Decorations may have been removed, so we may have to sync those lines.
    676             var chunkNumber = this._chunkNumberForLine(newRange.startLine);
    677             var chunk = this._textChunks[chunkNumber];
    678             while (chunk && chunk.startLine <= newRange.endLine) {
    679                 if (chunk.linesCount === 1)
    680                     this._syncDecorationsForLineListener(chunk.startLine);
    681                 chunk = this._textChunks[++chunkNumber];
    682             }
    683         }
    684 
    685         this.endDomUpdates();
    686     },
    687 
    688     syncClientHeight: function(clientHeight)
    689     {
    690         if (this.element.offsetHeight > clientHeight)
    691             this._container.style.setProperty("padding-bottom", (this.element.offsetHeight - clientHeight) + "px");
    692         else
    693             this._container.style.removeProperty("padding-bottom");
    694     }
    695 }
    696 
    697 WebInspector.TextEditorGutterPanel.prototype.__proto__ = WebInspector.TextEditorChunkedPanel.prototype;
    698 
    699 WebInspector.TextEditorGutterChunk = function(textViewer, startLine, endLine)
    700 {
    701     this._textViewer = textViewer;
    702     this._textModel = textViewer._textModel;
    703 
    704     this.startLine = startLine;
    705     endLine = Math.min(this._textModel.linesCount, endLine);
    706     this.linesCount = endLine - startLine;
    707 
    708     this._expanded = false;
    709 
    710     this.element = document.createElement("div");
    711     this.element.lineNumber = startLine;
    712     this.element.className = "webkit-line-number";
    713 
    714     if (this.linesCount === 1) {
    715         // Single line chunks are typically created for decorations. Host line number in
    716         // the sub-element in order to allow flexible border / margin management.
    717         var innerSpan = document.createElement("span");
    718         innerSpan.className = "webkit-line-number-inner";
    719         innerSpan.textContent = startLine + 1;
    720         var outerSpan = document.createElement("div");
    721         outerSpan.className = "webkit-line-number-outer";
    722         outerSpan.appendChild(innerSpan);
    723         this.element.appendChild(outerSpan);
    724     } else {
    725         var lineNumbers = [];
    726         for (var i = startLine; i < endLine; ++i)
    727             lineNumbers.push(i + 1);
    728         this.element.textContent = lineNumbers.join("\n");
    729     }
    730 }
    731 
    732 WebInspector.TextEditorGutterChunk.prototype = {
    733     addDecoration: function(decoration)
    734     {
    735         this._textViewer.beginDomUpdates();
    736         if (typeof decoration === "string")
    737             this.element.addStyleClass(decoration);
    738         this._textViewer.endDomUpdates();
    739     },
    740 
    741     removeDecoration: function(decoration)
    742     {
    743         this._textViewer.beginDomUpdates();
    744         if (typeof decoration === "string")
    745             this.element.removeStyleClass(decoration);
    746         this._textViewer.endDomUpdates();
    747     },
    748 
    749     get expanded()
    750     {
    751         return this._expanded;
    752     },
    753 
    754     set expanded(expanded)
    755     {
    756         if (this.linesCount === 1)
    757             this._textViewer._syncDecorationsForLineListener(this.startLine);
    758 
    759         if (this._expanded === expanded)
    760             return;
    761 
    762         this._expanded = expanded;
    763 
    764         if (this.linesCount === 1)
    765             return;
    766 
    767         this._textViewer.beginDomUpdates();
    768 
    769         if (expanded) {
    770             this._expandedLineRows = [];
    771             var parentElement = this.element.parentElement;
    772             for (var i = this.startLine; i < this.startLine + this.linesCount; ++i) {
    773                 var lineRow = this._createRow(i);
    774                 parentElement.insertBefore(lineRow, this.element);
    775                 this._expandedLineRows.push(lineRow);
    776             }
    777             parentElement.removeChild(this.element);
    778         } else {
    779             var elementInserted = false;
    780             for (var i = 0; i < this._expandedLineRows.length; ++i) {
    781                 var lineRow = this._expandedLineRows[i];
    782                 var parentElement = lineRow.parentElement;
    783                 if (parentElement) {
    784                     if (!elementInserted) {
    785                         elementInserted = true;
    786                         parentElement.insertBefore(this.element, lineRow);
    787                     }
    788                     parentElement.removeChild(lineRow);
    789                 }
    790                 this._textViewer._cachedRows.push(lineRow);
    791             }
    792             delete this._expandedLineRows;
    793         }
    794 
    795         this._textViewer.endDomUpdates();
    796     },
    797 
    798     get height()
    799     {
    800         if (!this._expandedLineRows)
    801             return this._textViewer._totalHeight(this.element);
    802         return this._textViewer._totalHeight(this._expandedLineRows[0], this._expandedLineRows[this._expandedLineRows.length - 1]);
    803     },
    804 
    805     get offsetTop()
    806     {
    807         return (this._expandedLineRows && this._expandedLineRows.length) ? this._expandedLineRows[0].offsetTop : this.element.offsetTop;
    808     },
    809 
    810     _createRow: function(lineNumber)
    811     {
    812         var lineRow = this._textViewer._cachedRows.pop() || document.createElement("div");
    813         lineRow.lineNumber = lineNumber;
    814         lineRow.className = "webkit-line-number";
    815         lineRow.textContent = lineNumber + 1;
    816         return lineRow;
    817     }
    818 }
    819 
    820 WebInspector.TextEditorMainPanel = function(textModel, url, syncScrollListener, syncDecorationsForLineListener, enterTextChangeMode, exitTextChangeMode)
    821 {
    822     WebInspector.TextEditorChunkedPanel.call(this, textModel);
    823 
    824     this._syncScrollListener = syncScrollListener;
    825     this._syncDecorationsForLineListener = syncDecorationsForLineListener;
    826     this._enterTextChangeMode = enterTextChangeMode;
    827     this._exitTextChangeMode = exitTextChangeMode;
    828 
    829     this._url = url;
    830     this._highlighter = new WebInspector.TextEditorHighlighter(textModel, this._highlightDataReady.bind(this));
    831     this._readOnly = true;
    832 
    833     this.element = document.createElement("div");
    834     this.element.className = "text-editor-contents";
    835     this.element.tabIndex = 0;
    836 
    837     this._container = document.createElement("div");
    838     this._container.className = "inner-container";
    839     this._container.tabIndex = 0;
    840     this.element.appendChild(this._container);
    841 
    842     this.element.addEventListener("scroll", this._scroll.bind(this), false);
    843 
    844     // In WebKit the DOMNodeRemoved event is fired AFTER the node is removed, thus it should be
    845     // attached to all DOM nodes that we want to track. Instead, we attach the DOMNodeRemoved
    846     // listeners only on the line rows, and use DOMSubtreeModified to track node removals inside
    847     // the line rows. For more info see: https://bugs.webkit.org/show_bug.cgi?id=55666
    848     this._handleDOMUpdatesCallback = this._handleDOMUpdates.bind(this);
    849     this._container.addEventListener("DOMCharacterDataModified", this._handleDOMUpdatesCallback, false);
    850     this._container.addEventListener("DOMNodeInserted", this._handleDOMUpdatesCallback, false);
    851     this._container.addEventListener("DOMSubtreeModified", this._handleDOMUpdatesCallback, false);
    852 
    853     this.freeCachedElements();
    854     this._buildChunks();
    855 }
    856 
    857 WebInspector.TextEditorMainPanel.prototype = {
    858     set mimeType(mimeType)
    859     {
    860         this._highlighter.mimeType = mimeType;
    861     },
    862 
    863     set readOnly(readOnly)
    864     {
    865         if (this._readOnly === readOnly)
    866             return;
    867 
    868         this.beginDomUpdates();
    869         this._readOnly = readOnly;
    870         if (this._readOnly)
    871             this._container.removeStyleClass("text-editor-editable");
    872         else
    873             this._container.addStyleClass("text-editor-editable");
    874         this.endDomUpdates();
    875     },
    876 
    877     get readOnly()
    878     {
    879         return this._readOnly;
    880     },
    881 
    882     markAndRevealRange: function(range)
    883     {
    884         if (this._rangeToMark) {
    885             var markedLine = this._rangeToMark.startLine;
    886             delete this._rangeToMark;
    887             // Remove the marked region immediately.
    888             if (!this._dirtyLines) {
    889                 this.beginDomUpdates();
    890                 var chunk = this.chunkForLine(markedLine);
    891                 var wasExpanded = chunk.expanded;
    892                 chunk.expanded = false;
    893                 chunk.updateCollapsedLineRow();
    894                 chunk.expanded = wasExpanded;
    895                 this.endDomUpdates();
    896             } else
    897                 this._paintLines(markedLine, markedLine + 1);
    898         }
    899 
    900         if (range) {
    901             this._rangeToMark = range;
    902             this.revealLine(range.startLine);
    903             var chunk = this.makeLineAChunk(range.startLine);
    904             this._paintLine(chunk.element);
    905             if (this._markedRangeElement)
    906                 this._markedRangeElement.scrollIntoViewIfNeeded();
    907         }
    908         delete this._markedRangeElement;
    909     },
    910 
    911     highlightLine: function(lineNumber)
    912     {
    913         this.clearLineHighlight();
    914         this._highlightedLine = lineNumber;
    915         this.revealLine(lineNumber);
    916         this.addDecoration(lineNumber, "webkit-highlighted-line");
    917     },
    918 
    919     clearLineHighlight: function()
    920     {
    921         if (typeof this._highlightedLine === "number") {
    922             this.removeDecoration(this._highlightedLine, "webkit-highlighted-line");
    923             delete this._highlightedLine;
    924         }
    925     },
    926 
    927     freeCachedElements: function()
    928     {
    929         this._cachedSpans = [];
    930         this._cachedTextNodes = [];
    931         this._cachedRows = [];
    932     },
    933 
    934     handleUndoRedo: function(redo)
    935     {
    936         if (this._readOnly || this._dirtyLines)
    937             return false;
    938 
    939         this.beginUpdates();
    940         this._enterTextChangeMode();
    941 
    942         var callback = function(oldRange, newRange) {
    943             this._exitTextChangeMode(oldRange, newRange);
    944             this._enterTextChangeMode();
    945         }.bind(this);
    946 
    947         var range = redo ? this._textModel.redo(callback) : this._textModel.undo(callback);
    948         if (range)
    949             this._setCaretLocation(range.endLine, range.endColumn, true);
    950 
    951         this._exitTextChangeMode(null, null);
    952         this.endUpdates();
    953 
    954         return true;
    955     },
    956 
    957     handleTabKeyPress: function(shiftKey)
    958     {
    959         if (this._readOnly || this._dirtyLines)
    960             return false;
    961 
    962         var selection = this._getSelection();
    963         if (!selection)
    964             return false;
    965 
    966         if (shiftKey)
    967             return true;
    968 
    969         this.beginUpdates();
    970         this._enterTextChangeMode();
    971 
    972         var range = selection;
    973         if (range.startLine > range.endLine || (range.startLine === range.endLine && range.startColumn > range.endColumn))
    974             range = new WebInspector.TextRange(range.endLine, range.endColumn, range.startLine, range.startColumn);
    975 
    976         var newRange = this._setText(range, "\t");
    977 
    978         this._exitTextChangeMode(range, newRange);
    979         this.endUpdates();
    980 
    981         this._setCaretLocation(newRange.endLine, newRange.endColumn, true);
    982         return true;
    983     },
    984 
    985     _splitChunkOnALine: function(lineNumber, chunkNumber)
    986     {
    987         var selection = this._getSelection();
    988         var chunk = WebInspector.TextEditorChunkedPanel.prototype._splitChunkOnALine.call(this, lineNumber, chunkNumber);
    989         this._restoreSelection(selection);
    990         return chunk;
    991     },
    992 
    993     _buildChunks: function()
    994     {
    995         for (var i = 0; i < this._textModel.linesCount; ++i)
    996             this._textModel.removeAttribute(i, "highlight");
    997 
    998         WebInspector.TextEditorChunkedPanel.prototype._buildChunks.call(this);
    999     },
   1000 
   1001     _createNewChunk: function(startLine, endLine)
   1002     {
   1003         return new WebInspector.TextEditorMainChunk(this, startLine, endLine);
   1004     },
   1005 
   1006     _expandChunks: function(fromIndex, toIndex)
   1007     {
   1008         var lastChunk = this._textChunks[toIndex - 1];
   1009         var lastVisibleLine = lastChunk.startLine + lastChunk.linesCount;
   1010 
   1011         var selection = this._getSelection();
   1012 
   1013         this._muteHighlightListener = true;
   1014         this._highlighter.highlight(lastVisibleLine);
   1015         delete this._muteHighlightListener;
   1016 
   1017         this._restorePaintLinesOperationsCredit();
   1018         WebInspector.TextEditorChunkedPanel.prototype._expandChunks.call(this, fromIndex, toIndex);
   1019         this._adjustPaintLinesOperationsRefreshValue();
   1020 
   1021         this._restoreSelection(selection);
   1022     },
   1023 
   1024     _highlightDataReady: function(fromLine, toLine)
   1025     {
   1026         if (this._muteHighlightListener)
   1027             return;
   1028         this._restorePaintLinesOperationsCredit();
   1029         this._paintLines(fromLine, toLine, true /*restoreSelection*/);
   1030     },
   1031 
   1032     _schedulePaintLines: function(startLine, endLine)
   1033     {
   1034         if (startLine >= endLine)
   1035             return;
   1036 
   1037         if (!this._scheduledPaintLines) {
   1038             this._scheduledPaintLines = [ { startLine: startLine, endLine: endLine } ];
   1039             this._paintScheduledLinesTimer = setTimeout(this._paintScheduledLines.bind(this), 0);
   1040         } else {
   1041             for (var i = 0; i < this._scheduledPaintLines.length; ++i) {
   1042                 var chunk = this._scheduledPaintLines[i];
   1043                 if (chunk.startLine <= endLine && chunk.endLine >= startLine) {
   1044                     chunk.startLine = Math.min(chunk.startLine, startLine);
   1045                     chunk.endLine = Math.max(chunk.endLine, endLine);
   1046                     return;
   1047                 }
   1048                 if (chunk.startLine > endLine) {
   1049                     this._scheduledPaintLines.splice(i, 0, { startLine: startLine, endLine: endLine });
   1050                     return;
   1051                 }
   1052             }
   1053             this._scheduledPaintLines.push({ startLine: startLine, endLine: endLine });
   1054         }
   1055     },
   1056 
   1057     _paintScheduledLines: function(skipRestoreSelection)
   1058     {
   1059         if (this._paintScheduledLinesTimer)
   1060             clearTimeout(this._paintScheduledLinesTimer);
   1061         delete this._paintScheduledLinesTimer;
   1062 
   1063         if (!this._scheduledPaintLines)
   1064             return;
   1065 
   1066         // Reschedule the timer if we can not paint the lines yet, or the user is scrolling.
   1067         if (this._dirtyLines || this._repaintAllTimer) {
   1068             this._paintScheduledLinesTimer = setTimeout(this._paintScheduledLines.bind(this), 50);
   1069             return;
   1070         }
   1071 
   1072         var scheduledPaintLines = this._scheduledPaintLines;
   1073         delete this._scheduledPaintLines;
   1074 
   1075         this._restorePaintLinesOperationsCredit();
   1076         this._paintLineChunks(scheduledPaintLines, !skipRestoreSelection);
   1077         this._adjustPaintLinesOperationsRefreshValue();
   1078     },
   1079 
   1080     _restorePaintLinesOperationsCredit: function()
   1081     {
   1082         if (!this._paintLinesOperationsRefreshValue)
   1083             this._paintLinesOperationsRefreshValue = 250;
   1084         this._paintLinesOperationsCredit = this._paintLinesOperationsRefreshValue;
   1085         this._paintLinesOperationsLastRefresh = Date.now();
   1086     },
   1087 
   1088     _adjustPaintLinesOperationsRefreshValue: function()
   1089     {
   1090         var operationsDone = this._paintLinesOperationsRefreshValue - this._paintLinesOperationsCredit;
   1091         if (operationsDone <= 0)
   1092             return;
   1093         var timePast = Date.now() - this._paintLinesOperationsLastRefresh;
   1094         if (timePast <= 0)
   1095             return;
   1096         // Make the synchronous CPU chunk for painting the lines 50 msec.
   1097         var value = Math.floor(operationsDone / timePast * 50);
   1098         this._paintLinesOperationsRefreshValue = Number.constrain(value, 150, 1500);
   1099     },
   1100 
   1101     _paintLines: function(fromLine, toLine, restoreSelection)
   1102     {
   1103         this._paintLineChunks([ { startLine: fromLine, endLine: toLine } ], restoreSelection);
   1104     },
   1105 
   1106     _paintLineChunks: function(lineChunks, restoreSelection)
   1107     {
   1108         // First, paint visible lines, so that in case of long lines we should start highlighting
   1109         // the visible area immediately, instead of waiting for the lines above the visible area.
   1110         var visibleFrom = this.element.scrollTop;
   1111         var firstVisibleLineNumber = this._findFirstVisibleLineNumber(visibleFrom);
   1112 
   1113         var chunk;
   1114         var selection;
   1115         var invisibleLineRows = [];
   1116         for (var i = 0; i < lineChunks.length; ++i) {
   1117             var lineChunk = lineChunks[i];
   1118             if (this._dirtyLines || this._scheduledPaintLines) {
   1119                 this._schedulePaintLines(lineChunk.startLine, lineChunk.endLine);
   1120                 continue;
   1121             }
   1122             for (var lineNumber = lineChunk.startLine; lineNumber < lineChunk.endLine; ++lineNumber) {
   1123                 if (!chunk || lineNumber < chunk.startLine || lineNumber >= chunk.startLine + chunk.linesCount)
   1124                     chunk = this.chunkForLine(lineNumber);
   1125                 var lineRow = chunk.getExpandedLineRow(lineNumber);
   1126                 if (!lineRow)
   1127                     continue;
   1128                 if (lineNumber < firstVisibleLineNumber) {
   1129                     invisibleLineRows.push(lineRow);
   1130                     continue;
   1131                 }
   1132                 if (restoreSelection && !selection)
   1133                     selection = this._getSelection();
   1134                 this._paintLine(lineRow);
   1135                 if (this._paintLinesOperationsCredit < 0) {
   1136                     this._schedulePaintLines(lineNumber + 1, lineChunk.endLine);
   1137                     break;
   1138                 }
   1139             }
   1140         }
   1141 
   1142         for (var i = 0; i < invisibleLineRows.length; ++i) {
   1143             if (restoreSelection && !selection)
   1144                 selection = this._getSelection();
   1145             this._paintLine(invisibleLineRows[i]);
   1146         }
   1147 
   1148         if (restoreSelection)
   1149             this._restoreSelection(selection);
   1150     },
   1151 
   1152     _paintLine: function(lineRow)
   1153     {
   1154         var lineNumber = lineRow.lineNumber;
   1155         if (this._dirtyLines) {
   1156             this._schedulePaintLines(lineNumber, lineNumber + 1);
   1157             return;
   1158         }
   1159 
   1160         this.beginDomUpdates();
   1161         try {
   1162             if (this._scheduledPaintLines || this._paintLinesOperationsCredit < 0) {
   1163                 this._schedulePaintLines(lineNumber, lineNumber + 1);
   1164                 return;
   1165             }
   1166 
   1167             var highlight = this._textModel.getAttribute(lineNumber, "highlight");
   1168             if (!highlight)
   1169                 return;
   1170 
   1171             lineRow.removeChildren();
   1172             var line = this._textModel.line(lineNumber);
   1173             if (!line)
   1174                 lineRow.appendChild(document.createElement("br"));
   1175 
   1176             var plainTextStart = -1;
   1177             for (var j = 0; j < line.length;) {
   1178                 if (j > 1000) {
   1179                     // This line is too long - do not waste cycles on minified js highlighting.
   1180                     if (plainTextStart === -1)
   1181                         plainTextStart = j;
   1182                     break;
   1183                 }
   1184                 var attribute = highlight[j];
   1185                 if (!attribute || !attribute.tokenType) {
   1186                     if (plainTextStart === -1)
   1187                         plainTextStart = j;
   1188                     j++;
   1189                 } else {
   1190                     if (plainTextStart !== -1) {
   1191                         this._appendTextNode(lineRow, line.substring(plainTextStart, j));
   1192                         plainTextStart = -1;
   1193                         --this._paintLinesOperationsCredit;
   1194                     }
   1195                     this._appendSpan(lineRow, line.substring(j, j + attribute.length), attribute.tokenType);
   1196                     j += attribute.length;
   1197                     --this._paintLinesOperationsCredit;
   1198                 }
   1199             }
   1200             if (plainTextStart !== -1) {
   1201                 this._appendTextNode(lineRow, line.substring(plainTextStart, line.length));
   1202                 --this._paintLinesOperationsCredit;
   1203             }
   1204             if (lineRow.decorationsElement)
   1205                 lineRow.appendChild(lineRow.decorationsElement);
   1206         } finally {
   1207             if (this._rangeToMark && this._rangeToMark.startLine === lineNumber)
   1208                 this._markedRangeElement = highlightSearchResult(lineRow, this._rangeToMark.startColumn, this._rangeToMark.endColumn - this._rangeToMark.startColumn);
   1209             this.endDomUpdates();
   1210         }
   1211     },
   1212 
   1213     _releaseLinesHighlight: function(lineRow)
   1214     {
   1215         if (!lineRow)
   1216             return;
   1217         if ("spans" in lineRow) {
   1218             var spans = lineRow.spans;
   1219             for (var j = 0; j < spans.length; ++j)
   1220                 this._cachedSpans.push(spans[j]);
   1221             delete lineRow.spans;
   1222         }
   1223         if ("textNodes" in lineRow) {
   1224             var textNodes = lineRow.textNodes;
   1225             for (var j = 0; j < textNodes.length; ++j)
   1226                 this._cachedTextNodes.push(textNodes[j]);
   1227             delete lineRow.textNodes;
   1228         }
   1229         this._cachedRows.push(lineRow);
   1230     },
   1231 
   1232     _getSelection: function()
   1233     {
   1234         var selection = window.getSelection();
   1235         if (!selection.rangeCount)
   1236             return null;
   1237         var selectionRange = selection.getRangeAt(0);
   1238         // Selection may be outside of the viewer.
   1239         if (!this._container.isAncestor(selectionRange.startContainer) || !this._container.isAncestor(selectionRange.endContainer))
   1240             return null;
   1241         var start = this._selectionToPosition(selectionRange.startContainer, selectionRange.startOffset);
   1242         var end = selectionRange.collapsed ? start : this._selectionToPosition(selectionRange.endContainer, selectionRange.endOffset);
   1243         if (selection.anchorNode === selectionRange.startContainer && selection.anchorOffset === selectionRange.startOffset)
   1244             return new WebInspector.TextRange(start.line, start.column, end.line, end.column);
   1245         else
   1246             return new WebInspector.TextRange(end.line, end.column, start.line, start.column);
   1247     },
   1248 
   1249     _restoreSelection: function(range, scrollIntoView)
   1250     {
   1251         if (!range)
   1252             return;
   1253         var start = this._positionToSelection(range.startLine, range.startColumn);
   1254         var end = range.isEmpty() ? start : this._positionToSelection(range.endLine, range.endColumn);
   1255         window.getSelection().setBaseAndExtent(start.container, start.offset, end.container, end.offset);
   1256 
   1257         if (scrollIntoView) {
   1258             for (var node = end.container; node; node = node.parentElement) {
   1259                 if (node.scrollIntoViewIfNeeded) {
   1260                     node.scrollIntoViewIfNeeded();
   1261                     break;
   1262                 }
   1263             }
   1264         }
   1265     },
   1266 
   1267     _setCaretLocation: function(line, column, scrollIntoView)
   1268     {
   1269         var range = new WebInspector.TextRange(line, column, line, column);
   1270         this._restoreSelection(range, scrollIntoView);
   1271     },
   1272 
   1273     _selectionToPosition: function(container, offset)
   1274     {
   1275         if (container === this._container && offset === 0)
   1276             return { line: 0, column: 0 };
   1277         if (container === this._container && offset === 1)
   1278             return { line: this._textModel.linesCount - 1, column: this._textModel.lineLength(this._textModel.linesCount - 1) };
   1279 
   1280         var lineRow = this._enclosingLineRowOrSelf(container);
   1281         var lineNumber = lineRow.lineNumber;
   1282         if (container === lineRow && offset === 0)
   1283             return { line: lineNumber, column: 0 };
   1284 
   1285         // This may be chunk and chunks may contain \n.
   1286         var column = 0;
   1287         var node = lineRow.nodeType === Node.TEXT_NODE ? lineRow : lineRow.traverseNextTextNode(lineRow);
   1288         while (node && node !== container) {
   1289             var text = node.textContent;
   1290             for (var i = 0; i < text.length; ++i) {
   1291                 if (text.charAt(i) === "\n") {
   1292                     lineNumber++;
   1293                     column = 0;
   1294                 } else
   1295                     column++;
   1296             }
   1297             node = node.traverseNextTextNode(lineRow);
   1298         }
   1299 
   1300         if (node === container && offset) {
   1301             var text = node.textContent;
   1302             for (var i = 0; i < offset; ++i) {
   1303                 if (text.charAt(i) === "\n") {
   1304                     lineNumber++;
   1305                     column = 0;
   1306                 } else
   1307                     column++;
   1308             }
   1309         }
   1310         return { line: lineNumber, column: column };
   1311     },
   1312 
   1313     _positionToSelection: function(line, column)
   1314     {
   1315         var chunk = this.chunkForLine(line);
   1316         // One-lined collapsed chunks may still stay highlighted.
   1317         var lineRow = chunk.linesCount === 1 ? chunk.element : chunk.getExpandedLineRow(line);
   1318         if (lineRow)
   1319             var rangeBoundary = lineRow.rangeBoundaryForOffset(column);
   1320         else {
   1321             var offset = column;
   1322             for (var i = chunk.startLine; i < line; ++i)
   1323                 offset += this._textModel.lineLength(i) + 1; // \n
   1324             lineRow = chunk.element;
   1325             if (lineRow.firstChild)
   1326                 var rangeBoundary = { container: lineRow.firstChild, offset: offset };
   1327             else
   1328                 var rangeBoundary = { container: lineRow, offset: 0 };
   1329         }
   1330         return rangeBoundary;
   1331     },
   1332 
   1333     _enclosingLineRowOrSelf: function(element)
   1334     {
   1335         var lineRow = element.enclosingNodeOrSelfWithClass("webkit-line-content");
   1336         if (lineRow)
   1337             return lineRow;
   1338         for (var lineRow = element; lineRow; lineRow = lineRow.parentElement) {
   1339             if (lineRow.parentElement === this._container)
   1340                 return lineRow;
   1341         }
   1342         return null;
   1343     },
   1344 
   1345     _appendSpan: function(element, content, className)
   1346     {
   1347         if (className === "html-resource-link" || className === "html-external-link") {
   1348             element.appendChild(this._createLink(content, className === "html-external-link"));
   1349             return;
   1350         }
   1351 
   1352         var span = this._cachedSpans.pop() || document.createElement("span");
   1353         span.className = "webkit-" + className;
   1354         span.textContent = content;
   1355         element.appendChild(span);
   1356         if (!("spans" in element))
   1357             element.spans = [];
   1358         element.spans.push(span);
   1359     },
   1360 
   1361     _appendTextNode: function(element, text)
   1362     {
   1363         var textNode = this._cachedTextNodes.pop();
   1364         if (textNode)
   1365             textNode.nodeValue = text;
   1366         else
   1367             textNode = document.createTextNode(text);
   1368         element.appendChild(textNode);
   1369         if (!("textNodes" in element))
   1370             element.textNodes = [];
   1371         element.textNodes.push(textNode);
   1372     },
   1373 
   1374     _createLink: function(content, isExternal)
   1375     {
   1376         var quote = content.charAt(0);
   1377         if (content.length > 1 && (quote === "\"" ||   quote === "'"))
   1378             content = content.substring(1, content.length - 1);
   1379         else
   1380             quote = null;
   1381 
   1382         var a = WebInspector.linkifyURLAsNode(this._rewriteHref(content), content, null, isExternal);
   1383         var span = document.createElement("span");
   1384         span.className = "webkit-html-attribute-value";
   1385         if (quote)
   1386             span.appendChild(document.createTextNode(quote));
   1387         span.appendChild(a);
   1388         if (quote)
   1389             span.appendChild(document.createTextNode(quote));
   1390         return span;
   1391     },
   1392 
   1393     _rewriteHref: function(hrefValue, isExternal)
   1394     {
   1395         if (!this._url || !hrefValue || hrefValue.indexOf("://") > 0)
   1396             return hrefValue;
   1397         return WebInspector.completeURL(this._url, hrefValue);
   1398     },
   1399 
   1400     _handleDOMUpdates: function(e)
   1401     {
   1402         if (this._domUpdateCoalescingLevel)
   1403             return;
   1404 
   1405         var target = e.target;
   1406         if (target === this._container)
   1407             return;
   1408 
   1409         var lineRow = this._enclosingLineRowOrSelf(target);
   1410         if (!lineRow)
   1411             return;
   1412 
   1413         if (lineRow.decorationsElement && (lineRow.decorationsElement === target || lineRow.decorationsElement.isAncestor(target))) {
   1414             if (this._syncDecorationsForLineListener)
   1415                 this._syncDecorationsForLineListener(lineRow.lineNumber);
   1416             return;
   1417         }
   1418 
   1419         if (this._readOnly)
   1420             return;
   1421 
   1422         if (target === lineRow && e.type === "DOMNodeInserted") {
   1423             // Ensure that the newly inserted line row has no lineNumber.
   1424             delete lineRow.lineNumber;
   1425         }
   1426 
   1427         var startLine = 0;
   1428         for (var row = lineRow; row; row = row.previousSibling) {
   1429             if (typeof row.lineNumber === "number") {
   1430                 startLine = row.lineNumber;
   1431                 break;
   1432             }
   1433         }
   1434 
   1435         var endLine = startLine + 1;
   1436         for (var row = lineRow.nextSibling; row; row = row.nextSibling) {
   1437             if (typeof row.lineNumber === "number" && row.lineNumber > startLine) {
   1438                 endLine = row.lineNumber;
   1439                 break;
   1440             }
   1441         }
   1442 
   1443         if (target === lineRow && e.type === "DOMNodeRemoved") {
   1444             // Now this will no longer be valid.
   1445             delete lineRow.lineNumber;
   1446         }
   1447 
   1448         if (this._dirtyLines) {
   1449             this._dirtyLines.start = Math.min(this._dirtyLines.start, startLine);
   1450             this._dirtyLines.end = Math.max(this._dirtyLines.end, endLine);
   1451         } else {
   1452             this._dirtyLines = { start: startLine, end: endLine };
   1453             setTimeout(this._applyDomUpdates.bind(this), 0);
   1454             // Remove marked ranges, if any.
   1455             this.markAndRevealRange(null);
   1456         }
   1457     },
   1458 
   1459     _applyDomUpdates: function()
   1460     {
   1461         if (!this._dirtyLines)
   1462             return;
   1463 
   1464         // Check if the editor had been set readOnly by the moment when this async callback got executed.
   1465         if (this._readOnly) {
   1466             delete this._dirtyLines;
   1467             return;
   1468         }
   1469 
   1470         // This is a "foreign" call outside of this class. Should be before we delete the dirty lines flag.
   1471         this._enterTextChangeMode();
   1472 
   1473         var dirtyLines = this._dirtyLines;
   1474         delete this._dirtyLines;
   1475 
   1476         var firstChunkNumber = this._chunkNumberForLine(dirtyLines.start);
   1477         var startLine = this._textChunks[firstChunkNumber].startLine;
   1478         var endLine = this._textModel.linesCount;
   1479 
   1480         // Collect lines.
   1481         var firstLineRow;
   1482         if (firstChunkNumber) {
   1483             var chunk = this._textChunks[firstChunkNumber - 1];
   1484             firstLineRow = chunk.expanded ? chunk.getExpandedLineRow(chunk.startLine + chunk.linesCount - 1) : chunk.element;
   1485             firstLineRow = firstLineRow.nextSibling;
   1486         } else
   1487             firstLineRow = this._container.firstChild;
   1488 
   1489         var lines = [];
   1490         for (var lineRow = firstLineRow; lineRow; lineRow = lineRow.nextSibling) {
   1491             if (typeof lineRow.lineNumber === "number" && lineRow.lineNumber >= dirtyLines.end) {
   1492                 endLine = lineRow.lineNumber;
   1493                 break;
   1494             }
   1495             // Update with the newest lineNumber, so that the call to the _getSelection method below should work.
   1496             lineRow.lineNumber = startLine + lines.length;
   1497             this._collectLinesFromDiv(lines, lineRow);
   1498         }
   1499 
   1500         // Try to decrease the range being replaced, if possible.
   1501         var startOffset = 0;
   1502         while (startLine < dirtyLines.start && startOffset < lines.length) {
   1503             if (this._textModel.line(startLine) !== lines[startOffset])
   1504                 break;
   1505             ++startOffset;
   1506             ++startLine;
   1507         }
   1508 
   1509         var endOffset = lines.length;
   1510         while (endLine > dirtyLines.end && endOffset > startOffset) {
   1511             if (this._textModel.line(endLine - 1) !== lines[endOffset - 1])
   1512                 break;
   1513             --endOffset;
   1514             --endLine;
   1515         }
   1516 
   1517         lines = lines.slice(startOffset, endOffset);
   1518 
   1519         // Try to decrease the range being replaced by column offsets, if possible.
   1520         var startColumn = 0;
   1521         var endColumn = this._textModel.lineLength(endLine - 1);
   1522         if (lines.length > 0) {
   1523             var line1 = this._textModel.line(startLine);
   1524             var line2 = lines[0];
   1525             while (line1[startColumn] && line1[startColumn] === line2[startColumn])
   1526                 ++startColumn;
   1527             lines[0] = line2.substring(startColumn);
   1528 
   1529             var line1 = this._textModel.line(endLine - 1);
   1530             var line2 = lines[lines.length - 1];
   1531             for (var i = 0; i < endColumn && i < line2.length; ++i) {
   1532                 if (startLine === endLine - 1 && endColumn - i <= startColumn)
   1533                     break;
   1534                 if (line1[endColumn - i - 1] !== line2[line2.length - i - 1])
   1535                     break;
   1536             }
   1537             if (i) {
   1538                 endColumn -= i;
   1539                 lines[lines.length - 1] = line2.substring(0, line2.length - i);
   1540             }
   1541         }
   1542 
   1543         var selection = this._getSelection();
   1544 
   1545         if (lines.length === 0 && endLine < this._textModel.linesCount)
   1546             var oldRange = new WebInspector.TextRange(startLine, 0, endLine, 0);
   1547         else if (lines.length === 0 && startLine > 0)
   1548             var oldRange = new WebInspector.TextRange(startLine - 1, this._textModel.lineLength(startLine - 1), endLine - 1, this._textModel.lineLength(endLine - 1));
   1549         else
   1550             var oldRange = new WebInspector.TextRange(startLine, startColumn, endLine - 1, endColumn);
   1551 
   1552         var newRange = this._setText(oldRange, lines.join("\n"));
   1553 
   1554         this._paintScheduledLines(true);
   1555         this._restoreSelection(selection);
   1556 
   1557         this._exitTextChangeMode(oldRange, newRange);
   1558     },
   1559 
   1560     textChanged: function(oldRange, newRange)
   1561     {
   1562         this.beginDomUpdates();
   1563         this._removeDecorationsInRange(oldRange);
   1564         this._updateChunksForRanges(oldRange, newRange);
   1565         this._updateHighlightsForRange(newRange);
   1566         this.endDomUpdates();
   1567     },
   1568 
   1569     _setText: function(range, text)
   1570     {
   1571         if (this._lastEditedRange && (!text || text.indexOf("\n") !== -1 || this._lastEditedRange.endLine !== range.startLine || this._lastEditedRange.endColumn !== range.startColumn))
   1572             this._textModel.markUndoableState();
   1573 
   1574         var newRange = this._textModel.setText(range, text);
   1575         this._lastEditedRange = newRange;
   1576 
   1577         return newRange;
   1578     },
   1579 
   1580     _removeDecorationsInRange: function(range)
   1581     {
   1582         for (var i = this._chunkNumberForLine(range.startLine); i < this._textChunks.length; ++i) {
   1583             var chunk = this._textChunks[i];
   1584             if (chunk.startLine > range.endLine)
   1585                 break;
   1586             chunk.removeAllDecorations();
   1587         }
   1588     },
   1589 
   1590     _updateChunksForRanges: function(oldRange, newRange)
   1591     {
   1592         // Update the chunks in range: firstChunkNumber <= index <= lastChunkNumber
   1593         var firstChunkNumber = this._chunkNumberForLine(oldRange.startLine);
   1594         var lastChunkNumber = firstChunkNumber;
   1595         while (lastChunkNumber + 1 < this._textChunks.length) {
   1596             if (this._textChunks[lastChunkNumber + 1].startLine > oldRange.endLine)
   1597                 break;
   1598             ++lastChunkNumber;
   1599         }
   1600 
   1601         var startLine = this._textChunks[firstChunkNumber].startLine;
   1602         var linesCount = this._textChunks[lastChunkNumber].startLine + this._textChunks[lastChunkNumber].linesCount - startLine;
   1603         var linesDiff = newRange.linesCount - oldRange.linesCount;
   1604         linesCount += linesDiff;
   1605 
   1606         if (linesDiff) {
   1607             // Lines shifted, update the line numbers of the chunks below.
   1608             for (var chunkNumber = lastChunkNumber + 1; chunkNumber < this._textChunks.length; ++chunkNumber)
   1609                 this._textChunks[chunkNumber].startLine += linesDiff;
   1610         }
   1611 
   1612         var firstLineRow;
   1613         if (firstChunkNumber) {
   1614             var chunk = this._textChunks[firstChunkNumber - 1];
   1615             firstLineRow = chunk.expanded ? chunk.getExpandedLineRow(chunk.startLine + chunk.linesCount - 1) : chunk.element;
   1616             firstLineRow = firstLineRow.nextSibling;
   1617         } else
   1618             firstLineRow = this._container.firstChild;
   1619 
   1620         // Most frequent case: a chunk remained the same.
   1621         for (var chunkNumber = firstChunkNumber; chunkNumber <= lastChunkNumber; ++chunkNumber) {
   1622             var chunk = this._textChunks[chunkNumber];
   1623             if (chunk.startLine + chunk.linesCount > this._textModel.linesCount)
   1624                 break;
   1625             var lineNumber = chunk.startLine;
   1626             for (var lineRow = firstLineRow; lineRow && lineNumber < chunk.startLine + chunk.linesCount; lineRow = lineRow.nextSibling) {
   1627                 if (lineRow.lineNumber !== lineNumber || lineRow !== chunk.getExpandedLineRow(lineNumber) || lineRow.textContent !== this._textModel.line(lineNumber) || !lineRow.firstChild)
   1628                     break;
   1629                 ++lineNumber;
   1630             }
   1631             if (lineNumber < chunk.startLine + chunk.linesCount)
   1632                 break;
   1633             chunk.updateCollapsedLineRow();
   1634             ++firstChunkNumber;
   1635             firstLineRow = lineRow;
   1636             startLine += chunk.linesCount;
   1637             linesCount -= chunk.linesCount;
   1638         }
   1639 
   1640         if (firstChunkNumber > lastChunkNumber && linesCount === 0)
   1641             return;
   1642 
   1643         // Maybe merge with the next chunk, so that we should not create 1-sized chunks when appending new lines one by one.
   1644         var chunk = this._textChunks[lastChunkNumber + 1];
   1645         var linesInLastChunk = linesCount % this._defaultChunkSize;
   1646         if (chunk && !chunk.decorated && linesInLastChunk > 0 && linesInLastChunk + chunk.linesCount <= this._defaultChunkSize) {
   1647             ++lastChunkNumber;
   1648             linesCount += chunk.linesCount;
   1649         }
   1650 
   1651         var scrollTop = this.element.scrollTop;
   1652         var scrollLeft = this.element.scrollLeft;
   1653 
   1654         // Delete all DOM elements that were either controlled by the old chunks, or have just been inserted.
   1655         var firstUnmodifiedLineRow = null;
   1656         var chunk = this._textChunks[lastChunkNumber + 1];
   1657         if (chunk) {
   1658             firstUnmodifiedLineRow = chunk.expanded ? chunk.getExpandedLineRow(chunk.startLine) : chunk.element;
   1659         }
   1660         while (firstLineRow && firstLineRow !== firstUnmodifiedLineRow) {
   1661             var lineRow = firstLineRow;
   1662             firstLineRow = firstLineRow.nextSibling;
   1663             this._container.removeChild(lineRow);
   1664         }
   1665 
   1666         // Replace old chunks with the new ones.
   1667         for (var chunkNumber = firstChunkNumber; linesCount > 0; ++chunkNumber) {
   1668             var chunkLinesCount = Math.min(this._defaultChunkSize, linesCount);
   1669             var newChunk = this._createNewChunk(startLine, startLine + chunkLinesCount);
   1670             this._container.insertBefore(newChunk.element, firstUnmodifiedLineRow);
   1671 
   1672             if (chunkNumber <= lastChunkNumber)
   1673                 this._textChunks[chunkNumber] = newChunk;
   1674             else
   1675                 this._textChunks.splice(chunkNumber, 0, newChunk);
   1676             startLine += chunkLinesCount;
   1677             linesCount -= chunkLinesCount;
   1678         }
   1679         if (chunkNumber <= lastChunkNumber)
   1680             this._textChunks.splice(chunkNumber, lastChunkNumber - chunkNumber + 1);
   1681 
   1682         this.element.scrollTop = scrollTop;
   1683         this.element.scrollLeft = scrollLeft;
   1684     },
   1685 
   1686     _updateHighlightsForRange: function(range)
   1687     {
   1688         var visibleFrom = this.element.scrollTop;
   1689         var visibleTo = this.element.scrollTop + this.element.clientHeight;
   1690 
   1691         var result = this._findVisibleChunks(visibleFrom, visibleTo);
   1692         var chunk = this._textChunks[result.end - 1];
   1693         var lastVisibleLine = chunk.startLine + chunk.linesCount;
   1694 
   1695         lastVisibleLine = Math.max(lastVisibleLine, range.endLine + 1);
   1696         lastVisibleLine = Math.min(lastVisibleLine, this._textModel.linesCount);
   1697 
   1698         var updated = this._highlighter.updateHighlight(range.startLine, lastVisibleLine);
   1699         if (!updated) {
   1700             // Highlights for the chunks below are invalid, so just collapse them.
   1701             for (var i = this._chunkNumberForLine(range.startLine); i < this._textChunks.length; ++i)
   1702                 this._textChunks[i].expanded = false;
   1703         }
   1704 
   1705         this._repaintAll();
   1706     },
   1707 
   1708     _collectLinesFromDiv: function(lines, element)
   1709     {
   1710         var textContents = [];
   1711         var node = element.nodeType === Node.TEXT_NODE ? element : element.traverseNextNode(element);
   1712         while (node) {
   1713             if (element.decorationsElement === node) {
   1714                 node = node.nextSibling;
   1715                 continue;
   1716             }
   1717             if (node.nodeName.toLowerCase() === "br")
   1718                 textContents.push("\n");
   1719             else if (node.nodeType === Node.TEXT_NODE)
   1720                 textContents.push(node.textContent);
   1721             node = node.traverseNextNode(element);
   1722         }
   1723 
   1724         var textContent = textContents.join("");
   1725         // The last \n (if any) does not "count" in a DIV.
   1726         textContent = textContent.replace(/\n$/, "");
   1727 
   1728         textContents = textContent.split("\n");
   1729         for (var i = 0; i < textContents.length; ++i)
   1730             lines.push(textContents[i]);
   1731     }
   1732 }
   1733 
   1734 WebInspector.TextEditorMainPanel.prototype.__proto__ = WebInspector.TextEditorChunkedPanel.prototype;
   1735 
   1736 WebInspector.TextEditorMainChunk = function(textViewer, startLine, endLine)
   1737 {
   1738     this._textViewer = textViewer;
   1739     this._textModel = textViewer._textModel;
   1740 
   1741     this.element = document.createElement("div");
   1742     this.element.lineNumber = startLine;
   1743     this.element.className = "webkit-line-content";
   1744     this.element.addEventListener("DOMNodeRemoved", this._textViewer._handleDOMUpdatesCallback, false);
   1745 
   1746     this._startLine = startLine;
   1747     endLine = Math.min(this._textModel.linesCount, endLine);
   1748     this.linesCount = endLine - startLine;
   1749 
   1750     this._expanded = false;
   1751 
   1752     this.updateCollapsedLineRow();
   1753 }
   1754 
   1755 WebInspector.TextEditorMainChunk.prototype = {
   1756     addDecoration: function(decoration)
   1757     {
   1758         this._textViewer.beginDomUpdates();
   1759         if (typeof decoration === "string")
   1760             this.element.addStyleClass(decoration);
   1761         else {
   1762             if (!this.element.decorationsElement) {
   1763                 this.element.decorationsElement = document.createElement("div");
   1764                 this.element.decorationsElement.className = "webkit-line-decorations";
   1765                 this.element.appendChild(this.element.decorationsElement);
   1766             }
   1767             this.element.decorationsElement.appendChild(decoration);
   1768         }
   1769         this._textViewer.endDomUpdates();
   1770     },
   1771 
   1772     removeDecoration: function(decoration)
   1773     {
   1774         this._textViewer.beginDomUpdates();
   1775         if (typeof decoration === "string")
   1776             this.element.removeStyleClass(decoration);
   1777         else if (this.element.decorationsElement)
   1778             this.element.decorationsElement.removeChild(decoration);
   1779         this._textViewer.endDomUpdates();
   1780     },
   1781 
   1782     removeAllDecorations: function()
   1783     {
   1784         this._textViewer.beginDomUpdates();
   1785         this.element.className = "webkit-line-content";
   1786         if (this.element.decorationsElement) {
   1787             this.element.removeChild(this.element.decorationsElement);
   1788             delete this.element.decorationsElement;
   1789         }
   1790         this._textViewer.endDomUpdates();
   1791     },
   1792 
   1793     get decorated()
   1794     {
   1795         return this.element.className !== "webkit-line-content" || !!(this.element.decorationsElement && this.element.decorationsElement.firstChild);
   1796     },
   1797 
   1798     get startLine()
   1799     {
   1800         return this._startLine;
   1801     },
   1802 
   1803     set startLine(startLine)
   1804     {
   1805         this._startLine = startLine;
   1806         this.element.lineNumber = startLine;
   1807         if (this._expandedLineRows) {
   1808             for (var i = 0; i < this._expandedLineRows.length; ++i)
   1809                 this._expandedLineRows[i].lineNumber = startLine + i;
   1810         }
   1811     },
   1812 
   1813     get expanded()
   1814     {
   1815         return this._expanded;
   1816     },
   1817 
   1818     set expanded(expanded)
   1819     {
   1820         if (this._expanded === expanded)
   1821             return;
   1822 
   1823         this._expanded = expanded;
   1824 
   1825         if (this.linesCount === 1) {
   1826             if (expanded)
   1827                 this._textViewer._paintLine(this.element);
   1828             return;
   1829         }
   1830 
   1831         this._textViewer.beginDomUpdates();
   1832 
   1833         if (expanded) {
   1834             this._expandedLineRows = [];
   1835             var parentElement = this.element.parentElement;
   1836             for (var i = this.startLine; i < this.startLine + this.linesCount; ++i) {
   1837                 var lineRow = this._createRow(i);
   1838                 parentElement.insertBefore(lineRow, this.element);
   1839                 this._expandedLineRows.push(lineRow);
   1840             }
   1841             parentElement.removeChild(this.element);
   1842             this._textViewer._paintLines(this.startLine, this.startLine + this.linesCount);
   1843         } else {
   1844             var elementInserted = false;
   1845             for (var i = 0; i < this._expandedLineRows.length; ++i) {
   1846                 var lineRow = this._expandedLineRows[i];
   1847                 var parentElement = lineRow.parentElement;
   1848                 if (parentElement) {
   1849                     if (!elementInserted) {
   1850                         elementInserted = true;
   1851                         parentElement.insertBefore(this.element, lineRow);
   1852                     }
   1853                     parentElement.removeChild(lineRow);
   1854                 }
   1855                 this._textViewer._releaseLinesHighlight(lineRow);
   1856             }
   1857             delete this._expandedLineRows;
   1858         }
   1859 
   1860         this._textViewer.endDomUpdates();
   1861     },
   1862 
   1863     get height()
   1864     {
   1865         if (!this._expandedLineRows)
   1866             return this._textViewer._totalHeight(this.element);
   1867         return this._textViewer._totalHeight(this._expandedLineRows[0], this._expandedLineRows[this._expandedLineRows.length - 1]);
   1868     },
   1869 
   1870     get offsetTop()
   1871     {
   1872         return (this._expandedLineRows && this._expandedLineRows.length) ? this._expandedLineRows[0].offsetTop : this.element.offsetTop;
   1873     },
   1874 
   1875     _createRow: function(lineNumber)
   1876     {
   1877         var lineRow = this._textViewer._cachedRows.pop() || document.createElement("div");
   1878         lineRow.lineNumber = lineNumber;
   1879         lineRow.className = "webkit-line-content";
   1880         lineRow.addEventListener("DOMNodeRemoved", this._textViewer._handleDOMUpdatesCallback, false);
   1881         lineRow.textContent = this._textModel.line(lineNumber);
   1882         if (!lineRow.textContent)
   1883             lineRow.appendChild(document.createElement("br"));
   1884         return lineRow;
   1885     },
   1886 
   1887     getExpandedLineRow: function(lineNumber)
   1888     {
   1889         if (!this._expanded || lineNumber < this.startLine || lineNumber >= this.startLine + this.linesCount)
   1890             return null;
   1891         if (!this._expandedLineRows)
   1892             return this.element;
   1893         return this._expandedLineRows[lineNumber - this.startLine];
   1894     },
   1895 
   1896     updateCollapsedLineRow: function()
   1897     {
   1898         if (this.linesCount === 1 && this._expanded)
   1899             return;
   1900 
   1901         var lines = [];
   1902         for (var i = this.startLine; i < this.startLine + this.linesCount; ++i)
   1903             lines.push(this._textModel.line(i));
   1904 
   1905         this.element.removeChildren();
   1906         this.element.textContent = lines.join("\n");
   1907 
   1908         // The last empty line will get swallowed otherwise.
   1909         if (!lines[lines.length - 1])
   1910             this.element.appendChild(document.createElement("br"));
   1911     }
   1912 }
   1913