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