66c1683cd63ccf1187e580a779feab19181898a3
[WebKit-https.git] / Source / WebCore / inspector / front-end / DefaultTextEditor.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 /**
33  * @constructor
34  * @extends {WebInspector.View}
35  * @implements {WebInspector.TextEditor}
36  * @param {?string} url
37  * @param {WebInspector.TextEditorDelegate} delegate
38  */
39 WebInspector.DefaultTextEditor = function(url, delegate)
40 {
41     WebInspector.View.call(this);
42     this._delegate = delegate;
43     this._url = url;
44
45     this.registerRequiredCSS("textEditor.css");
46
47     this.element.className = "text-editor monospace";
48
49     // Prevent middle-click pasting in the editor unless it is explicitly enabled for certain component.
50     this.element.addEventListener("mouseup", preventDefaultOnMouseUp.bind(this), false);
51     function preventDefaultOnMouseUp(event)
52     {
53         if (event.button === 1)
54             event.consume(true);
55     }
56
57     this._textModel = new WebInspector.TextEditorModel();
58     this._textModel.addEventListener(WebInspector.TextEditorModel.Events.TextChanged, this._textChanged, this);
59
60     var syncScrollListener = this._syncScroll.bind(this);
61     var syncDecorationsForLineListener = this._syncDecorationsForLine.bind(this);
62     var syncLineHeightListener = this._syncLineHeight.bind(this);
63     this._mainPanel = new WebInspector.TextEditorMainPanel(this._delegate, this._textModel, url, syncScrollListener, syncDecorationsForLineListener);
64     this._gutterPanel = new WebInspector.TextEditorGutterPanel(this._textModel, syncDecorationsForLineListener, syncLineHeightListener);
65
66     this._mainPanel.element.addEventListener("scroll", this._handleScrollChanged.bind(this), false);
67
68     this._gutterPanel.element.addEventListener("mousedown", this._onMouseDown.bind(this), true);
69
70     // Explicitly enable middle-click pasting in the editor main panel.
71     this._mainPanel.element.addEventListener("mouseup", consumeMouseUp.bind(this), false);
72     function consumeMouseUp(event)
73     {
74         if (event.button === 1)
75             event.consume(false);
76     }
77
78     this.element.appendChild(this._mainPanel.element);
79     this.element.appendChild(this._gutterPanel.element);
80
81     // Forward mouse wheel events from the unscrollable gutter to the main panel.
82     function forwardWheelEvent(event)
83     {
84         var clone = document.createEvent("WheelEvent");
85         clone.initWebKitWheelEvent(event.wheelDeltaX, event.wheelDeltaY,
86                                    event.view,
87                                    event.screenX, event.screenY,
88                                    event.clientX, event.clientY,
89                                    event.ctrlKey, event.altKey, event.shiftKey, event.metaKey);
90         this._mainPanel.element.dispatchEvent(clone);
91     }
92     this._gutterPanel.element.addEventListener("mousewheel", forwardWheelEvent.bind(this), false);
93
94     this.element.addEventListener("keydown", this._handleKeyDown.bind(this), false);
95     this.element.addEventListener("contextmenu", this._contextMenu.bind(this), true);
96
97     this._registerShortcuts();
98 }
99
100 /**
101  * @constructor
102  * @param {WebInspector.TextRange} range
103  * @param {string} text
104  */
105 WebInspector.DefaultTextEditor.EditInfo = function(range, text)
106 {
107     this.range = range;
108     this.text = text;
109 }
110
111 WebInspector.DefaultTextEditor.prototype = {
112     /**
113      * @param {string} regex
114      * @param {string} cssClass
115      */
116     highlightRegex: function(regex, cssClass)
117     {
118         this._mainPanel.highlightRegex(regex, cssClass);
119     },
120
121     /**
122      * @param {string} regex
123      * @return {boolean}
124      */
125     removeRegexHighlight: function(regex)
126     {
127         return this._mainPanel.removeRegexHighlight(regex);
128     },
129
130     /**
131      * @param {string} mimeType
132      */
133     set mimeType(mimeType)
134     {
135         this._mainPanel.mimeType = mimeType;
136     },
137
138     /**
139      * @param {boolean} readOnly
140      */
141     setReadOnly: function(readOnly)
142     {
143         if (this._mainPanel.readOnly() === readOnly)
144             return;
145         this._mainPanel.setReadOnly(readOnly, this.isShowing());
146         WebInspector.markBeingEdited(this.element, !readOnly);
147     },
148
149     /**
150      * @return {boolean}
151      */
152     readOnly: function()
153     {
154         return this._mainPanel.readOnly();
155     },
156
157     /**
158      * @return {Element}
159      */
160     defaultFocusedElement: function()
161     {
162         return this._mainPanel.defaultFocusedElement();
163     },
164
165     /**
166      * @param {number} lineNumber
167      */
168     revealLine: function(lineNumber)
169     {
170         this._mainPanel.revealLine(lineNumber);
171     },
172
173     _onMouseDown: function(event)
174     {
175         var target = event.target.enclosingNodeOrSelfWithClass("webkit-line-number");
176         if (!target)
177             return;
178         this.dispatchEventToListeners(WebInspector.TextEditor.Events.GutterClick, { lineNumber: target.lineNumber, event: event });
179     },
180
181     /**
182      * @param {number} lineNumber
183      * @param {boolean} disabled
184      * @param {boolean} conditional
185      */
186     addBreakpoint: function(lineNumber, disabled, conditional)
187     {
188         this.beginUpdates();
189         this._gutterPanel.addDecoration(lineNumber, "webkit-breakpoint");
190         if (disabled)
191             this._gutterPanel.addDecoration(lineNumber, "webkit-breakpoint-disabled");
192         else
193             this._gutterPanel.removeDecoration(lineNumber, "webkit-breakpoint-disabled");
194         if (conditional)
195             this._gutterPanel.addDecoration(lineNumber, "webkit-breakpoint-conditional");
196         else
197             this._gutterPanel.removeDecoration(lineNumber, "webkit-breakpoint-conditional");
198         this.endUpdates();
199     },
200
201     /**
202      * @param {number} lineNumber
203      */
204     removeBreakpoint: function(lineNumber)
205     {
206         this.beginUpdates();
207         this._gutterPanel.removeDecoration(lineNumber, "webkit-breakpoint");
208         this._gutterPanel.removeDecoration(lineNumber, "webkit-breakpoint-disabled");
209         this._gutterPanel.removeDecoration(lineNumber, "webkit-breakpoint-conditional");
210         this.endUpdates();
211     },
212
213     /**
214      * @param {number} lineNumber
215      */
216     setExecutionLine: function(lineNumber)
217     {
218         this._executionLineNumber = lineNumber;
219         this._mainPanel.addDecoration(lineNumber, "webkit-execution-line");
220         this._gutterPanel.addDecoration(lineNumber, "webkit-execution-line");
221     },
222
223     clearExecutionLine: function()
224     {
225         if (typeof this._executionLineNumber === "number") {
226             this._mainPanel.removeDecoration(this._executionLineNumber, "webkit-execution-line");
227             this._gutterPanel.removeDecoration(this._executionLineNumber, "webkit-execution-line");
228         }
229         delete this._executionLineNumber;
230     },
231
232     /**
233      * @param {number} lineNumber
234      * @param {Element} element
235      */
236     addDecoration: function(lineNumber, element)
237     {
238         this._mainPanel.addDecoration(lineNumber, element);
239         this._gutterPanel.addDecoration(lineNumber, element);
240         this._syncDecorationsForLine(lineNumber);
241     },
242
243     /**
244      * @param {number} lineNumber
245      * @param {Element} element
246      */
247     removeDecoration: function(lineNumber, element)
248     {
249         this._mainPanel.removeDecoration(lineNumber, element);
250         this._gutterPanel.removeDecoration(lineNumber, element);
251         this._syncDecorationsForLine(lineNumber);
252     },
253
254     /**
255      * @param {WebInspector.TextRange} range
256      */
257     markAndRevealRange: function(range)
258     {
259         if (range)
260             this.setSelection(range);
261         this._mainPanel.markAndRevealRange(range);
262     },
263
264     /**
265      * @param {number} lineNumber
266      */
267     highlightLine: function(lineNumber)
268     {
269         if (typeof lineNumber !== "number" || lineNumber < 0)
270             return;
271
272         lineNumber = Math.min(lineNumber, this._textModel.linesCount - 1);
273         this._mainPanel.highlightLine(lineNumber);
274     },
275
276     clearLineHighlight: function()
277     {
278         this._mainPanel.clearLineHighlight();
279     },
280
281     /**
282      * @return {Array.<Element>}
283      */
284     elementsToRestoreScrollPositionsFor: function()
285     {
286         return [this._mainPanel.element];
287     },
288
289     /**
290      * @param {WebInspector.TextEditor} textEditor
291      */
292     inheritScrollPositions: function(textEditor)
293     {
294         this._mainPanel.element._scrollTop = textEditor._mainPanel.element.scrollTop;
295         this._mainPanel.element._scrollLeft = textEditor._mainPanel.element.scrollLeft;
296     },
297
298     beginUpdates: function()
299     {
300         this._mainPanel.beginUpdates();
301         this._gutterPanel.beginUpdates();
302     },
303
304     endUpdates: function()
305     {
306         this._mainPanel.endUpdates();
307         this._gutterPanel.endUpdates();
308         this._updatePanelOffsets();
309     },
310
311     onResize: function()
312     {
313         this._mainPanel.resize();
314         this._gutterPanel.resize();
315         this._updatePanelOffsets();
316     },
317
318     _textChanged: function(event)
319     {
320         this._mainPanel.textChanged(event.data.oldRange, event.data.newRange);
321         this._gutterPanel.textChanged(event.data.oldRange, event.data.newRange);
322         this._updatePanelOffsets();
323         if (event.data.editRange)
324             this._delegate.onTextChanged(event.data.oldRange, event.data.newRange);
325     },
326
327     /**
328      * @param {WebInspector.TextRange} range
329      * @param {string} text
330      * @return {WebInspector.TextRange}
331      */
332     editRange: function(range, text)
333     {
334         return this._textModel.editRange(range, text);
335     },
336
337     _updatePanelOffsets: function()
338     {
339         var lineNumbersWidth = this._gutterPanel.element.offsetWidth;
340         if (lineNumbersWidth)
341             this._mainPanel.element.style.setProperty("left", (lineNumbersWidth + 2) + "px");
342         else
343             this._mainPanel.element.style.removeProperty("left"); // Use default value set in CSS.
344     },
345
346     _syncScroll: function()
347     {
348         var mainElement = this._mainPanel.element;
349         var gutterElement = this._gutterPanel.element;
350         // Handle horizontal scroll bar at the bottom of the main panel.
351         this._gutterPanel.syncClientHeight(mainElement.clientHeight);
352         gutterElement.scrollTop = mainElement.scrollTop;
353     },
354
355     /**
356      * @param {number} lineNumber
357      */
358     _syncDecorationsForLine: function(lineNumber)
359     {
360         if (lineNumber >= this._textModel.linesCount)
361             return;
362
363         var mainChunk = this._mainPanel.chunkForLine(lineNumber);
364         if (mainChunk.linesCount === 1 && mainChunk.isDecorated()) {
365             var gutterChunk = this._gutterPanel.makeLineAChunk(lineNumber);
366             var height = mainChunk.height;
367             if (height)
368                 gutterChunk.element.style.setProperty("height", height + "px");
369             else
370                 gutterChunk.element.style.removeProperty("height");
371         } else {
372             var gutterChunk = this._gutterPanel.chunkForLine(lineNumber);
373             if (gutterChunk.linesCount === 1)
374                 gutterChunk.element.style.removeProperty("height");
375         }
376     },
377
378     /**
379      * @param {Element} gutterRow
380      */
381     _syncLineHeight: function(gutterRow)
382     {
383         if (this._lineHeightSynced)
384             return;
385         if (gutterRow && gutterRow.offsetHeight) {
386             // Force equal line heights for the child panels.
387             this.element.style.setProperty("line-height", gutterRow.offsetHeight + "px");
388             this._lineHeightSynced = true;
389         }
390     },
391
392     _registerShortcuts: function()
393     {
394         var keys = WebInspector.KeyboardShortcut.Keys;
395         var modifiers = WebInspector.KeyboardShortcut.Modifiers;
396
397         this._shortcuts = {};
398
399         var handleEnterKey = this._mainPanel.handleEnterKey.bind(this._mainPanel);
400         this._shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Enter.code, WebInspector.KeyboardShortcut.Modifiers.None)] = handleEnterKey;
401
402         this._shortcuts[WebInspector.KeyboardShortcut.makeKey("z", modifiers.CtrlOrMeta)] = this._mainPanel.handleUndoRedo.bind(this._mainPanel, false);
403         this._shortcuts[WebInspector.KeyboardShortcut.SelectAll] = this._handleSelectAll.bind(this);
404
405         var handleRedo = this._mainPanel.handleUndoRedo.bind(this._mainPanel, true);
406         this._shortcuts[WebInspector.KeyboardShortcut.makeKey("z", modifiers.Shift | modifiers.CtrlOrMeta)] = handleRedo;
407         if (!WebInspector.isMac())
408             this._shortcuts[WebInspector.KeyboardShortcut.makeKey("y", modifiers.CtrlOrMeta)] = handleRedo;
409
410         var handleTabKey = this._mainPanel.handleTabKeyPress.bind(this._mainPanel, false);
411         var handleShiftTabKey = this._mainPanel.handleTabKeyPress.bind(this._mainPanel, true);
412         this._shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Tab.code)] = handleTabKey;
413         this._shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Tab.code, modifiers.Shift)] = handleShiftTabKey;
414     },
415
416     _handleSelectAll: function()
417     {
418         this.setSelection(this._textModel.range());
419         return true;
420     },
421
422     _handleKeyDown: function(e)
423     {
424         // If the event was not triggered from the entire editor, then
425         // ignore it. https://bugs.webkit.org/show_bug.cgi?id=102906
426         if (e.target.enclosingNodeOrSelfWithClass("webkit-line-decorations"))
427             return;
428
429         var shortcutKey = WebInspector.KeyboardShortcut.makeKeyFromEvent(e);
430
431         var handler = this._shortcuts[shortcutKey];
432         if (handler && handler()) {
433             e.consume(true);
434             return;
435         }
436         this._mainPanel.handleKeyDown(e);
437     },
438
439     _contextMenu: function(event)
440     {
441         var anchor = event.target.enclosingNodeOrSelfWithNodeName("a");
442         if (anchor)
443             return;
444         var contextMenu = new WebInspector.ContextMenu(event);
445         var target = event.target.enclosingNodeOrSelfWithClass("webkit-line-number");
446         if (target)
447             this._delegate.populateLineGutterContextMenu(contextMenu, target.lineNumber);
448         else {
449             this._mainPanel.populateContextMenu(event.target, contextMenu);
450         }
451         contextMenu.show();
452     },
453
454     _handleScrollChanged: function(event)
455     {
456         var visibleFrom = this._mainPanel.scrollTop();
457         var firstVisibleLineNumber = this._mainPanel.lineNumberAtOffset(visibleFrom);
458         this._delegate.scrollChanged(firstVisibleLineNumber);
459     },
460
461     /**
462      * @param {number} lineNumber
463      */
464     scrollToLine: function(lineNumber)
465     {
466         this._mainPanel.scrollToLine(lineNumber);
467     },
468
469     /**
470      * @return {WebInspector.TextRange}
471      */
472     selection: function(textRange)
473     {
474         return this._mainPanel.selection();
475     },
476
477     /**
478      * @return {WebInspector.TextRange?}
479      */
480     lastSelection: function()
481     {
482         return this._mainPanel.lastSelection();
483     },
484
485     /**
486      * @param {WebInspector.TextRange} textRange
487      */
488     setSelection: function(textRange)
489     {
490         this._mainPanel.setSelection(textRange);
491     },
492
493     /**
494      * @param {string} text
495      */
496     setText: function(text)
497     {
498         this._textModel.setText(text);
499     },
500
501     /**
502      * @return {string}
503      */
504     text: function()
505     {
506         return this._textModel.text();
507     },
508
509     /**
510      * @return {WebInspector.TextRange}
511      */
512     range: function()
513     {
514         return this._textModel.range();
515     },
516
517     /**
518      * @param {number} lineNumber
519      * @return {string}
520      */
521     line: function(lineNumber)
522     {
523         return this._textModel.line(lineNumber);
524     },
525
526     /**
527      * @return {number}
528      */
529     get linesCount()
530     {
531         return this._textModel.linesCount;
532     },
533
534     /**
535      * @param {number} line
536      * @param {string} name
537      * @param {?Object} value
538      */
539     setAttribute: function(line, name, value)
540     {
541         this._textModel.setAttribute(line, name, value);
542     },
543
544     /**
545      * @param {number} line
546      * @param {string} name
547      * @return {?Object} value
548      */
549     getAttribute: function(line, name)
550     {
551         return this._textModel.getAttribute(line, name);
552     },
553
554     /**
555      * @param {number} line
556      * @param {string} name
557      */
558     removeAttribute: function(line, name)
559     {
560         this._textModel.removeAttribute(line, name);
561     },
562
563     wasShown: function()
564     {
565         if (!this.readOnly())
566             WebInspector.markBeingEdited(this.element, true);
567
568         this._mainPanel.wasShown();
569     },
570
571     willHide: function()
572     {
573         this._mainPanel.willHide();
574         this._gutterPanel.willHide();
575
576         if (!this.readOnly())
577             WebInspector.markBeingEdited(this.element, false);
578     },
579
580     /**
581      * @param {Element} element
582      * @param {Array.<Object>} resultRanges
583      * @param {string} styleClass
584      * @param {Array.<Object>=} changes
585      */
586     highlightRangesWithStyleClass: function(element, resultRanges, styleClass, changes)
587     {
588         this._mainPanel.beginDomUpdates();
589         WebInspector.highlightRangesWithStyleClass(element, resultRanges, styleClass, changes);
590         this._mainPanel.endDomUpdates();
591     },
592
593     /**
594      * @param {Element} element
595      * @param {Object} skipClasses
596      * @param {Object} skipTokens
597      * @return {Element}
598      */
599     highlightExpression: function(element, skipClasses, skipTokens)
600     {
601         // Collect tokens belonging to evaluated expression.
602         var tokens = [element];
603         var token = element.previousSibling;
604         while (token && (skipClasses[token.className] || skipTokens[token.textContent.trim()])) {
605             tokens.push(token);
606             token = token.previousSibling;
607         }
608         tokens.reverse();
609
610         // Wrap them with highlight element.
611         this._mainPanel.beginDomUpdates();
612         var parentElement = element.parentElement;
613         var nextElement = element.nextSibling;
614         var container = document.createElement("span");
615         for (var i = 0; i < tokens.length; ++i)
616             container.appendChild(tokens[i]);
617         parentElement.insertBefore(container, nextElement);
618         this._mainPanel.endDomUpdates();
619         return container;
620     },
621
622     /**
623      * @param {Element} highlightElement
624      */
625     hideHighlightedExpression: function(highlightElement)
626     {
627         this._mainPanel.beginDomUpdates();
628         var parentElement = highlightElement.parentElement;
629         if (parentElement) {
630             var child = highlightElement.firstChild;
631             while (child) {
632                 var nextSibling = child.nextSibling;
633                 parentElement.insertBefore(child, highlightElement);
634                 child = nextSibling;
635             }
636             parentElement.removeChild(highlightElement);
637         }
638         this._mainPanel.endDomUpdates();
639     },
640
641     /**
642      * @param {number} scrollTop
643      * @param {number} clientHeight
644      * @param {number} chunkSize
645      */
646     overrideViewportForTest: function(scrollTop, clientHeight, chunkSize)
647     {
648         this._mainPanel.overrideViewportForTest(scrollTop, clientHeight, chunkSize);
649     },
650
651     __proto__: WebInspector.View.prototype
652 }
653
654 /**
655  * @constructor
656  * @param {WebInspector.TextEditorModel} textModel
657  */
658 WebInspector.TextEditorChunkedPanel = function(textModel)
659 {
660     this._textModel = textModel;
661
662     this.element = document.createElement("div");
663     this.element.addEventListener("scroll", this._scroll.bind(this), false);
664
665     this._defaultChunkSize = 50;
666     this._paintCoalescingLevel = 0;
667     this._domUpdateCoalescingLevel = 0;
668 }
669
670 WebInspector.TextEditorChunkedPanel.prototype = {
671     /**
672      * @param {number} lineNumber
673      */
674     scrollToLine: function(lineNumber)
675     {
676         if (lineNumber >= this._textModel.linesCount)
677             return;
678
679         var chunk = this.makeLineAChunk(lineNumber);
680         this.element.scrollTop = chunk.offsetTop;
681     },
682
683     /**
684      * @param {number} lineNumber
685      */
686     revealLine: function(lineNumber)
687     {
688         if (lineNumber >= this._textModel.linesCount)
689             return;
690
691         var chunk = this.makeLineAChunk(lineNumber);
692         chunk.element.scrollIntoViewIfNeeded();
693     },
694
695     /**
696      * @param {number} lineNumber
697      * @param {string|Element} decoration
698      */
699     addDecoration: function(lineNumber, decoration)
700     {
701         if (lineNumber >= this._textModel.linesCount)
702             return;
703
704         var chunk = this.makeLineAChunk(lineNumber);
705         chunk.addDecoration(decoration);
706     },
707
708     /**
709      * @param {number} lineNumber
710      * @param {string|Element} decoration
711      */
712     removeDecoration: function(lineNumber, decoration)
713     {
714         if (lineNumber >= this._textModel.linesCount)
715             return;
716
717         var chunk = this.chunkForLine(lineNumber);
718         chunk.removeDecoration(decoration);
719     },
720
721     buildChunks: function()
722     {
723         this.beginDomUpdates();
724
725         this._container.removeChildren();
726
727         this._textChunks = [];
728         for (var i = 0; i < this._textModel.linesCount; i += this._defaultChunkSize) {
729             var chunk = this.createNewChunk(i, i + this._defaultChunkSize);
730             this._textChunks.push(chunk);
731             this._container.appendChild(chunk.element);
732         }
733
734         this.repaintAll();
735
736         this.endDomUpdates();
737     },
738
739     /**
740      * @param {number} lineNumber
741      * @return {Object}
742      */
743     makeLineAChunk: function(lineNumber)
744     {
745         var chunkNumber = this.chunkNumberForLine(lineNumber);
746         var oldChunk = this._textChunks[chunkNumber];
747
748         if (!oldChunk) {
749             console.error("No chunk for line number: " + lineNumber);
750             return null;
751         }
752
753         if (oldChunk.linesCount === 1)
754             return oldChunk;
755
756         return this.splitChunkOnALine(lineNumber, chunkNumber, true);
757     },
758
759     /**
760      * @param {number} lineNumber
761      * @param {number} chunkNumber
762      * @param {boolean=} createSuffixChunk
763      * @return {Object}
764      */
765     splitChunkOnALine: function(lineNumber, chunkNumber, createSuffixChunk)
766     {
767         this.beginDomUpdates();
768
769         var oldChunk = this._textChunks[chunkNumber];
770         var wasExpanded = oldChunk.expanded();
771         oldChunk.collapse();
772
773         var insertIndex = chunkNumber + 1;
774
775         // Prefix chunk.
776         if (lineNumber > oldChunk.startLine) {
777             var prefixChunk = this.createNewChunk(oldChunk.startLine, lineNumber);
778             this._textChunks.splice(insertIndex++, 0, prefixChunk);
779             this._container.insertBefore(prefixChunk.element, oldChunk.element);
780         }
781
782         // Line chunk.
783         var endLine = createSuffixChunk ? lineNumber + 1 : oldChunk.startLine + oldChunk.linesCount;
784         var lineChunk = this.createNewChunk(lineNumber, endLine);
785         this._textChunks.splice(insertIndex++, 0, lineChunk);
786         this._container.insertBefore(lineChunk.element, oldChunk.element);
787
788         // Suffix chunk.
789         if (oldChunk.startLine + oldChunk.linesCount > endLine) {
790             var suffixChunk = this.createNewChunk(endLine, oldChunk.startLine + oldChunk.linesCount);
791             this._textChunks.splice(insertIndex, 0, suffixChunk);
792             this._container.insertBefore(suffixChunk.element, oldChunk.element);
793         }
794
795         // Remove enclosing chunk.
796         this._textChunks.splice(chunkNumber, 1);
797         this._container.removeChild(oldChunk.element);
798
799         if (wasExpanded) {
800             if (prefixChunk)
801                 prefixChunk.expand();
802             lineChunk.expand();
803             if (suffixChunk)
804                 suffixChunk.expand();
805         }
806
807         this.endDomUpdates();
808
809         return lineChunk;
810     },
811
812     createNewChunk: function(startLine, endLine)
813     {
814         throw new Error("createNewChunk() should be implemented by descendants");
815     },
816
817     _scroll: function()
818     {
819         this._scheduleRepaintAll();
820         if (this._syncScrollListener)
821             this._syncScrollListener();
822     },
823
824     _scheduleRepaintAll: function()
825     {
826         if (this._repaintAllTimer)
827             clearTimeout(this._repaintAllTimer);
828         this._repaintAllTimer = setTimeout(this.repaintAll.bind(this), 50);
829     },
830
831     beginUpdates: function()
832     {
833         this._paintCoalescingLevel++;
834     },
835
836     endUpdates: function()
837     {
838         this._paintCoalescingLevel--;
839         if (!this._paintCoalescingLevel)
840             this.repaintAll();
841     },
842
843     beginDomUpdates: function()
844     {
845         this._domUpdateCoalescingLevel++;
846     },
847
848     endDomUpdates: function()
849     {
850         this._domUpdateCoalescingLevel--;
851     },
852
853     /**
854      * @param {number} lineNumber
855      * @return {number}
856      */
857     chunkNumberForLine: function(lineNumber)
858     {
859         function compareLineNumbers(value, chunk)
860         {
861             return value < chunk.startLine ? -1 : 1;
862         }
863         var insertBefore = insertionIndexForObjectInListSortedByFunction(lineNumber, this._textChunks, compareLineNumbers);
864         return insertBefore - 1;
865     },
866
867     /**
868      * @param {number} lineNumber
869      * @return {Object}
870      */
871     chunkForLine: function(lineNumber)
872     {
873         return this._textChunks[this.chunkNumberForLine(lineNumber)];
874     },
875
876     /**
877      * @param {number} visibleFrom
878      * @return {number}
879      */
880     _findFirstVisibleChunkNumber: function(visibleFrom)
881     {
882         function compareOffsetTops(value, chunk)
883         {
884             return value < chunk.offsetTop ? -1 : 1;
885         }
886         var insertBefore = insertionIndexForObjectInListSortedByFunction(visibleFrom, this._textChunks, compareOffsetTops);
887         return insertBefore - 1;
888     },
889
890     /**
891      * @param {number} visibleFrom
892      * @param {number} visibleTo
893      * @return {{start: number, end: number}}
894      */
895     findVisibleChunks: function(visibleFrom, visibleTo)
896     {
897         var from = this._findFirstVisibleChunkNumber(visibleFrom);
898         for (var to = from + 1; to < this._textChunks.length; ++to) {
899             if (this._textChunks[to].offsetTop >= visibleTo)
900                 break;
901         }
902         return { start: from, end: to };
903     },
904
905     /**
906      * @param {number} visibleFrom
907      * @return {number}
908      */
909     lineNumberAtOffset: function(visibleFrom)
910     {
911         var chunk = this._textChunks[this._findFirstVisibleChunkNumber(visibleFrom)];
912         if (!chunk.expanded())
913             return chunk.startLine;
914
915         var lineNumbers = [];
916         for (var i = 0; i < chunk.linesCount; ++i) {
917             lineNumbers.push(chunk.startLine + i);
918         }
919
920         function compareLineRowOffsetTops(value, lineNumber)
921         {
922             var lineRow = chunk.expandedLineRow(lineNumber);
923             return value < lineRow.offsetTop ? -1 : 1;
924         }
925         var insertBefore = insertionIndexForObjectInListSortedByFunction(visibleFrom, lineNumbers, compareLineRowOffsetTops);
926         return lineNumbers[insertBefore - 1];
927     },
928
929     repaintAll: function()
930     {
931         delete this._repaintAllTimer;
932
933         if (this._paintCoalescingLevel)
934             return;
935
936         var visibleFrom = this.scrollTop();
937         var visibleTo = visibleFrom + this.clientHeight();
938
939         if (visibleTo) {
940             var result = this.findVisibleChunks(visibleFrom, visibleTo);
941             this.expandChunks(result.start, result.end);
942         }
943     },
944
945     scrollTop: function()
946     {
947         return typeof this._scrollTopOverrideForTest === "number" ? this._scrollTopOverrideForTest : this.element.scrollTop;
948     },
949
950     clientHeight: function()
951     {
952         return typeof this._clientHeightOverrideForTest === "number" ? this._clientHeightOverrideForTest : this.element.clientHeight;
953     },
954
955     /**
956      * @param {number} fromIndex
957      * @param {number} toIndex
958      */
959     expandChunks: function(fromIndex, toIndex)
960     {
961         // First collapse chunks to collect the DOM elements into a cache to reuse them later.
962         for (var i = 0; i < fromIndex; ++i)
963             this._textChunks[i].collapse();
964         for (var i = toIndex; i < this._textChunks.length; ++i)
965             this._textChunks[i].collapse();
966         for (var i = fromIndex; i < toIndex; ++i)
967             this._textChunks[i].expand();
968     },
969
970     /**
971      * @param {Element} firstElement
972      * @param {Element=} lastElement
973      * @return {number}
974      */
975     totalHeight: function(firstElement, lastElement)
976     {
977         lastElement = (lastElement || firstElement).nextElementSibling;
978         if (lastElement)
979             return lastElement.offsetTop - firstElement.offsetTop;
980
981         var offsetParent = firstElement.offsetParent;
982         if (offsetParent && offsetParent.scrollHeight > offsetParent.clientHeight)
983             return offsetParent.scrollHeight - firstElement.offsetTop;
984
985         var total = 0;
986         while (firstElement && firstElement !== lastElement) {
987             total += firstElement.offsetHeight;
988             firstElement = firstElement.nextElementSibling;
989         }
990         return total;
991     },
992
993     resize: function()
994     {
995         this.repaintAll();
996     }
997 }
998
999 /**
1000  * @constructor
1001  * @extends {WebInspector.TextEditorChunkedPanel}
1002  * @param {WebInspector.TextEditorModel} textModel
1003  * @param {function(number)} syncDecorationsForLineListener
1004  * @param {function(Element)} syncLineHeightListener
1005  */
1006 WebInspector.TextEditorGutterPanel = function(textModel, syncDecorationsForLineListener, syncLineHeightListener)
1007 {
1008     WebInspector.TextEditorChunkedPanel.call(this, textModel);
1009
1010     this._syncDecorationsForLineListener = syncDecorationsForLineListener;
1011     this._syncLineHeightListener = syncLineHeightListener;
1012
1013     this.element.className = "text-editor-lines";
1014
1015     this._container = document.createElement("div");
1016     this._container.className = "inner-container";
1017     this.element.appendChild(this._container);
1018
1019     this._freeCachedElements();
1020     this.buildChunks();
1021     this._decorations = {};
1022 }
1023
1024 WebInspector.TextEditorGutterPanel.prototype = {
1025     _freeCachedElements: function()
1026     {
1027         this._cachedRows = [];
1028     },
1029
1030     willHide: function()
1031     {
1032         this._freeCachedElements();
1033     },
1034
1035     /**
1036      * @param {number} startLine
1037      * @param {number} endLine
1038      * @return {WebInspector.TextEditorGutterChunk}
1039      */
1040     createNewChunk: function(startLine, endLine)
1041     {
1042         return new WebInspector.TextEditorGutterChunk(this, startLine, endLine);
1043     },
1044
1045     /**
1046      * @param {WebInspector.TextRange} oldRange
1047      * @param {WebInspector.TextRange} newRange
1048      */
1049     textChanged: function(oldRange, newRange)
1050     {
1051         this.beginDomUpdates();
1052
1053         var linesDiff = newRange.linesCount - oldRange.linesCount;
1054         if (linesDiff) {
1055             // Remove old chunks (if needed).
1056             for (var chunkNumber = this._textChunks.length - 1; chunkNumber >= 0; --chunkNumber) {
1057                 var chunk = this._textChunks[chunkNumber];
1058                 if (chunk.startLine + chunk.linesCount <= this._textModel.linesCount)
1059                     break;
1060                 chunk.collapse();
1061                 this._container.removeChild(chunk.element);
1062             }
1063             this._textChunks.length = chunkNumber + 1;
1064
1065             // Add new chunks (if needed).
1066             var totalLines = 0;
1067             if (this._textChunks.length) {
1068                 var lastChunk = this._textChunks[this._textChunks.length - 1];
1069                 totalLines = lastChunk.startLine + lastChunk.linesCount;
1070             }
1071
1072             for (var i = totalLines; i < this._textModel.linesCount; i += this._defaultChunkSize) {
1073                 var chunk = this.createNewChunk(i, i + this._defaultChunkSize);
1074                 this._textChunks.push(chunk);
1075                 this._container.appendChild(chunk.element);
1076             }
1077
1078             // Shift decorations if necessary
1079             for (var lineNumber in this._decorations) {
1080                 lineNumber = parseInt(lineNumber, 10);
1081
1082                 // Do not move decorations before the start position.
1083                 if (lineNumber < oldRange.startLine)
1084                     continue;
1085                 // Decorations follow the first character of line.
1086                 if (lineNumber === oldRange.startLine && oldRange.startColumn)
1087                     continue;
1088
1089                 var lineDecorationsCopy = this._decorations[lineNumber].slice();
1090                 for (var i = 0; i < lineDecorationsCopy.length; ++i) {
1091                     var decoration = lineDecorationsCopy[i];
1092                     this.removeDecoration(lineNumber, decoration);
1093
1094                     // Do not restore the decorations before the end position.
1095                     if (lineNumber < oldRange.endLine)
1096                         continue;
1097
1098                     this.addDecoration(lineNumber + linesDiff, decoration);
1099                 }
1100             }
1101
1102             this.repaintAll();
1103         } else {
1104             // Decorations may have been removed, so we may have to sync those lines.
1105             var chunkNumber = this.chunkNumberForLine(newRange.startLine);
1106             var chunk = this._textChunks[chunkNumber];
1107             while (chunk && chunk.startLine <= newRange.endLine) {
1108                 if (chunk.linesCount === 1)
1109                     this._syncDecorationsForLineListener(chunk.startLine);
1110                 chunk = this._textChunks[++chunkNumber];
1111             }
1112         }
1113
1114         this.endDomUpdates();
1115     },
1116
1117     /**
1118      * @param {number} clientHeight
1119      */
1120     syncClientHeight: function(clientHeight)
1121     {
1122         if (this.element.offsetHeight > clientHeight)
1123             this._container.style.setProperty("padding-bottom", (this.element.offsetHeight - clientHeight) + "px");
1124         else
1125             this._container.style.removeProperty("padding-bottom");
1126     },
1127
1128     /**
1129      * @param {number} lineNumber
1130      * @param {string|Element} decoration
1131      */
1132     addDecoration: function(lineNumber, decoration)
1133     {
1134         WebInspector.TextEditorChunkedPanel.prototype.addDecoration.call(this, lineNumber, decoration);
1135         var decorations = this._decorations[lineNumber];
1136         if (!decorations) {
1137             decorations = [];
1138             this._decorations[lineNumber] = decorations;
1139         }
1140         decorations.push(decoration);
1141     },
1142
1143     /**
1144      * @param {number} lineNumber
1145      * @param {string|Element} decoration
1146      */
1147     removeDecoration: function(lineNumber, decoration)
1148     {
1149         WebInspector.TextEditorChunkedPanel.prototype.removeDecoration.call(this, lineNumber, decoration);
1150         var decorations = this._decorations[lineNumber];
1151         if (decorations) {
1152             decorations.remove(decoration);
1153             if (!decorations.length)
1154                 delete this._decorations[lineNumber];
1155         }
1156     },
1157
1158     __proto__: WebInspector.TextEditorChunkedPanel.prototype
1159 }
1160
1161 /**
1162  * @constructor
1163  * @param {WebInspector.TextEditorGutterPanel} chunkedPanel
1164  * @param {number} startLine
1165  * @param {number} endLine
1166  */
1167 WebInspector.TextEditorGutterChunk = function(chunkedPanel, startLine, endLine)
1168 {
1169     this._chunkedPanel = chunkedPanel;
1170     this._textModel = chunkedPanel._textModel;
1171
1172     this.startLine = startLine;
1173     endLine = Math.min(this._textModel.linesCount, endLine);
1174     this.linesCount = endLine - startLine;
1175
1176     this._expanded = false;
1177
1178     this.element = document.createElement("div");
1179     this.element.lineNumber = startLine;
1180     this.element.className = "webkit-line-number";
1181
1182     if (this.linesCount === 1) {
1183         // Single line chunks are typically created for decorations. Host line number in
1184         // the sub-element in order to allow flexible border / margin management.
1185         var innerSpan = document.createElement("span");
1186         innerSpan.className = "webkit-line-number-inner";
1187         innerSpan.textContent = startLine + 1;
1188         var outerSpan = document.createElement("div");
1189         outerSpan.className = "webkit-line-number-outer";
1190         outerSpan.appendChild(innerSpan);
1191         this.element.appendChild(outerSpan);
1192     } else {
1193         var lineNumbers = [];
1194         for (var i = startLine; i < endLine; ++i)
1195             lineNumbers.push(i + 1);
1196         this.element.textContent = lineNumbers.join("\n");
1197     }
1198 }
1199
1200 WebInspector.TextEditorGutterChunk.prototype = {
1201     /**
1202      * @param {string} decoration
1203      */
1204     addDecoration: function(decoration)
1205     {
1206         this._chunkedPanel.beginDomUpdates();
1207         if (typeof decoration === "string")
1208             this.element.addStyleClass(decoration);
1209         this._chunkedPanel.endDomUpdates();
1210     },
1211
1212     /**
1213      * @param {string} decoration
1214      */
1215     removeDecoration: function(decoration)
1216     {
1217         this._chunkedPanel.beginDomUpdates();
1218         if (typeof decoration === "string")
1219             this.element.removeStyleClass(decoration);
1220         this._chunkedPanel.endDomUpdates();
1221     },
1222
1223     /**
1224      * @return {boolean}
1225      */
1226     expanded: function()
1227     {
1228         return this._expanded;
1229     },
1230
1231     expand: function()
1232     {
1233         if (this.linesCount === 1)
1234             this._chunkedPanel._syncDecorationsForLineListener(this.startLine);
1235
1236         if (this._expanded)
1237             return;
1238
1239         this._expanded = true;
1240
1241         if (this.linesCount === 1)
1242             return;
1243
1244         this._chunkedPanel.beginDomUpdates();
1245
1246         this._expandedLineRows = [];
1247         var parentElement = this.element.parentElement;
1248         for (var i = this.startLine; i < this.startLine + this.linesCount; ++i) {
1249             var lineRow = this._createRow(i);
1250             parentElement.insertBefore(lineRow, this.element);
1251             this._expandedLineRows.push(lineRow);
1252         }
1253         parentElement.removeChild(this.element);
1254         this._chunkedPanel._syncLineHeightListener(this._expandedLineRows[0]);
1255
1256         this._chunkedPanel.endDomUpdates();
1257     },
1258
1259     collapse: function()
1260     {
1261         if (this.linesCount === 1)
1262             this._chunkedPanel._syncDecorationsForLineListener(this.startLine);
1263
1264         if (!this._expanded)
1265             return;
1266
1267         this._expanded = false;
1268
1269         if (this.linesCount === 1)
1270             return;
1271
1272         this._chunkedPanel.beginDomUpdates();
1273
1274         var elementInserted = false;
1275         for (var i = 0; i < this._expandedLineRows.length; ++i) {
1276             var lineRow = this._expandedLineRows[i];
1277             var parentElement = lineRow.parentElement;
1278             if (parentElement) {
1279                 if (!elementInserted) {
1280                     elementInserted = true;
1281                     parentElement.insertBefore(this.element, lineRow);
1282                 }
1283                 parentElement.removeChild(lineRow);
1284             }
1285             this._chunkedPanel._cachedRows.push(lineRow);
1286         }
1287         delete this._expandedLineRows;
1288
1289         this._chunkedPanel.endDomUpdates();
1290     },
1291
1292     /**
1293      * @return {number}
1294      */
1295     get height()
1296     {
1297         if (!this._expandedLineRows)
1298             return this._chunkedPanel.totalHeight(this.element);
1299         return this._chunkedPanel.totalHeight(this._expandedLineRows[0], this._expandedLineRows[this._expandedLineRows.length - 1]);
1300     },
1301
1302     /**
1303      * @return {number}
1304      */
1305     get offsetTop()
1306     {
1307         return (this._expandedLineRows && this._expandedLineRows.length) ? this._expandedLineRows[0].offsetTop : this.element.offsetTop;
1308     },
1309
1310     /**
1311      * @param {number} lineNumber
1312      * @return {Element}
1313      */
1314     _createRow: function(lineNumber)
1315     {
1316         var lineRow = this._chunkedPanel._cachedRows.pop() || document.createElement("div");
1317         lineRow.lineNumber = lineNumber;
1318         lineRow.className = "webkit-line-number";
1319         lineRow.textContent = lineNumber + 1;
1320         return lineRow;
1321     }
1322 }
1323
1324 /**
1325  * @constructor
1326  * @extends {WebInspector.TextEditorChunkedPanel}
1327  * @param {WebInspector.TextEditorDelegate} delegate
1328  * @param {WebInspector.TextEditorModel} textModel
1329  * @param {?string} url
1330  * @param {function()} syncScrollListener
1331  * @param {function(number)} syncDecorationsForLineListener
1332  */
1333 WebInspector.TextEditorMainPanel = function(delegate, textModel, url, syncScrollListener, syncDecorationsForLineListener)
1334 {
1335     WebInspector.TextEditorChunkedPanel.call(this, textModel);
1336
1337     this._delegate = delegate;
1338     this._syncScrollListener = syncScrollListener;
1339     this._syncDecorationsForLineListener = syncDecorationsForLineListener;
1340
1341     this._url = url;
1342     this._highlighter = new WebInspector.TextEditorHighlighter(textModel, this._highlightDataReady.bind(this));
1343     this._readOnly = true;
1344
1345     this.element.className = "text-editor-contents";
1346     this.element.tabIndex = 0;
1347
1348     this._container = document.createElement("div");
1349     this._container.className = "inner-container";
1350     this._container.tabIndex = 0;
1351     this.element.appendChild(this._container);
1352
1353     this.element.addEventListener("focus", this._handleElementFocus.bind(this), false);
1354     this.element.addEventListener("textInput", this._handleTextInput.bind(this), false);
1355     this.element.addEventListener("cut", this._handleCut.bind(this), false);
1356
1357     this._container.addEventListener("focus", this._handleFocused.bind(this), false);
1358
1359     this._highlightRegexs = {};
1360
1361     this._freeCachedElements();
1362     this.buildChunks();
1363 }
1364
1365 WebInspector.TextEditorMainPanel.prototype = {
1366     /**
1367      * @param {string} regex
1368      * @param {string} cssClass
1369      */
1370     highlightRegex: function(regex, cssClass)
1371     {
1372         this._highlightRegexs[regex] = {
1373             regex: new RegExp(regex, "g"),
1374             cssClass: cssClass
1375         };
1376         this._repaintVisibleChunks();
1377     },
1378
1379     /**
1380      * @param {string} regex
1381      * @return {boolean}
1382      */
1383     removeRegexHighlight: function(regex)
1384     {
1385         var result = delete this._highlightRegexs[regex];
1386         this._repaintVisibleChunks();
1387         return result;
1388     },
1389
1390     _repaintVisibleChunks: function()
1391     {
1392         var visibleFrom = this.scrollTop();
1393         var visibleTo = visibleFrom + this.clientHeight();
1394
1395         var visibleChunks = this.findVisibleChunks(visibleFrom, visibleTo);
1396         var selection = this.selection();
1397
1398         for(var i = visibleChunks.start; i < visibleChunks.end; ++i) {
1399             var chunk = this._textChunks[i];
1400             this._paintLines(chunk._startLine, chunk._startLine + chunk.linesCount);
1401         }
1402         this._restoreSelection(selection);
1403     },
1404
1405     wasShown: function()
1406     {
1407         this._boundSelectionChangeListener = this._handleSelectionChange.bind(this);
1408         document.addEventListener("selectionchange", this._boundSelectionChangeListener, false);
1409
1410         this._isShowing = true;
1411         this._attachMutationObserver();
1412     },
1413
1414     willHide: function()
1415     {
1416         document.removeEventListener("selectionchange", this._boundSelectionChangeListener, false);
1417         delete this._boundSelectionChangeListener;
1418
1419         this._detachMutationObserver();
1420         this._isShowing = false;
1421         this._freeCachedElements();
1422     },
1423
1424     /**
1425      * @param {Element} eventTarget
1426      * @param {WebInspector.ContextMenu} contextMenu
1427      */
1428     populateContextMenu: function(eventTarget, contextMenu)
1429     {
1430         var target = this._enclosingLineRowOrSelf(eventTarget);
1431         this._delegate.populateTextAreaContextMenu(contextMenu, target && target.lineNumber);
1432     },
1433
1434     /**
1435      * @param {WebInspector.TextRange} textRange
1436      */
1437     setSelection: function(textRange)
1438     {
1439         this._lastSelection = textRange;
1440         if (this.element.isAncestor(document.activeElement))
1441             this._restoreSelection(textRange);
1442     },
1443
1444     _handleFocused: function()
1445     {
1446         if (this._lastSelection)
1447             this.setSelection(this._lastSelection);
1448     },
1449
1450     _attachMutationObserver: function()
1451     {
1452         if (!this._isShowing)
1453             return;
1454
1455         if (this._mutationObserver)
1456             this._mutationObserver.disconnect();
1457         this._mutationObserver = new NonLeakingMutationObserver(this._handleMutations.bind(this));
1458         this._mutationObserver.observe(this._container, { subtree: true, childList: true, characterData: true });
1459     },
1460
1461     _detachMutationObserver: function()
1462     {
1463         if (!this._isShowing)
1464             return;
1465
1466         if (this._mutationObserver) {
1467             this._mutationObserver.disconnect();
1468             delete this._mutationObserver;
1469         }
1470     },
1471
1472     /**
1473      * @param {string} mimeType
1474      */
1475     set mimeType(mimeType)
1476     {
1477         this._highlighter.mimeType = mimeType;
1478     },
1479
1480     /**
1481      * @param {boolean} readOnly
1482      * @param {boolean} requestFocus
1483      */
1484     setReadOnly: function(readOnly, requestFocus)
1485     {
1486         if (this._readOnly === readOnly)
1487             return;
1488
1489         this.beginDomUpdates();
1490         this._readOnly = readOnly;
1491         if (this._readOnly)
1492             this._container.removeStyleClass("text-editor-editable");
1493         else {
1494             this._container.addStyleClass("text-editor-editable");
1495             if (requestFocus)
1496                 this._updateSelectionOnStartEditing();
1497         }
1498         this.endDomUpdates();
1499     },
1500
1501     /**
1502      * @return {boolean}
1503      */
1504     readOnly: function()
1505     {
1506         return this._readOnly;
1507     },
1508
1509     _handleElementFocus: function()
1510     {
1511         if (!this._readOnly)
1512             this._container.focus();
1513     },
1514
1515     /**
1516      * @return {Element}
1517      */
1518     defaultFocusedElement: function()
1519     {
1520         if (this._readOnly)
1521             return this.element;
1522         return this._container;
1523     },
1524
1525     _updateSelectionOnStartEditing: function()
1526     {
1527         // focus() needs to go first for the case when the last selection was inside the editor and
1528         // the "Edit" button was clicked. In this case we bail at the check below, but the
1529         // editor does not receive the focus, thus "Esc" does not cancel editing until at least
1530         // one change has been made to the editor contents.
1531         this._container.focus();
1532         var selection = window.getSelection();
1533         if (selection.rangeCount) {
1534             var commonAncestorContainer = selection.getRangeAt(0).commonAncestorContainer;
1535             if (this._container.isSelfOrAncestor(commonAncestorContainer))
1536                 return;
1537         }
1538
1539         selection.removeAllRanges();
1540         var range = document.createRange();
1541         range.setStart(this._container, 0);
1542         range.setEnd(this._container, 0);
1543         selection.addRange(range);
1544     },
1545
1546     /**
1547      * @param {WebInspector.TextRange} range
1548      */
1549     markAndRevealRange: function(range)
1550     {
1551         if (this._rangeToMark) {
1552             var markedLine = this._rangeToMark.startLine;
1553             delete this._rangeToMark;
1554             // Remove the marked region immediately.
1555             this.beginDomUpdates();
1556             var chunk = this.chunkForLine(markedLine);
1557             var wasExpanded = chunk.expanded();
1558             chunk.collapse();
1559             chunk.updateCollapsedLineRow();
1560             if (wasExpanded)
1561                 chunk.expand();
1562             this.endDomUpdates();
1563         }
1564
1565         if (range) {
1566             this._rangeToMark = range;
1567             this.revealLine(range.startLine);
1568             var chunk = this.makeLineAChunk(range.startLine);
1569             this._paintLine(chunk.element);
1570             if (this._markedRangeElement)
1571                 this._markedRangeElement.scrollIntoViewIfNeeded();
1572         }
1573         delete this._markedRangeElement;
1574     },
1575
1576     /**
1577      * @param {number} lineNumber
1578      */
1579     highlightLine: function(lineNumber)
1580     {
1581         this.clearLineHighlight();
1582         this._highlightedLine = lineNumber;
1583         this.revealLine(lineNumber);
1584
1585         if (!this._readOnly)
1586             this._restoreSelection(WebInspector.TextRange.createFromLocation(lineNumber, 0), false);
1587
1588         this.addDecoration(lineNumber, "webkit-highlighted-line");
1589     },
1590
1591     clearLineHighlight: function()
1592     {
1593         if (typeof this._highlightedLine === "number") {
1594             this.removeDecoration(this._highlightedLine, "webkit-highlighted-line");
1595             delete this._highlightedLine;
1596         }
1597     },
1598
1599     _freeCachedElements: function()
1600     {
1601         this._cachedSpans = [];
1602         this._cachedTextNodes = [];
1603         this._cachedRows = [];
1604     },
1605
1606     /**
1607      * @param {boolean} redo
1608      * @return {boolean}
1609      */
1610     handleUndoRedo: function(redo)
1611     {
1612         if (this.readOnly())
1613             return false;
1614
1615         this.beginUpdates();
1616
1617         var range = redo ? this._textModel.redo() : this._textModel.undo();
1618
1619         this.endUpdates();
1620
1621         // Restore location post-repaint.
1622         if (range)
1623             this._restoreSelection(range, true);
1624
1625         return true;
1626     },
1627
1628     /**
1629      * @param {boolean} shiftKey
1630      * @return {boolean}
1631      */
1632     handleTabKeyPress: function(shiftKey)
1633     {
1634         if (this.readOnly())
1635             return false;
1636
1637         var selection = this.selection();
1638         if (!selection)
1639             return false;
1640
1641         var range = selection.normalize();
1642
1643         this.beginUpdates();
1644
1645         var newRange;
1646         var rangeWasEmpty = range.isEmpty();
1647         if (shiftKey)
1648             newRange = this._textModel.unindentLines(range);
1649         else {
1650             if (rangeWasEmpty)
1651                 newRange = this._textModel.editRange(range, WebInspector.settings.textEditorIndent.get());
1652             else
1653                 newRange = this._textModel.indentLines(range);
1654         }
1655
1656         this.endUpdates();
1657         if (rangeWasEmpty)
1658             newRange.startColumn = newRange.endColumn;
1659         this._restoreSelection(newRange, true);
1660         return true;
1661     },
1662
1663     handleEnterKey: function()
1664     {
1665         if (this.readOnly())
1666             return false;
1667
1668         var range = this.selection();
1669         if (!range)
1670             return false;
1671
1672         range = range.normalize();
1673
1674         if (range.endColumn === 0)
1675             return false;
1676
1677         var line = this._textModel.line(range.startLine);
1678         var linePrefix = line.substring(0, range.startColumn);
1679         var indentMatch = linePrefix.match(/^\s+/);
1680         var currentIndent = indentMatch ? indentMatch[0] : "";
1681
1682         var textEditorIndent = WebInspector.settings.textEditorIndent.get();
1683         var indent = WebInspector.TextEditorModel.endsWithBracketRegex.test(linePrefix) ? currentIndent + textEditorIndent : currentIndent;
1684
1685         if (!indent)
1686             return false;
1687
1688         this.beginDomUpdates();
1689
1690         var lineBreak = this._textModel.lineBreak;
1691         var newRange;
1692         if (range.isEmpty() && line.substr(range.endColumn - 1, 2) === '{}') {
1693             // {|}
1694             // becomes
1695             // {
1696             //     |
1697             // }
1698             newRange = this._textModel.editRange(range, lineBreak + indent + lineBreak + currentIndent);
1699             newRange.endLine--;
1700             newRange.endColumn += textEditorIndent.length;
1701         } else
1702             newRange = this._textModel.editRange(range, lineBreak + indent);
1703
1704         this.endDomUpdates();
1705         this._restoreSelection(newRange.collapseToEnd(), true);
1706
1707         return true;
1708     },
1709
1710     /**
1711      * @param {number} lineNumber
1712      * @param {number} chunkNumber
1713      * @param {boolean=} createSuffixChunk
1714      * @return {Object}
1715      */
1716     splitChunkOnALine: function(lineNumber, chunkNumber, createSuffixChunk)
1717     {
1718         var selection = this.selection();
1719         var chunk = WebInspector.TextEditorChunkedPanel.prototype.splitChunkOnALine.call(this, lineNumber, chunkNumber, createSuffixChunk);
1720         this._restoreSelection(selection);
1721         return chunk;
1722     },
1723
1724     beginDomUpdates: function()
1725     {
1726         if (!this._domUpdateCoalescingLevel)
1727             this._detachMutationObserver();
1728         WebInspector.TextEditorChunkedPanel.prototype.beginDomUpdates.call(this);
1729     },
1730
1731     endDomUpdates: function()
1732     {
1733         WebInspector.TextEditorChunkedPanel.prototype.endDomUpdates.call(this);
1734         if (!this._domUpdateCoalescingLevel)
1735             this._attachMutationObserver();
1736     },
1737
1738     buildChunks: function()
1739     {
1740         for (var i = 0; i < this._textModel.linesCount; ++i)
1741             this._textModel.removeAttribute(i, "highlight");
1742
1743         WebInspector.TextEditorChunkedPanel.prototype.buildChunks.call(this);
1744     },
1745
1746     /**
1747      * @param {number} startLine
1748      * @param {number} endLine
1749      * @return {WebInspector.TextEditorMainChunk}
1750      */
1751     createNewChunk: function(startLine, endLine)
1752     {
1753         return new WebInspector.TextEditorMainChunk(this, startLine, endLine);
1754     },
1755
1756     /**
1757      * @param {number} fromIndex
1758      * @param {number} toIndex
1759      */
1760     expandChunks: function(fromIndex, toIndex)
1761     {
1762         var lastChunk = this._textChunks[toIndex - 1];
1763         var lastVisibleLine = lastChunk.startLine + lastChunk.linesCount;
1764
1765         var selection = this.selection();
1766
1767         this._muteHighlightListener = true;
1768         this._highlighter.highlight(lastVisibleLine);
1769         delete this._muteHighlightListener;
1770
1771         WebInspector.TextEditorChunkedPanel.prototype.expandChunks.call(this, fromIndex, toIndex);
1772
1773         this._restoreSelection(selection);
1774     },
1775
1776     /**
1777      * @param {number} fromLine
1778      * @param {number} toLine
1779      */
1780     _highlightDataReady: function(fromLine, toLine)
1781     {
1782         if (this._muteHighlightListener)
1783             return;
1784         this._paintLines(fromLine, toLine, true /*restoreSelection*/);
1785     },
1786
1787     /**
1788      * @param {number} fromLine
1789      * @param {number} toLine
1790      * @param {boolean=} restoreSelection
1791      */
1792     _paintLines: function(fromLine, toLine, restoreSelection)
1793     {
1794         var chunk;
1795         var selection;
1796         for (var lineNumber = fromLine; lineNumber < toLine; ++lineNumber) {
1797             if (!chunk || lineNumber < chunk.startLine || lineNumber >= chunk.startLine + chunk.linesCount)
1798                 chunk = this.chunkForLine(lineNumber);
1799             var lineRow = chunk.expandedLineRow(lineNumber);
1800             if (!lineRow)
1801                 continue;
1802             if (restoreSelection && !selection)
1803                 selection = this.selection();
1804             this._paintLine(lineRow);
1805         }
1806
1807         if (restoreSelection)
1808             this._restoreSelection(selection);
1809     },
1810
1811     /**
1812      * @param {string} line
1813      * @param {RegExp} regex
1814      * @return {Array.<{startColumn: number, endColumn: number}>}
1815      */
1816     _findRegexOccurrences: function(line, regex)
1817     {
1818         var ranges = [];
1819         var regexResult;
1820         while (regexResult = regex.exec(line)) {
1821             ranges.push({
1822                 startColumn: regexResult.index,
1823                 endColumn: regexResult.index + regexResult[0].length - 1
1824             });
1825         }
1826         return ranges;
1827     },
1828
1829     /**
1830      * @param {Element} lineRow
1831      * @param {string} line
1832      * @param {RegExp} regex
1833      * @return {Array.<{left: number, width: number}>}
1834      */
1835     _measureRegex: function(lineRow, line, regex)
1836     {
1837         var ranges = this._findRegexOccurrences(line, regex);
1838         if (ranges.length === 0)
1839             return [];
1840
1841         this._renderRanges(lineRow, line, ranges);
1842         var spans = lineRow.getElementsByTagName("span");
1843         if (WebInspector.debugDefaultTextEditor)
1844             console.assert(spans.length === ranges.length, "Ranges number: " + ranges.length + " !== spans number: " + spans.length);
1845
1846         var metrics = [];
1847         for(var i = 0; i < ranges.length; ++i)
1848             metrics.push({
1849                 left: spans[i].offsetLeft,
1850                 width: spans[i].offsetWidth
1851             });
1852         return metrics;
1853     },
1854
1855     /**
1856      * @param {Element} lineRow
1857      * @param {Array.<{offsetLeft: number, offsetTop: number, offsetWidth: number, offsetHeight: number}>} metrics
1858      * @param {string} cssClass
1859      */
1860     _appendOverlayHighlight: function(lineRow, metrics, cssClass)
1861     {
1862         const extraWidth = 1;
1863         for(var i = 0; i < metrics.length; ++i) {
1864             var highlight = document.createElement("span");
1865             highlight.addStyleClass(cssClass);
1866             lineRow.appendChild(highlight);
1867
1868             highlight.style.marginLeft = (metrics[i].left - highlight.offsetLeft - extraWidth) + "px";
1869             highlight.style.width = (metrics[i].width + extraWidth * 2) + "px";
1870             highlight.innerHTML = "&nbsp;";
1871             highlight.addStyleClass("text-editor-overlay-highlight");
1872         }
1873     },
1874
1875     /**
1876      * @param {Element} lineRow
1877      * @param {string} line
1878      * @param {Array.<{startColumn: number, endColumn: number, token: ?string}>} ranges
1879      */
1880     _renderRanges: function(lineRow, line, ranges)
1881     {
1882         var decorationsElement = lineRow.decorationsElement;
1883
1884         if (!decorationsElement)
1885             lineRow.removeChildren();
1886         else {
1887             while (true) {
1888                 var child = lineRow.firstChild;
1889                 if (!child || child === decorationsElement)
1890                     break;
1891                 lineRow.removeChild(child);
1892             }
1893         }
1894
1895         if (!line)
1896             lineRow.insertBefore(document.createElement("br"), decorationsElement);
1897
1898         var plainTextStart = 0;
1899         for(var i = 0; i < ranges.length; i++) {
1900             var rangeStart = ranges[i].startColumn;
1901             var rangeEnd = ranges[i].endColumn;
1902             var cssClass = ranges[i].token ? "webkit-" + ranges[i].token : "";
1903
1904             if (plainTextStart < rangeStart) {
1905                 this._insertTextNodeBefore(lineRow, decorationsElement, line.substring(plainTextStart, rangeStart));
1906             }
1907             this._insertSpanBefore(lineRow, decorationsElement, line.substring(rangeStart, rangeEnd + 1), cssClass);
1908             plainTextStart = rangeEnd + 1;
1909         }
1910         if (plainTextStart < line.length) {
1911             this._insertTextNodeBefore(lineRow, decorationsElement, line.substring(plainTextStart, line.length));
1912         }
1913     },
1914
1915     /**
1916      * @param {Element} lineRow
1917      */
1918     _paintLine: function(lineRow)
1919     {
1920         var lineNumber = lineRow.lineNumber;
1921
1922         this.beginDomUpdates();
1923         try {
1924             var highlight = this._textModel.getAttribute(lineNumber, "highlight");
1925             if (!highlight)
1926                 return;
1927
1928             var line = this._textModel.line(lineNumber);
1929
1930             var metrics = [];
1931             var cssClasses = [];
1932             for(var key in this._highlightRegexs) {
1933                 var value = this._highlightRegexs[key];
1934                 metrics.push(this._measureRegex(lineRow, line, value.regex));
1935                 cssClasses.push(value.cssClass);
1936             }
1937
1938             var ranges = highlight.ranges;
1939             this._renderRanges(lineRow, line, ranges);
1940
1941             for(var i = 0; i < metrics.length; ++i)
1942                 this._appendOverlayHighlight(lineRow, metrics[i], cssClasses[i]);
1943         } finally {
1944             if (this._rangeToMark && this._rangeToMark.startLine === lineNumber)
1945                 this._markedRangeElement = WebInspector.highlightSearchResult(lineRow, this._rangeToMark.startColumn, this._rangeToMark.endColumn - this._rangeToMark.startColumn);
1946             this.endDomUpdates();
1947         }
1948     },
1949
1950     /**
1951      * @param {Element} lineRow
1952      */
1953     _releaseLinesHighlight: function(lineRow)
1954     {
1955         if (!lineRow)
1956             return;
1957         if ("spans" in lineRow) {
1958             var spans = lineRow.spans;
1959             for (var j = 0; j < spans.length; ++j)
1960                 this._cachedSpans.push(spans[j]);
1961             delete lineRow.spans;
1962         }
1963         if ("textNodes" in lineRow) {
1964             var textNodes = lineRow.textNodes;
1965             for (var j = 0; j < textNodes.length; ++j)
1966                 this._cachedTextNodes.push(textNodes[j]);
1967             delete lineRow.textNodes;
1968         }
1969         this._cachedRows.push(lineRow);
1970     },
1971
1972     /**
1973      * @param {?Node=} lastUndamagedLineRow
1974      * @return {WebInspector.TextRange}
1975      */
1976     selection: function(lastUndamagedLineRow)
1977     {
1978         var selection = window.getSelection();
1979         if (!selection.rangeCount)
1980             return null;
1981         // Selection may be outside of the editor.
1982         if (!this._container.isAncestor(selection.anchorNode) || !this._container.isAncestor(selection.focusNode))
1983             return null;
1984         var start = this._selectionToPosition(selection.anchorNode, selection.anchorOffset, lastUndamagedLineRow);
1985         var end = selection.isCollapsed ? start : this._selectionToPosition(selection.focusNode, selection.focusOffset, lastUndamagedLineRow);
1986         return new WebInspector.TextRange(start.line, start.column, end.line, end.column);
1987     },
1988
1989     lastSelection: function()
1990     {
1991         return this._lastSelection;
1992     },
1993
1994     /**
1995      * @param {boolean=} scrollIntoView
1996      */
1997     _restoreSelection: function(range, scrollIntoView)
1998     {
1999         if (!range)
2000             return;
2001
2002         var start = this._positionToSelection(range.startLine, range.startColumn);
2003         var end = range.isEmpty() ? start : this._positionToSelection(range.endLine, range.endColumn);
2004         window.getSelection().setBaseAndExtent(start.container, start.offset, end.container, end.offset);
2005
2006         if (scrollIntoView) {
2007             for (var node = end.container; node; node = node.parentElement) {
2008                 if (node.scrollIntoViewIfNeeded) {
2009                     node.scrollIntoViewIfNeeded();
2010                     break;
2011                 }
2012             }
2013         }
2014         this._lastSelection = range;
2015     },
2016
2017     /**
2018      * @param {Node} container
2019      * @param {number} offset
2020      * @param {?Node=} lastUndamagedLineRow
2021      * @return {{line: number, column: number}}
2022      */
2023     _selectionToPosition: function(container, offset, lastUndamagedLineRow)
2024     {
2025         if (container === this._container && offset === 0)
2026             return { line: 0, column: 0 };
2027         if (container === this._container && offset === 1)
2028             return { line: this._textModel.linesCount - 1, column: this._textModel.lineLength(this._textModel.linesCount - 1) };
2029
2030         // This method can be called on the damaged DOM (when DOM does not match model).
2031         // We need to start counting lines from the first undamaged line if it is given.
2032         var lineNumber;
2033         var column = 0;
2034         var node;
2035         var scopeNode;
2036         if (lastUndamagedLineRow === null) {
2037              // Last undamaged row is given, but is null - force traverse from the beginning
2038             node = this._container.firstChild;
2039             scopeNode = this._container;
2040             lineNumber = 0;
2041         } else {
2042             var lineRow = this._enclosingLineRowOrSelf(container);
2043             if (!lastUndamagedLineRow || (typeof lineRow.lineNumber === "number" && lineRow.lineNumber <= lastUndamagedLineRow.lineNumber)) {
2044                 // DOM is consistent (or we belong to the first damaged row)- lookup the row we belong to and start with it.
2045                 node = lineRow;
2046                 scopeNode = node;
2047                 lineNumber = node.lineNumber;
2048             } else {
2049                 // Start with the node following undamaged row. It corresponds to lineNumber + 1.
2050                 node = lastUndamagedLineRow.nextSibling;
2051                 scopeNode = this._container;
2052                 lineNumber = lastUndamagedLineRow.lineNumber + 1;
2053             }
2054         }
2055
2056         // Fast return the line start.
2057         if (container === node && offset === 0)
2058             return { line: lineNumber, column: 0 };
2059
2060         // Traverse text and increment lineNumber / column.
2061         for (; node && node !== container; node = node.traverseNextNode(scopeNode)) {
2062             if (node.nodeName.toLowerCase() === "br") {
2063                 lineNumber++;
2064                 column = 0;
2065             } else if (node.nodeType === Node.TEXT_NODE) {
2066                 var text = node.textContent;
2067                 for (var i = 0; i < text.length; ++i) {
2068                     if (text.charAt(i) === "\n") {
2069                         lineNumber++;
2070                         column = 0;
2071                     } else
2072                         column++;
2073                 }
2074             }
2075         }
2076
2077         // We reached our container node, traverse within itself until we reach given offset.
2078         if (node === container && offset) {
2079             var text = node.textContent;
2080             // In case offset == 1 and lineRow is a chunk div, we need to traverse it all.
2081             var textOffset = (node._chunk && offset === 1) ? text.length : offset;
2082             for (var i = 0; i < textOffset; ++i) {
2083                 if (text.charAt(i) === "\n") {
2084                     lineNumber++;
2085                     column = 0;
2086                 } else
2087                     column++;
2088             }
2089         }
2090         return { line: lineNumber, column: column };
2091     },
2092
2093     /**
2094      * @param {number} line
2095      * @param {number} column
2096      * @return {{container: Element, offset: number}}
2097      */
2098     _positionToSelection: function(line, column)
2099     {
2100         var chunk = this.chunkForLine(line);
2101         // One-lined collapsed chunks may still stay highlighted.
2102         var lineRow = chunk.linesCount === 1 ? chunk.element : chunk.expandedLineRow(line);
2103         if (lineRow)
2104             var rangeBoundary = lineRow.rangeBoundaryForOffset(column);
2105         else {
2106             var offset = column;
2107             for (var i = chunk.startLine; i < line && i < this._textModel.linesCount; ++i)
2108                 offset += this._textModel.lineLength(i) + 1; // \n
2109             lineRow = chunk.element;
2110             if (lineRow.firstChild)
2111                 var rangeBoundary = { container: lineRow.firstChild, offset: offset };
2112             else
2113                 var rangeBoundary = { container: lineRow, offset: 0 };
2114         }
2115         return rangeBoundary;
2116     },
2117
2118     /**
2119      * @param {Node} element
2120      * @return {?Node}
2121      */
2122     _enclosingLineRowOrSelf: function(element)
2123     {
2124         var lineRow = element.enclosingNodeOrSelfWithClass("webkit-line-content");
2125         if (lineRow)
2126             return lineRow;
2127
2128         for (lineRow = element; lineRow; lineRow = lineRow.parentElement) {
2129             if (lineRow.parentElement === this._container)
2130                 return lineRow;
2131         }
2132         return null;
2133     },
2134
2135     /**
2136      * @param {Element} element
2137      * @param {Element} oldChild
2138      * @param {string} content
2139      * @param {string} className
2140      */
2141     _insertSpanBefore: function(element, oldChild, content, className)
2142     {
2143         if (className === "html-resource-link" || className === "html-external-link") {
2144             element.insertBefore(this._createLink(content, className === "html-external-link"), oldChild);
2145             return;
2146         }
2147
2148         var span = this._cachedSpans.pop() || document.createElement("span");
2149         span.className = className;
2150         if (WebInspector.FALSE) // For paint debugging.
2151             span.addStyleClass("debug-fadeout");
2152         span.textContent = content;
2153         element.insertBefore(span, oldChild);
2154         if (!("spans" in element))
2155             element.spans = [];
2156         element.spans.push(span);
2157     },
2158
2159     /**
2160      * @param {Element} element
2161      * @param {Element} oldChild
2162      * @param {string} text
2163      */
2164     _insertTextNodeBefore: function(element, oldChild, text)
2165     {
2166         var textNode = this._cachedTextNodes.pop();
2167         if (textNode)
2168             textNode.nodeValue = text;
2169         else
2170             textNode = document.createTextNode(text);
2171         element.insertBefore(textNode, oldChild);
2172         if (!("textNodes" in element))
2173             element.textNodes = [];
2174         element.textNodes.push(textNode);
2175     },
2176
2177     /**
2178      * @param {string} content
2179      * @param {boolean} isExternal
2180      * @return {Element}
2181      */
2182     _createLink: function(content, isExternal)
2183     {
2184         var quote = content.charAt(0);
2185         if (content.length > 1 && (quote === "\"" || quote === "'"))
2186             content = content.substring(1, content.length - 1);
2187         else
2188             quote = null;
2189
2190         var span = document.createElement("span");
2191         span.className = "webkit-html-attribute-value";
2192         if (quote)
2193             span.appendChild(document.createTextNode(quote));
2194         span.appendChild(this._delegate.createLink(content, isExternal));
2195         if (quote)
2196             span.appendChild(document.createTextNode(quote));
2197         return span;
2198     },
2199
2200     /**
2201      * @param {Array.<WebKitMutation>} mutations
2202      */
2203     _handleMutations: function(mutations)
2204     {
2205         if (this._readOnly) {
2206             delete this._keyDownCode;
2207             return;
2208         }
2209
2210         // Annihilate noop BR addition + removal that takes place upon line removal.
2211         var filteredMutations = mutations.slice();
2212         var addedBRs = new Map();
2213         for (var i = 0; i < mutations.length; ++i) {
2214             var mutation = mutations[i];
2215             if (mutation.type !== "childList")
2216                 continue;
2217             if (mutation.addedNodes.length === 1 && mutation.addedNodes[0].nodeName === "BR")
2218                 addedBRs.put(mutation.addedNodes[0], mutation);
2219             else if (mutation.removedNodes.length === 1 && mutation.removedNodes[0].nodeName === "BR") {
2220                 var noopMutation = addedBRs.get(mutation.removedNodes[0]);
2221                 if (noopMutation) {
2222                     filteredMutations.remove(mutation);
2223                     filteredMutations.remove(noopMutation);
2224                 }
2225             }
2226         }
2227
2228         var dirtyLines;
2229         for (var i = 0; i < filteredMutations.length; ++i) {
2230             var mutation = filteredMutations[i];
2231             var changedNodes = [];
2232             if (mutation.type === "childList" && mutation.addedNodes.length)
2233                 changedNodes = Array.prototype.slice.call(mutation.addedNodes);
2234             else if (mutation.type === "childList" && mutation.removedNodes.length)
2235                 changedNodes = Array.prototype.slice.call(mutation.removedNodes);
2236             changedNodes.push(mutation.target);
2237
2238             for (var j = 0; j < changedNodes.length; ++j) {
2239                 var lines = this._collectDirtyLines(mutation, changedNodes[j]);
2240                 if (!lines)
2241                     continue;
2242                 if (!dirtyLines) {
2243                     dirtyLines = lines;
2244                     continue;
2245                 }
2246                 dirtyLines.start = Math.min(dirtyLines.start, lines.start);
2247                 dirtyLines.end = Math.max(dirtyLines.end, lines.end);
2248             }
2249         }
2250         if (dirtyLines) {
2251             delete this._rangeToMark;
2252             this._applyDomUpdates(dirtyLines);
2253         }
2254
2255         this._assertDOMMatchesTextModel();
2256
2257         delete this._keyDownCode;
2258     },
2259
2260     /**
2261      * @param {WebKitMutation} mutation
2262      * @param {Node} target
2263      * @return {?Object}
2264      */
2265     _collectDirtyLines: function(mutation, target)
2266     {
2267         var lineRow = this._enclosingLineRowOrSelf(target);
2268         if (!lineRow)
2269             return null;
2270
2271         if (lineRow.decorationsElement && lineRow.decorationsElement.isSelfOrAncestor(target)) {
2272             if (this._syncDecorationsForLineListener)
2273                 this._syncDecorationsForLineListener(lineRow.lineNumber);
2274             return null;
2275         }
2276
2277         if (typeof lineRow.lineNumber !== "number")
2278             return null;
2279
2280         var startLine = lineRow.lineNumber;
2281         var endLine = lineRow._chunk ? lineRow._chunk.endLine - 1 : lineRow.lineNumber;
2282         return { start: startLine, end: endLine };
2283     },
2284
2285     /**
2286      * @param {Object} dirtyLines
2287      */
2288     _applyDomUpdates: function(dirtyLines)
2289     {
2290         var lastUndamagedLineNumber = dirtyLines.start - 1; // Can be -1
2291         var firstUndamagedLineNumber = dirtyLines.end + 1; // Can be this._textModel.linesCount
2292
2293         var lastUndamagedLineChunk = lastUndamagedLineNumber >= 0 ? this._textChunks[this.chunkNumberForLine(lastUndamagedLineNumber)] : null;
2294         var firstUndamagedLineChunk = firstUndamagedLineNumber < this._textModel.linesCount ? this._textChunks[this.chunkNumberForLine(firstUndamagedLineNumber)] : null;
2295
2296         var collectLinesFromNode = lastUndamagedLineChunk ? lastUndamagedLineChunk.lineRowContainingLine(lastUndamagedLineNumber) : null;
2297         var collectLinesToNode = firstUndamagedLineChunk ? firstUndamagedLineChunk.lineRowContainingLine(firstUndamagedLineNumber) : null;
2298         var lines = this._collectLinesFromDOM(collectLinesFromNode, collectLinesToNode);
2299
2300         var startLine = dirtyLines.start;
2301         var endLine = dirtyLines.end;
2302
2303         var editInfo = this._guessEditRangeBasedOnSelection(startLine, endLine, lines);
2304         if (!editInfo) {
2305             if (WebInspector.debugDefaultTextEditor)
2306                 console.warn("Falling back to expensive edit");
2307             var range = new WebInspector.TextRange(startLine, 0, endLine, this._textModel.lineLength(endLine));
2308             if (!lines.length) {
2309                 // Entire damaged area has collapsed. Replace everything between start and end lines with nothing.
2310                 editInfo = new WebInspector.DefaultTextEditor.EditInfo(this._textModel.growRangeRight(range), "");
2311             } else
2312                 editInfo = new WebInspector.DefaultTextEditor.EditInfo(range, lines.join("\n"));
2313         }
2314
2315         var selection = this.selection(collectLinesFromNode);
2316
2317         // Unindent after block
2318         if (editInfo.text === "}" && editInfo.range.isEmpty() && selection.isEmpty() && !this._textModel.line(editInfo.range.endLine).trim()) {
2319             var offset = this._closingBlockOffset(editInfo.range, selection);
2320             if (offset >= 0) {
2321                 editInfo.range.startColumn = offset;
2322                 selection.startColumn = offset + 1;
2323                 selection.endColumn = offset + 1;
2324             }
2325         }
2326
2327         this._textModel.editRange(editInfo.range, editInfo.text);
2328         this._restoreSelection(selection);
2329     },
2330
2331     /**
2332      * @param {number} startLine
2333      * @param {number} endLine
2334      * @param {Array.<string>} lines
2335      * @return {?WebInspector.DefaultTextEditor.EditInfo}
2336      */
2337     _guessEditRangeBasedOnSelection: function(startLine, endLine, lines)
2338     {
2339         // Analyze input data
2340         var textInputData = this._textInputData;
2341         delete this._textInputData;
2342         var isBackspace = this._keyDownCode === WebInspector.KeyboardShortcut.Keys.Backspace.code;
2343         var isDelete = this._keyDownCode === WebInspector.KeyboardShortcut.Keys.Delete.code;
2344
2345         if (!textInputData && (isDelete || isBackspace))
2346             textInputData = "";
2347
2348         // Return if there is no input data or selection
2349         if (typeof textInputData === "undefined" || !this._lastSelection)
2350             return null;
2351
2352         // Adjust selection based on the keyboard actions (grow for backspace, etc.).
2353         textInputData = textInputData || "";
2354         var range = this._lastSelection.normalize();
2355         if (isBackspace && range.isEmpty())
2356             range = this._textModel.growRangeLeft(range);
2357         else if (isDelete && range.isEmpty())
2358             range = this._textModel.growRangeRight(range);
2359
2360         // Test that selection intersects damaged lines
2361         if (startLine > range.endLine || endLine < range.startLine)
2362             return null;
2363
2364         var replacementLineCount = textInputData.split("\n").length - 1;
2365         var lineCountDelta = replacementLineCount - range.linesCount;
2366         if (startLine + lines.length - endLine - 1 !== lineCountDelta)
2367             return null;
2368
2369         // Clone text model of the size that fits both: selection before edit and the damaged lines after edit.
2370         var cloneFromLine = Math.min(range.startLine, startLine);
2371         var postLastLine = startLine + lines.length + lineCountDelta;
2372         var cloneToLine = Math.min(Math.max(postLastLine, range.endLine) + 1, this._textModel.linesCount);
2373         var domModel = this._textModel.slice(cloneFromLine, cloneToLine);
2374         domModel.editRange(range.shift(-cloneFromLine), textInputData);
2375
2376         // Then we'll test if this new model matches the DOM lines.
2377         for (var i = 0; i < lines.length; ++i) {
2378             if (domModel.line(i + startLine - cloneFromLine) !== lines[i])
2379                 return null;
2380         }
2381         return new WebInspector.DefaultTextEditor.EditInfo(range, textInputData);
2382     },
2383
2384     _assertDOMMatchesTextModel: function()
2385     {
2386         if (!WebInspector.debugDefaultTextEditor)
2387             return;
2388
2389         console.assert(this.element.innerText === this._textModel.text() + "\n", "DOM does not match model.");
2390         for (var lineRow = this._container.firstChild; lineRow; lineRow = lineRow.nextSibling) {
2391             var lineNumber = lineRow.lineNumber;
2392             if (typeof lineNumber !== "number") {
2393                 console.warn("No line number on line row");
2394                 continue;
2395             }
2396             if (lineRow._chunk) {
2397                 var chunk = lineRow._chunk;
2398                 console.assert(lineNumber === chunk.startLine);
2399                 var chunkText = this._textModel.copyRange(new WebInspector.TextRange(chunk.startLine, 0, chunk.endLine - 1, this._textModel.lineLength(chunk.endLine - 1)));
2400                 if (chunkText !== lineRow.textContent)
2401                     console.warn("Chunk is not matching: %d %O", lineNumber, lineRow);
2402             } else if (this._textModel.line(lineNumber) !== lineRow.textContent)
2403                 console.warn("Line is not matching: %d %O", lineNumber, lineRow);
2404         }
2405     },
2406
2407     /**
2408      * @param {WebInspector.TextRange} oldRange
2409      * @param {WebInspector.TextRange} selection
2410      * @return {number}
2411      */
2412     _closingBlockOffset: function(oldRange, selection)
2413     {
2414         var nestingLevel = 1;
2415         for (var i = oldRange.endLine; i >= 0; --i) {
2416             var attribute = this._textModel.getAttribute(i, "highlight");
2417             if (!attribute)
2418                 continue;
2419             var ranges = attribute.ranges;
2420             for (var j = ranges.length - 1; j >= 0; j--) {
2421                 var token = ranges[j].token;
2422                 if (token === "block-start") {
2423                     if (!(--nestingLevel)) {
2424                         var lineContent = this._textModel.line(i);
2425                         return lineContent.length - lineContent.trimLeft().length;
2426                     }
2427                 }
2428                 if (token === "block-end")
2429                     ++nestingLevel;
2430             }
2431         }
2432         return -1;
2433     },
2434
2435     /**
2436      * @param {WebInspector.TextRange} oldRange
2437      * @param {WebInspector.TextRange} newRange
2438      */
2439     textChanged: function(oldRange, newRange)
2440     {
2441         this.beginDomUpdates();
2442         this._removeDecorationsInRange(oldRange);
2443         this._updateChunksForRanges(oldRange, newRange);
2444         this._updateHighlightsForRange(newRange);
2445         this.endDomUpdates();
2446     },
2447
2448     /**
2449      * @param {WebInspector.TextRange} range
2450      */
2451     _removeDecorationsInRange: function(range)
2452     {
2453         for (var i = this.chunkNumberForLine(range.startLine); i < this._textChunks.length; ++i) {
2454             var chunk = this._textChunks[i];
2455             if (chunk.startLine > range.endLine)
2456                 break;
2457             chunk.removeAllDecorations();
2458         }
2459     },
2460
2461     /**
2462      * @param {WebInspector.TextRange} oldRange
2463      * @param {WebInspector.TextRange} newRange
2464      */
2465     _updateChunksForRanges: function(oldRange, newRange)
2466     {
2467         var firstDamagedChunkNumber = this.chunkNumberForLine(oldRange.startLine);
2468         var lastDamagedChunkNumber = firstDamagedChunkNumber;
2469         while (lastDamagedChunkNumber + 1 < this._textChunks.length) {
2470             if (this._textChunks[lastDamagedChunkNumber + 1].startLine > oldRange.endLine)
2471                 break;
2472             ++lastDamagedChunkNumber;
2473         }
2474
2475         var firstDamagedChunk = this._textChunks[firstDamagedChunkNumber];
2476         var lastDamagedChunk = this._textChunks[lastDamagedChunkNumber];
2477
2478         var linesDiff = newRange.linesCount - oldRange.linesCount;
2479
2480         // First, detect chunks that have not been modified and simply shift them.
2481         if (linesDiff) {
2482             for (var chunkNumber = lastDamagedChunkNumber + 1; chunkNumber < this._textChunks.length; ++chunkNumber)
2483                 this._textChunks[chunkNumber].startLine += linesDiff;
2484         }
2485
2486         // Remove damaged chunks from DOM and from textChunks model.
2487         var lastUndamagedChunk = firstDamagedChunkNumber > 0 ? this._textChunks[firstDamagedChunkNumber - 1] : null;
2488         var firstUndamagedChunk = lastDamagedChunkNumber + 1 < this._textChunks.length ? this._textChunks[lastDamagedChunkNumber + 1] : null;
2489
2490         var removeDOMFromNode = lastUndamagedChunk ? lastUndamagedChunk.lastElement().nextSibling : this._container.firstChild;
2491         var removeDOMToNode = firstUndamagedChunk ? firstUndamagedChunk.firstElement() : null;
2492
2493         // Fast case - patch single expanded chunk that did not grow / shrink during edit.
2494         if (!linesDiff && firstDamagedChunk === lastDamagedChunk && firstDamagedChunk._expandedLineRows) {
2495             var lastUndamagedLineRow = lastDamagedChunk.expandedLineRow(oldRange.startLine - 1);
2496             var firstUndamagedLineRow = firstDamagedChunk.expandedLineRow(oldRange.endLine + 1);
2497             var localRemoveDOMFromNode = lastUndamagedLineRow ? lastUndamagedLineRow.nextSibling : removeDOMFromNode;
2498             var localRemoveDOMToNode = firstUndamagedLineRow || removeDOMToNode;
2499             removeSubsequentNodes(localRemoveDOMFromNode, localRemoveDOMToNode);
2500             for (var i = newRange.startLine; i < newRange.endLine + 1; ++i) {
2501                 var row = firstDamagedChunk._createRow(i);
2502                 firstDamagedChunk._expandedLineRows[i - firstDamagedChunk.startLine] = row;
2503                 this._container.insertBefore(row, localRemoveDOMToNode);
2504             }
2505             firstDamagedChunk.updateCollapsedLineRow();
2506             this._assertDOMMatchesTextModel();
2507             return;
2508         }
2509
2510         removeSubsequentNodes(removeDOMFromNode, removeDOMToNode);
2511         this._textChunks.splice(firstDamagedChunkNumber, lastDamagedChunkNumber - firstDamagedChunkNumber + 1);
2512
2513         // Compute damaged chunks span
2514         var startLine = firstDamagedChunk.startLine;
2515         var endLine = lastDamagedChunk.endLine + linesDiff;
2516         var lineSpan = endLine - startLine;
2517
2518         // Re-create chunks for damaged area.
2519         var insertionIndex = firstDamagedChunkNumber;
2520         var chunkSize = Math.ceil(lineSpan / Math.ceil(lineSpan / this._defaultChunkSize));
2521
2522         for (var i = startLine; i < endLine; i += chunkSize) {
2523             var chunk = this.createNewChunk(i, Math.min(endLine, i + chunkSize));
2524             this._textChunks.splice(insertionIndex++, 0, chunk);
2525             this._container.insertBefore(chunk.element, removeDOMToNode);
2526         }
2527
2528         this._assertDOMMatchesTextModel();
2529     },
2530
2531     /**
2532      * @param {WebInspector.TextRange} range
2533      */
2534     _updateHighlightsForRange: function(range)
2535     {
2536         var visibleFrom = this.scrollTop();
2537         var visibleTo = visibleFrom + this.clientHeight();
2538
2539         var result = this.findVisibleChunks(visibleFrom, visibleTo);
2540         var chunk = this._textChunks[result.end - 1];
2541         var lastVisibleLine = chunk.startLine + chunk.linesCount;
2542
2543         lastVisibleLine = Math.max(lastVisibleLine, range.endLine + 1);
2544         lastVisibleLine = Math.min(lastVisibleLine, this._textModel.linesCount);
2545
2546         var updated = this._highlighter.updateHighlight(range.startLine, lastVisibleLine);
2547         if (!updated) {
2548             // Highlights for the chunks below are invalid, so just collapse them.
2549             for (var i = this.chunkNumberForLine(range.startLine); i < this._textChunks.length; ++i)
2550                 this._textChunks[i].collapse();
2551         }
2552
2553         this.repaintAll();
2554     },
2555
2556     /**
2557      * @param {Node} from
2558      * @param {Node} to
2559      * @return {Array.<string>}
2560      */
2561     _collectLinesFromDOM: function(from, to)
2562     {
2563         var textContents = [];
2564         var hasContent = false;
2565         for (var node = from ? from.nextSibling : this._container; node && node !== to; node = node.traverseNextNode(this._container)) {
2566             if (node._isDecorationsElement) {
2567                 // Skip all children of the decoration container.
2568                 node = node.nextSibling;
2569                 if (!node || node === to)
2570                     break;
2571             }
2572             hasContent = true;
2573             if (node.nodeName.toLowerCase() === "br")
2574                 textContents.push("\n");
2575             else if (node.nodeType === Node.TEXT_NODE)
2576                 textContents.push(node.textContent);
2577         }
2578         if (!hasContent)
2579             return [];
2580
2581         var textContent = textContents.join("");
2582         // The last \n (if any) does not "count" in a DIV.
2583         textContent = textContent.replace(/\n$/, "");
2584
2585         return textContent.split("\n");
2586     },
2587
2588     /**
2589      * @param {Event} event
2590      */
2591     _handleSelectionChange: function(event)
2592     {
2593         var textRange = this.selection();
2594         if (textRange)
2595             this._lastSelection = textRange;
2596         this._delegate.selectionChanged(textRange);
2597     },
2598
2599     /**
2600      * @param {Event} event
2601      */
2602     _handleTextInput: function(event)
2603     {
2604         this._textInputData = event.data;
2605     },
2606
2607     /**
2608      * @param {Event} event
2609      */
2610     handleKeyDown: function(event)
2611     {
2612         this._keyDownCode = event.keyCode;
2613     },
2614
2615     /**
2616      * @param {Event} event
2617      */
2618     _handleCut: function(event)
2619     {
2620         this._keyDownCode = WebInspector.KeyboardShortcut.Keys.Delete.code;
2621     },
2622
2623     /**
2624      * @param {number} scrollTop
2625      * @param {number} clientHeight
2626      * @param {number} chunkSize
2627      */
2628     overrideViewportForTest: function(scrollTop, clientHeight, chunkSize)
2629     {
2630         this._scrollTopOverrideForTest = scrollTop;
2631         this._clientHeightOverrideForTest = clientHeight;
2632         this._defaultChunkSize = chunkSize;
2633     },
2634
2635     __proto__: WebInspector.TextEditorChunkedPanel.prototype
2636 }
2637
2638 /**
2639  * @constructor
2640  * @param {WebInspector.TextEditorChunkedPanel} chunkedPanel
2641  * @param {number} startLine
2642  * @param {number} endLine
2643  */
2644 WebInspector.TextEditorMainChunk = function(chunkedPanel, startLine, endLine)
2645 {
2646     this._chunkedPanel = chunkedPanel;
2647     this._textModel = chunkedPanel._textModel;
2648
2649     this.element = document.createElement("div");
2650     this.element.lineNumber = startLine;
2651     this.element.className = "webkit-line-content";
2652     this.element._chunk = this;
2653
2654     this._startLine = startLine;
2655     endLine = Math.min(this._textModel.linesCount, endLine);
2656     this.linesCount = endLine - startLine;
2657
2658     this._expanded = false;
2659
2660     this.updateCollapsedLineRow();
2661 }
2662
2663 WebInspector.TextEditorMainChunk.prototype = {
2664     /**
2665      * @param {Element|string} decoration
2666      */
2667     addDecoration: function(decoration)
2668     {
2669         this._chunkedPanel.beginDomUpdates();
2670         if (typeof decoration === "string")
2671             this.element.addStyleClass(decoration);
2672         else {
2673             if (!this.element.decorationsElement) {
2674                 this.element.decorationsElement = document.createElement("div");
2675                 this.element.decorationsElement.className = "webkit-line-decorations";
2676                 this.element.decorationsElement._isDecorationsElement = true;
2677                 this.element.appendChild(this.element.decorationsElement);
2678             }
2679             this.element.decorationsElement.appendChild(decoration);
2680         }
2681         this._chunkedPanel.endDomUpdates();
2682     },
2683
2684     /**
2685      * @param {string|Element} decoration
2686      */
2687     removeDecoration: function(decoration)
2688     {
2689         this._chunkedPanel.beginDomUpdates();
2690         if (typeof decoration === "string")
2691             this.element.removeStyleClass(decoration);
2692         else if (this.element.decorationsElement)
2693             this.element.decorationsElement.removeChild(decoration);
2694         this._chunkedPanel.endDomUpdates();
2695     },
2696
2697     removeAllDecorations: function()
2698     {
2699         this._chunkedPanel.beginDomUpdates();
2700         this.element.className = "webkit-line-content";
2701         if (this.element.decorationsElement) {
2702             this.element.removeChild(this.element.decorationsElement);
2703             delete this.element.decorationsElement;
2704         }
2705         this._chunkedPanel.endDomUpdates();
2706     },
2707
2708     /**
2709      * @return {boolean}
2710      */
2711     isDecorated: function()
2712     {
2713         return this.element.className !== "webkit-line-content" || !!(this.element.decorationsElement && this.element.decorationsElement.firstChild);
2714     },
2715
2716     /**
2717      * @return {number}
2718      */
2719     get startLine()
2720     {
2721         return this._startLine;
2722     },
2723
2724     /**
2725      * @return {number}
2726      */
2727     get endLine()
2728     {
2729         return this._startLine + this.linesCount;
2730     },
2731
2732     set startLine(startLine)
2733     {
2734         this._startLine = startLine;
2735         this.element.lineNumber = startLine;
2736         if (this._expandedLineRows) {
2737             for (var i = 0; i < this._expandedLineRows.length; ++i)
2738                 this._expandedLineRows[i].lineNumber = startLine + i;
2739         }
2740     },
2741
2742     /**
2743      * @return {boolean}
2744      */
2745     expanded: function()
2746     {
2747         return this._expanded;
2748     },
2749
2750     expand: function()
2751     {
2752         if (this._expanded)
2753             return;
2754
2755         this._expanded = true;
2756
2757         if (this.linesCount === 1) {
2758             this._chunkedPanel._paintLine(this.element);
2759             return;
2760         }
2761
2762         this._chunkedPanel.beginDomUpdates();
2763
2764         this._expandedLineRows = [];
2765         var parentElement = this.element.parentElement;
2766         for (var i = this.startLine; i < this.startLine + this.linesCount; ++i) {
2767             var lineRow = this._createRow(i);
2768             parentElement.insertBefore(lineRow, this.element);
2769             this._expandedLineRows.push(lineRow);
2770         }
2771         parentElement.removeChild(this.element);
2772         this._chunkedPanel._paintLines(this.startLine, this.startLine + this.linesCount);
2773
2774         this._chunkedPanel.endDomUpdates();
2775     },
2776
2777     collapse: function()
2778     {
2779         if (!this._expanded)
2780             return;
2781
2782         this._expanded = false;
2783         if (this.linesCount === 1)
2784             return;
2785
2786         this._chunkedPanel.beginDomUpdates();
2787
2788         var elementInserted = false;
2789         for (var i = 0; i < this._expandedLineRows.length; ++i) {
2790             var lineRow = this._expandedLineRows[i];
2791             var parentElement = lineRow.parentElement;
2792             if (parentElement) {
2793                 if (!elementInserted) {
2794                     elementInserted = true;
2795                     parentElement.insertBefore(this.element, lineRow);
2796                 }
2797                 parentElement.removeChild(lineRow);
2798             }
2799             this._chunkedPanel._releaseLinesHighlight(lineRow);
2800         }
2801         delete this._expandedLineRows;
2802
2803         this._chunkedPanel.endDomUpdates();
2804     },
2805
2806     /**
2807      * @return {number}
2808      */
2809     get height()
2810     {
2811         if (!this._expandedLineRows)
2812             return this._chunkedPanel.totalHeight(this.element);
2813         return this._chunkedPanel.totalHeight(this._expandedLineRows[0], this._expandedLineRows[this._expandedLineRows.length - 1]);
2814     },
2815
2816     /**
2817      * @return {number}
2818      */
2819     get offsetTop()
2820     {
2821         return (this._expandedLineRows && this._expandedLineRows.length) ? this._expandedLineRows[0].offsetTop : this.element.offsetTop;
2822     },
2823
2824     /**
2825      * @param {number} lineNumber
2826      * @return {Element}
2827      */
2828     _createRow: function(lineNumber)
2829     {
2830         var lineRow = this._chunkedPanel._cachedRows.pop() || document.createElement("div");
2831         lineRow.lineNumber = lineNumber;
2832         lineRow.className = "webkit-line-content";
2833         lineRow.textContent = this._textModel.line(lineNumber);
2834         if (!lineRow.textContent)
2835             lineRow.appendChild(document.createElement("br"));
2836         return lineRow;
2837     },
2838
2839     /**
2840      * Called on potentially damaged / inconsistent chunk
2841      * @param {number} lineNumber
2842      * @return {?Node}
2843      */
2844     lineRowContainingLine: function(lineNumber)
2845     {
2846         if (!this._expanded)
2847             return this.element;
2848         return this.expandedLineRow(lineNumber);
2849     },
2850
2851     /**
2852      * @param {number} lineNumber
2853      * @return {Element}
2854      */
2855     expandedLineRow: function(lineNumber)
2856     {
2857         if (!this._expanded || lineNumber < this.startLine || lineNumber >= this.startLine + this.linesCount)
2858             return null;
2859         if (!this._expandedLineRows)
2860             return this.element;
2861         return this._expandedLineRows[lineNumber - this.startLine];
2862     },
2863
2864     updateCollapsedLineRow: function()
2865     {
2866         if (this.linesCount === 1 && this._expanded)
2867             return;
2868
2869         var lines = [];
2870         for (var i = this.startLine; i < this.startLine + this.linesCount; ++i)
2871             lines.push(this._textModel.line(i));
2872
2873         if (WebInspector.FALSE)
2874             console.log("Rebuilding chunk with " + lines.length + " lines");
2875
2876         this.element.removeChildren();
2877         this.element.textContent = lines.join("\n");
2878         // The last empty line will get swallowed otherwise.
2879         if (!lines[lines.length - 1])
2880             this.element.appendChild(document.createElement("br"));
2881     },
2882
2883     firstElement: function()
2884     {
2885         return this._expandedLineRows ? this._expandedLineRows[0] : this.element;
2886     },
2887
2888     /**
2889      * @return {Element}
2890      */
2891     lastElement: function()
2892     {
2893         return this._expandedLineRows ? this._expandedLineRows[this._expandedLineRows.length - 1] : this.element;
2894     }
2895 }
2896
2897 WebInspector.debugDefaultTextEditor = false;