Web Inspector: highlight occurences of word in DefaultTextEditor
[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 span = (visibleTo - visibleFrom) * 0.5;
898         visibleFrom = Math.max(visibleFrom - span, 0);
899         visibleTo = visibleTo + span;
900
901         var from = this._findFirstVisibleChunkNumber(visibleFrom);
902         for (var to = from + 1; to < this._textChunks.length; ++to) {
903             if (this._textChunks[to].offsetTop >= visibleTo)
904                 break;
905         }
906         return { start: from, end: to };
907     },
908
909     /**
910      * @param {number} visibleFrom
911      * @return {number}
912      */
913     lineNumberAtOffset: function(visibleFrom)
914     {
915         var chunk = this._textChunks[this._findFirstVisibleChunkNumber(visibleFrom)];
916         if (!chunk.expanded())
917             return chunk.startLine;
918
919         var lineNumbers = [];
920         for (var i = 0; i < chunk.linesCount; ++i) {
921             lineNumbers.push(chunk.startLine + i);
922         }
923
924         function compareLineRowOffsetTops(value, lineNumber)
925         {
926             var lineRow = chunk.expandedLineRow(lineNumber);
927             return value < lineRow.offsetTop ? -1 : 1;
928         }
929         var insertBefore = insertionIndexForObjectInListSortedByFunction(visibleFrom, lineNumbers, compareLineRowOffsetTops);
930         return lineNumbers[insertBefore - 1];
931     },
932
933     repaintAll: function()
934     {
935         delete this._repaintAllTimer;
936
937         if (this._paintCoalescingLevel)
938             return;
939
940         var visibleFrom = this.scrollTop();
941         var visibleTo = visibleFrom + this.clientHeight();
942
943         if (visibleTo) {
944             var result = this.findVisibleChunks(visibleFrom, visibleTo);
945             this.expandChunks(result.start, result.end);
946         }
947     },
948
949     scrollTop: function()
950     {
951         return typeof this._scrollTopOverrideForTest === "number" ? this._scrollTopOverrideForTest : this.element.scrollTop;
952     },
953
954     clientHeight: function()
955     {
956         return typeof this._clientHeightOverrideForTest === "number" ? this._clientHeightOverrideForTest : this.element.clientHeight;
957     },
958
959     /**
960      * @param {number} fromIndex
961      * @param {number} toIndex
962      */
963     expandChunks: function(fromIndex, toIndex)
964     {
965         // First collapse chunks to collect the DOM elements into a cache to reuse them later.
966         for (var i = 0; i < fromIndex; ++i)
967             this._textChunks[i].collapse();
968         for (var i = toIndex; i < this._textChunks.length; ++i)
969             this._textChunks[i].collapse();
970         for (var i = fromIndex; i < toIndex; ++i)
971             this._textChunks[i].expand();
972     },
973
974     /**
975      * @param {Element} firstElement
976      * @param {Element=} lastElement
977      * @return {number}
978      */
979     totalHeight: function(firstElement, lastElement)
980     {
981         lastElement = (lastElement || firstElement).nextElementSibling;
982         if (lastElement)
983             return lastElement.offsetTop - firstElement.offsetTop;
984
985         var offsetParent = firstElement.offsetParent;
986         if (offsetParent && offsetParent.scrollHeight > offsetParent.clientHeight)
987             return offsetParent.scrollHeight - firstElement.offsetTop;
988
989         var total = 0;
990         while (firstElement && firstElement !== lastElement) {
991             total += firstElement.offsetHeight;
992             firstElement = firstElement.nextElementSibling;
993         }
994         return total;
995     },
996
997     resize: function()
998     {
999         this.repaintAll();
1000     }
1001 }
1002
1003 /**
1004  * @constructor
1005  * @extends {WebInspector.TextEditorChunkedPanel}
1006  * @param {WebInspector.TextEditorModel} textModel
1007  * @param {function(number)} syncDecorationsForLineListener
1008  * @param {function(Element)} syncLineHeightListener
1009  */
1010 WebInspector.TextEditorGutterPanel = function(textModel, syncDecorationsForLineListener, syncLineHeightListener)
1011 {
1012     WebInspector.TextEditorChunkedPanel.call(this, textModel);
1013
1014     this._syncDecorationsForLineListener = syncDecorationsForLineListener;
1015     this._syncLineHeightListener = syncLineHeightListener;
1016
1017     this.element.className = "text-editor-lines";
1018
1019     this._container = document.createElement("div");
1020     this._container.className = "inner-container";
1021     this.element.appendChild(this._container);
1022
1023     this._freeCachedElements();
1024     this.buildChunks();
1025     this._decorations = {};
1026 }
1027
1028 WebInspector.TextEditorGutterPanel.prototype = {
1029     _freeCachedElements: function()
1030     {
1031         this._cachedRows = [];
1032     },
1033
1034     willHide: function()
1035     {
1036         this._freeCachedElements();
1037     },
1038
1039     /**
1040      * @param {number} startLine
1041      * @param {number} endLine
1042      * @return {WebInspector.TextEditorGutterChunk}
1043      */
1044     createNewChunk: function(startLine, endLine)
1045     {
1046         return new WebInspector.TextEditorGutterChunk(this, startLine, endLine);
1047     },
1048
1049     /**
1050      * @param {WebInspector.TextRange} oldRange
1051      * @param {WebInspector.TextRange} newRange
1052      */
1053     textChanged: function(oldRange, newRange)
1054     {
1055         this.beginDomUpdates();
1056
1057         var linesDiff = newRange.linesCount - oldRange.linesCount;
1058         if (linesDiff) {
1059             // Remove old chunks (if needed).
1060             for (var chunkNumber = this._textChunks.length - 1; chunkNumber >= 0; --chunkNumber) {
1061                 var chunk = this._textChunks[chunkNumber];
1062                 if (chunk.startLine + chunk.linesCount <= this._textModel.linesCount)
1063                     break;
1064                 chunk.collapse();
1065                 this._container.removeChild(chunk.element);
1066             }
1067             this._textChunks.length = chunkNumber + 1;
1068
1069             // Add new chunks (if needed).
1070             var totalLines = 0;
1071             if (this._textChunks.length) {
1072                 var lastChunk = this._textChunks[this._textChunks.length - 1];
1073                 totalLines = lastChunk.startLine + lastChunk.linesCount;
1074             }
1075
1076             for (var i = totalLines; i < this._textModel.linesCount; i += this._defaultChunkSize) {
1077                 var chunk = this.createNewChunk(i, i + this._defaultChunkSize);
1078                 this._textChunks.push(chunk);
1079                 this._container.appendChild(chunk.element);
1080             }
1081
1082             // Shift decorations if necessary
1083             for (var lineNumber in this._decorations) {
1084                 lineNumber = parseInt(lineNumber, 10);
1085
1086                 // Do not move decorations before the start position.
1087                 if (lineNumber < oldRange.startLine)
1088                     continue;
1089                 // Decorations follow the first character of line.
1090                 if (lineNumber === oldRange.startLine && oldRange.startColumn)
1091                     continue;
1092
1093                 var lineDecorationsCopy = this._decorations[lineNumber].slice();
1094                 for (var i = 0; i < lineDecorationsCopy.length; ++i) {
1095                     var decoration = lineDecorationsCopy[i];
1096                     this.removeDecoration(lineNumber, decoration);
1097
1098                     // Do not restore the decorations before the end position.
1099                     if (lineNumber < oldRange.endLine)
1100                         continue;
1101
1102                     this.addDecoration(lineNumber + linesDiff, decoration);
1103                 }
1104             }
1105
1106             this.repaintAll();
1107         } else {
1108             // Decorations may have been removed, so we may have to sync those lines.
1109             var chunkNumber = this.chunkNumberForLine(newRange.startLine);
1110             var chunk = this._textChunks[chunkNumber];
1111             while (chunk && chunk.startLine <= newRange.endLine) {
1112                 if (chunk.linesCount === 1)
1113                     this._syncDecorationsForLineListener(chunk.startLine);
1114                 chunk = this._textChunks[++chunkNumber];
1115             }
1116         }
1117
1118         this.endDomUpdates();
1119     },
1120
1121     /**
1122      * @param {number} clientHeight
1123      */
1124     syncClientHeight: function(clientHeight)
1125     {
1126         if (this.element.offsetHeight > clientHeight)
1127             this._container.style.setProperty("padding-bottom", (this.element.offsetHeight - clientHeight) + "px");
1128         else
1129             this._container.style.removeProperty("padding-bottom");
1130     },
1131
1132     /**
1133      * @param {number} lineNumber
1134      * @param {string|Element} decoration
1135      */
1136     addDecoration: function(lineNumber, decoration)
1137     {
1138         WebInspector.TextEditorChunkedPanel.prototype.addDecoration.call(this, lineNumber, decoration);
1139         var decorations = this._decorations[lineNumber];
1140         if (!decorations) {
1141             decorations = [];
1142             this._decorations[lineNumber] = decorations;
1143         }
1144         decorations.push(decoration);
1145     },
1146
1147     /**
1148      * @param {number} lineNumber
1149      * @param {string|Element} decoration
1150      */
1151     removeDecoration: function(lineNumber, decoration)
1152     {
1153         WebInspector.TextEditorChunkedPanel.prototype.removeDecoration.call(this, lineNumber, decoration);
1154         var decorations = this._decorations[lineNumber];
1155         if (decorations) {
1156             decorations.remove(decoration);
1157             if (!decorations.length)
1158                 delete this._decorations[lineNumber];
1159         }
1160     },
1161
1162     __proto__: WebInspector.TextEditorChunkedPanel.prototype
1163 }
1164
1165 /**
1166  * @constructor
1167  * @param {WebInspector.TextEditorGutterPanel} chunkedPanel
1168  * @param {number} startLine
1169  * @param {number} endLine
1170  */
1171 WebInspector.TextEditorGutterChunk = function(chunkedPanel, startLine, endLine)
1172 {
1173     this._chunkedPanel = chunkedPanel;
1174     this._textModel = chunkedPanel._textModel;
1175
1176     this.startLine = startLine;
1177     endLine = Math.min(this._textModel.linesCount, endLine);
1178     this.linesCount = endLine - startLine;
1179
1180     this._expanded = false;
1181
1182     this.element = document.createElement("div");
1183     this.element.lineNumber = startLine;
1184     this.element.className = "webkit-line-number";
1185
1186     if (this.linesCount === 1) {
1187         // Single line chunks are typically created for decorations. Host line number in
1188         // the sub-element in order to allow flexible border / margin management.
1189         var innerSpan = document.createElement("span");
1190         innerSpan.className = "webkit-line-number-inner";
1191         innerSpan.textContent = startLine + 1;
1192         var outerSpan = document.createElement("div");
1193         outerSpan.className = "webkit-line-number-outer";
1194         outerSpan.appendChild(innerSpan);
1195         this.element.appendChild(outerSpan);
1196     } else {
1197         var lineNumbers = [];
1198         for (var i = startLine; i < endLine; ++i)
1199             lineNumbers.push(i + 1);
1200         this.element.textContent = lineNumbers.join("\n");
1201     }
1202 }
1203
1204 WebInspector.TextEditorGutterChunk.prototype = {
1205     /**
1206      * @param {string} decoration
1207      */
1208     addDecoration: function(decoration)
1209     {
1210         this._chunkedPanel.beginDomUpdates();
1211         if (typeof decoration === "string")
1212             this.element.addStyleClass(decoration);
1213         this._chunkedPanel.endDomUpdates();
1214     },
1215
1216     /**
1217      * @param {string} decoration
1218      */
1219     removeDecoration: function(decoration)
1220     {
1221         this._chunkedPanel.beginDomUpdates();
1222         if (typeof decoration === "string")
1223             this.element.removeStyleClass(decoration);
1224         this._chunkedPanel.endDomUpdates();
1225     },
1226
1227     /**
1228      * @return {boolean}
1229      */
1230     expanded: function()
1231     {
1232         return this._expanded;
1233     },
1234
1235     expand: function()
1236     {
1237         if (this.linesCount === 1)
1238             this._chunkedPanel._syncDecorationsForLineListener(this.startLine);
1239
1240         if (this._expanded)
1241             return;
1242
1243         this._expanded = true;
1244
1245         if (this.linesCount === 1)
1246             return;
1247
1248         this._chunkedPanel.beginDomUpdates();
1249
1250         this._expandedLineRows = [];
1251         var parentElement = this.element.parentElement;
1252         for (var i = this.startLine; i < this.startLine + this.linesCount; ++i) {
1253             var lineRow = this._createRow(i);
1254             parentElement.insertBefore(lineRow, this.element);
1255             this._expandedLineRows.push(lineRow);
1256         }
1257         parentElement.removeChild(this.element);
1258         this._chunkedPanel._syncLineHeightListener(this._expandedLineRows[0]);
1259
1260         this._chunkedPanel.endDomUpdates();
1261     },
1262
1263     collapse: function()
1264     {
1265         if (this.linesCount === 1)
1266             this._chunkedPanel._syncDecorationsForLineListener(this.startLine);
1267
1268         if (!this._expanded)
1269             return;
1270
1271         this._expanded = false;
1272
1273         if (this.linesCount === 1)
1274             return;
1275
1276         this._chunkedPanel.beginDomUpdates();
1277
1278         var elementInserted = false;
1279         for (var i = 0; i < this._expandedLineRows.length; ++i) {
1280             var lineRow = this._expandedLineRows[i];
1281             var parentElement = lineRow.parentElement;
1282             if (parentElement) {
1283                 if (!elementInserted) {
1284                     elementInserted = true;
1285                     parentElement.insertBefore(this.element, lineRow);
1286                 }
1287                 parentElement.removeChild(lineRow);
1288             }
1289             this._chunkedPanel._cachedRows.push(lineRow);
1290         }
1291         delete this._expandedLineRows;
1292
1293         this._chunkedPanel.endDomUpdates();
1294     },
1295
1296     /**
1297      * @return {number}
1298      */
1299     get height()
1300     {
1301         if (!this._expandedLineRows)
1302             return this._chunkedPanel.totalHeight(this.element);
1303         return this._chunkedPanel.totalHeight(this._expandedLineRows[0], this._expandedLineRows[this._expandedLineRows.length - 1]);
1304     },
1305
1306     /**
1307      * @return {number}
1308      */
1309     get offsetTop()
1310     {
1311         return (this._expandedLineRows && this._expandedLineRows.length) ? this._expandedLineRows[0].offsetTop : this.element.offsetTop;
1312     },
1313
1314     /**
1315      * @param {number} lineNumber
1316      * @return {Element}
1317      */
1318     _createRow: function(lineNumber)
1319     {
1320         var lineRow = this._chunkedPanel._cachedRows.pop() || document.createElement("div");
1321         lineRow.lineNumber = lineNumber;
1322         lineRow.className = "webkit-line-number";
1323         lineRow.textContent = lineNumber + 1;
1324         return lineRow;
1325     }
1326 }
1327
1328 /**
1329  * @constructor
1330  * @extends {WebInspector.TextEditorChunkedPanel}
1331  * @param {WebInspector.TextEditorDelegate} delegate
1332  * @param {WebInspector.TextEditorModel} textModel
1333  * @param {?string} url
1334  * @param {function()} syncScrollListener
1335  * @param {function(number)} syncDecorationsForLineListener
1336  */
1337 WebInspector.TextEditorMainPanel = function(delegate, textModel, url, syncScrollListener, syncDecorationsForLineListener)
1338 {
1339     WebInspector.TextEditorChunkedPanel.call(this, textModel);
1340
1341     this._delegate = delegate;
1342     this._syncScrollListener = syncScrollListener;
1343     this._syncDecorationsForLineListener = syncDecorationsForLineListener;
1344
1345     this._url = url;
1346     this._highlighter = new WebInspector.TextEditorHighlighter(textModel, this._highlightDataReady.bind(this));
1347     this._readOnly = true;
1348
1349     this.element.className = "text-editor-contents";
1350     this.element.tabIndex = 0;
1351
1352     this._container = document.createElement("div");
1353     this._container.className = "inner-container";
1354     this._container.tabIndex = 0;
1355     this.element.appendChild(this._container);
1356
1357     this.element.addEventListener("focus", this._handleElementFocus.bind(this), false);
1358     this.element.addEventListener("textInput", this._handleTextInput.bind(this), false);
1359     this.element.addEventListener("cut", this._handleCut.bind(this), false);
1360
1361     this._container.addEventListener("focus", this._handleFocused.bind(this), false);
1362
1363     this._highlightRegexs = {};
1364
1365     this._tokenHighlighter = new WebInspector.TextEditorMainPanel.TokenHighlighter(this, textModel);
1366
1367     this._freeCachedElements();
1368     this.buildChunks();
1369 }
1370
1371 WebInspector.TextEditorMainPanel.prototype = {
1372     /**
1373      * @param {string} regex
1374      * @param {string} cssClass
1375      */
1376     highlightRegex: function(regex, cssClass)
1377     {
1378         this._highlightRegexs[regex] = {
1379             regex: new RegExp(regex, "g"),
1380             cssClass: cssClass
1381         };
1382         this._repaintVisibleChunks();
1383     },
1384
1385     /**
1386      * @param {string} regex
1387      * @return {boolean}
1388      */
1389     removeRegexHighlight: function(regex)
1390     {
1391         var result = delete this._highlightRegexs[regex];
1392         this._repaintVisibleChunks();
1393         return result;
1394     },
1395
1396     _repaintVisibleChunks: function()
1397     {
1398         var visibleFrom = this.scrollTop();
1399         var visibleTo = visibleFrom + this.clientHeight();
1400
1401         var visibleChunks = this.findVisibleChunks(visibleFrom, visibleTo);
1402         var selection = this.selection();
1403
1404         for(var i = visibleChunks.start; i < visibleChunks.end; ++i) {
1405             var chunk = this._textChunks[i];
1406             this._paintLines(chunk._startLine, chunk._startLine + chunk.linesCount);
1407         }
1408         this._restoreSelection(selection);
1409     },
1410
1411     wasShown: function()
1412     {
1413         this._boundSelectionChangeListener = this._handleSelectionChange.bind(this);
1414         document.addEventListener("selectionchange", this._boundSelectionChangeListener, false);
1415
1416         this._isShowing = true;
1417         this._attachMutationObserver();
1418     },
1419
1420     willHide: function()
1421     {
1422         document.removeEventListener("selectionchange", this._boundSelectionChangeListener, false);
1423         delete this._boundSelectionChangeListener;
1424
1425         this._detachMutationObserver();
1426         this._isShowing = false;
1427         this._freeCachedElements();
1428     },
1429
1430     /**
1431      * @param {Element} eventTarget
1432      * @param {WebInspector.ContextMenu} contextMenu
1433      */
1434     populateContextMenu: function(eventTarget, contextMenu)
1435     {
1436         var target = this._enclosingLineRowOrSelf(eventTarget);
1437         this._delegate.populateTextAreaContextMenu(contextMenu, target && target.lineNumber);
1438     },
1439
1440     /**
1441      * @param {WebInspector.TextRange} textRange
1442      */
1443     setSelection: function(textRange)
1444     {
1445         this._lastSelection = textRange;
1446         if (this.element.isAncestor(document.activeElement))
1447             this._restoreSelection(textRange);
1448     },
1449
1450     _handleFocused: function()
1451     {
1452         if (this._lastSelection)
1453             this.setSelection(this._lastSelection);
1454     },
1455
1456     _attachMutationObserver: function()
1457     {
1458         if (!this._isShowing)
1459             return;
1460
1461         if (this._mutationObserver)
1462             this._mutationObserver.disconnect();
1463         this._mutationObserver = new NonLeakingMutationObserver(this._handleMutations.bind(this));
1464         this._mutationObserver.observe(this._container, { subtree: true, childList: true, characterData: true });
1465     },
1466
1467     _detachMutationObserver: function()
1468     {
1469         if (!this._isShowing)
1470             return;
1471
1472         if (this._mutationObserver) {
1473             this._mutationObserver.disconnect();
1474             delete this._mutationObserver;
1475         }
1476     },
1477
1478     /**
1479      * @param {string} mimeType
1480      */
1481     set mimeType(mimeType)
1482     {
1483         this._highlighter.mimeType = mimeType;
1484     },
1485
1486     /**
1487      * @param {boolean} readOnly
1488      * @param {boolean} requestFocus
1489      */
1490     setReadOnly: function(readOnly, requestFocus)
1491     {
1492         if (this._readOnly === readOnly)
1493             return;
1494
1495         this.beginDomUpdates();
1496         this._readOnly = readOnly;
1497         if (this._readOnly)
1498             this._container.removeStyleClass("text-editor-editable");
1499         else {
1500             this._container.addStyleClass("text-editor-editable");
1501             if (requestFocus)
1502                 this._updateSelectionOnStartEditing();
1503         }
1504         this.endDomUpdates();
1505     },
1506
1507     /**
1508      * @return {boolean}
1509      */
1510     readOnly: function()
1511     {
1512         return this._readOnly;
1513     },
1514
1515     _handleElementFocus: function()
1516     {
1517         if (!this._readOnly)
1518             this._container.focus();
1519     },
1520
1521     /**
1522      * @return {Element}
1523      */
1524     defaultFocusedElement: function()
1525     {
1526         if (this._readOnly)
1527             return this.element;
1528         return this._container;
1529     },
1530
1531     _updateSelectionOnStartEditing: function()
1532     {
1533         // focus() needs to go first for the case when the last selection was inside the editor and
1534         // the "Edit" button was clicked. In this case we bail at the check below, but the
1535         // editor does not receive the focus, thus "Esc" does not cancel editing until at least
1536         // one change has been made to the editor contents.
1537         this._container.focus();
1538         var selection = window.getSelection();
1539         if (selection.rangeCount) {
1540             var commonAncestorContainer = selection.getRangeAt(0).commonAncestorContainer;
1541             if (this._container.isSelfOrAncestor(commonAncestorContainer))
1542                 return;
1543         }
1544
1545         selection.removeAllRanges();
1546         var range = document.createRange();
1547         range.setStart(this._container, 0);
1548         range.setEnd(this._container, 0);
1549         selection.addRange(range);
1550     },
1551
1552     /**
1553      * @param {WebInspector.TextRange} range
1554      */
1555     markAndRevealRange: function(range)
1556     {
1557         if (this._rangeToMark) {
1558             var markedLine = this._rangeToMark.startLine;
1559             delete this._rangeToMark;
1560             // Remove the marked region immediately.
1561             this.beginDomUpdates();
1562             var chunk = this.chunkForLine(markedLine);
1563             var wasExpanded = chunk.expanded();
1564             chunk.collapse();
1565             chunk.updateCollapsedLineRow();
1566             if (wasExpanded)
1567                 chunk.expand();
1568             this.endDomUpdates();
1569         }
1570
1571         if (range) {
1572             this._rangeToMark = range;
1573             this.revealLine(range.startLine);
1574             var chunk = this.makeLineAChunk(range.startLine);
1575             this._paintLines(chunk.startLine, chunk.startLine + 1);
1576             if (this._markedRangeElement)
1577                 this._markedRangeElement.scrollIntoViewIfNeeded();
1578         }
1579         delete this._markedRangeElement;
1580     },
1581
1582     /**
1583      * @param {number} lineNumber
1584      */
1585     highlightLine: function(lineNumber)
1586     {
1587         this.clearLineHighlight();
1588         this._highlightedLine = lineNumber;
1589         this.revealLine(lineNumber);
1590
1591         if (!this._readOnly)
1592             this._restoreSelection(WebInspector.TextRange.createFromLocation(lineNumber, 0), false);
1593
1594         this.addDecoration(lineNumber, "webkit-highlighted-line");
1595     },
1596
1597     clearLineHighlight: function()
1598     {
1599         if (typeof this._highlightedLine === "number") {
1600             this.removeDecoration(this._highlightedLine, "webkit-highlighted-line");
1601             delete this._highlightedLine;
1602         }
1603     },
1604
1605     _freeCachedElements: function()
1606     {
1607         this._cachedSpans = [];
1608         this._cachedTextNodes = [];
1609         this._cachedRows = [];
1610     },
1611
1612     /**
1613      * @param {boolean} redo
1614      * @return {boolean}
1615      */
1616     handleUndoRedo: function(redo)
1617     {
1618         if (this.readOnly())
1619             return false;
1620
1621         this.beginUpdates();
1622
1623         var range = redo ? this._textModel.redo() : this._textModel.undo();
1624
1625         this.endUpdates();
1626
1627         // Restore location post-repaint.
1628         if (range)
1629             this._restoreSelection(range, true);
1630
1631         return true;
1632     },
1633
1634     /**
1635      * @param {boolean} shiftKey
1636      * @return {boolean}
1637      */
1638     handleTabKeyPress: function(shiftKey)
1639     {
1640         if (this.readOnly())
1641             return false;
1642
1643         var selection = this.selection();
1644         if (!selection)
1645             return false;
1646
1647         var range = selection.normalize();
1648
1649         this.beginUpdates();
1650
1651         var newRange;
1652         var rangeWasEmpty = range.isEmpty();
1653         if (shiftKey)
1654             newRange = this._textModel.unindentLines(range);
1655         else {
1656             if (rangeWasEmpty)
1657                 newRange = this._textModel.editRange(range, WebInspector.settings.textEditorIndent.get());
1658             else
1659                 newRange = this._textModel.indentLines(range);
1660         }
1661
1662         this.endUpdates();
1663         if (rangeWasEmpty)
1664             newRange.startColumn = newRange.endColumn;
1665         this._restoreSelection(newRange, true);
1666         return true;
1667     },
1668
1669     handleEnterKey: function()
1670     {
1671         if (this.readOnly())
1672             return false;
1673
1674         var range = this.selection();
1675         if (!range)
1676             return false;
1677
1678         range = range.normalize();
1679
1680         if (range.endColumn === 0)
1681             return false;
1682
1683         var line = this._textModel.line(range.startLine);
1684         var linePrefix = line.substring(0, range.startColumn);
1685         var indentMatch = linePrefix.match(/^\s+/);
1686         var currentIndent = indentMatch ? indentMatch[0] : "";
1687
1688         var textEditorIndent = WebInspector.settings.textEditorIndent.get();
1689         var indent = WebInspector.TextEditorModel.endsWithBracketRegex.test(linePrefix) ? currentIndent + textEditorIndent : currentIndent;
1690
1691         if (!indent)
1692             return false;
1693
1694         this.beginDomUpdates();
1695
1696         var lineBreak = this._textModel.lineBreak;
1697         var newRange;
1698         if (range.isEmpty() && line.substr(range.endColumn - 1, 2) === '{}') {
1699             // {|}
1700             // becomes
1701             // {
1702             //     |
1703             // }
1704             newRange = this._textModel.editRange(range, lineBreak + indent + lineBreak + currentIndent);
1705             newRange.endLine--;
1706             newRange.endColumn += textEditorIndent.length;
1707         } else
1708             newRange = this._textModel.editRange(range, lineBreak + indent);
1709
1710         this.endDomUpdates();
1711         this._restoreSelection(newRange.collapseToEnd(), true);
1712
1713         return true;
1714     },
1715
1716     /**
1717      * @param {number} lineNumber
1718      * @param {number} chunkNumber
1719      * @param {boolean=} createSuffixChunk
1720      * @return {Object}
1721      */
1722     splitChunkOnALine: function(lineNumber, chunkNumber, createSuffixChunk)
1723     {
1724         var selection = this.selection();
1725         var chunk = WebInspector.TextEditorChunkedPanel.prototype.splitChunkOnALine.call(this, lineNumber, chunkNumber, createSuffixChunk);
1726         this._restoreSelection(selection);
1727         return chunk;
1728     },
1729
1730     beginDomUpdates: function()
1731     {
1732         if (!this._domUpdateCoalescingLevel)
1733             this._detachMutationObserver();
1734         WebInspector.TextEditorChunkedPanel.prototype.beginDomUpdates.call(this);
1735     },
1736
1737     endDomUpdates: function()
1738     {
1739         WebInspector.TextEditorChunkedPanel.prototype.endDomUpdates.call(this);
1740         if (!this._domUpdateCoalescingLevel)
1741             this._attachMutationObserver();
1742     },
1743
1744     buildChunks: function()
1745     {
1746         for (var i = 0; i < this._textModel.linesCount; ++i)
1747             this._textModel.removeAttribute(i, "highlight");
1748
1749         WebInspector.TextEditorChunkedPanel.prototype.buildChunks.call(this);
1750     },
1751
1752     /**
1753      * @param {number} startLine
1754      * @param {number} endLine
1755      * @return {WebInspector.TextEditorMainChunk}
1756      */
1757     createNewChunk: function(startLine, endLine)
1758     {
1759         return new WebInspector.TextEditorMainChunk(this, startLine, endLine);
1760     },
1761
1762     /**
1763      * @param {number} fromIndex
1764      * @param {number} toIndex
1765      */
1766     expandChunks: function(fromIndex, toIndex)
1767     {
1768         var lastChunk = this._textChunks[toIndex - 1];
1769         var lastVisibleLine = lastChunk.startLine + lastChunk.linesCount;
1770
1771         var selection = this.selection();
1772
1773         this._muteHighlightListener = true;
1774         this._highlighter.highlight(lastVisibleLine);
1775         delete this._muteHighlightListener;
1776
1777         WebInspector.TextEditorChunkedPanel.prototype.expandChunks.call(this, fromIndex, toIndex);
1778
1779         this._restoreSelection(selection);
1780     },
1781
1782     /**
1783      * @param {number} fromLine
1784      * @param {number} toLine
1785      */
1786     _highlightDataReady: function(fromLine, toLine)
1787     {
1788         if (this._muteHighlightListener)
1789             return;
1790         this._paintLines(fromLine, toLine, true /*restoreSelection*/);
1791     },
1792
1793     /**
1794      * @param {number} fromLine
1795      * @param {number} toLine
1796      * @param {boolean=} restoreSelection
1797      */
1798     _paintLines: function(fromLine, toLine, restoreSelection)
1799     {
1800         var chunk;
1801         var selection;
1802         var lineRows = [];
1803         for (var lineNumber = fromLine; lineNumber < toLine; ++lineNumber) {
1804             if (!chunk || lineNumber < chunk.startLine || lineNumber >= chunk.startLine + chunk.linesCount)
1805                 chunk = this.chunkForLine(lineNumber);
1806             var lineRow = chunk.expandedLineRow(lineNumber);
1807             if (!lineRow)
1808                 continue;
1809             if (restoreSelection && !selection)
1810                 selection = this.selection();
1811             lineRows.push(lineRow);
1812         }
1813
1814         var highlight = {};
1815         this.beginDomUpdates();
1816         for(var regexString in this._highlightRegexs) {
1817             var regexHighlightDescriptor = this._highlightRegexs[regexString];
1818             this._measureRegexHighlight(highlight, lineRows, regexHighlightDescriptor.regex, regexHighlightDescriptor.cssClass);
1819         }
1820
1821         for(var i = 0; i < lineRows.length; ++i)
1822             this._paintLine(lineRows[i], highlight[lineRows[i].lineNumber]);
1823
1824         this.endDomUpdates();
1825
1826         if (restoreSelection)
1827             this._restoreSelection(selection);
1828     },
1829
1830     /**
1831      * @param {string} line
1832      * @param {RegExp} regex
1833      * @return {Array.<{startColumn: number, endColumn: number}>}
1834      */
1835     _findRegexOccurrences: function(line, regex)
1836     {
1837         var ranges = [];
1838         var regexResult;
1839         while (regexResult = regex.exec(line)) {
1840             ranges.push({
1841                 startColumn: regexResult.index,
1842                 endColumn: regexResult.index + regexResult[0].length - 1
1843             });
1844         }
1845         return ranges;
1846     },
1847
1848     /**
1849      * @param {Object.<number, Array.<WebInspector.TextEditorMainPanel.LineOverlayHighlight>>} highlight
1850      * @param {Array.<Element>} lineRows
1851      * @param {RegExp} regex
1852      * @param {string} cssClass
1853      */
1854     _measureRegexHighlight: function(highlight, lineRows, regex, cssClass)
1855     {
1856         var rowsToMeasure = [];
1857         for(var i = 0; i < lineRows.length; ++i) {
1858             var lineRow = lineRows[i];
1859             var line = this._textModel.line(lineRow.lineNumber);
1860             var ranges = this._findRegexOccurrences(line, regex);
1861             if (ranges.length === 0)
1862                 continue;
1863
1864             this._renderRanges(lineRow, line, ranges);
1865             rowsToMeasure.push(lineRow);
1866         }
1867
1868         for(var i = 0; i < rowsToMeasure.length; ++i) {
1869             var lineRow = rowsToMeasure[i];
1870             var lineNumber = lineRow.lineNumber;
1871             var metrics = this._measureSpans(lineRow);
1872
1873             if (!highlight[lineNumber])
1874                 highlight[lineNumber] = [];
1875
1876             highlight[lineNumber].push(new WebInspector.TextEditorMainPanel.LineOverlayHighlight(metrics, cssClass));
1877         }
1878     },
1879
1880     /**
1881      * @param {Element} lineRow
1882      * @return {Array.<WebInspector.TextEditorMainPanel.ElementMetrics>}
1883      */
1884     _measureSpans: function(lineRow)
1885     {
1886         var spans = lineRow.getElementsByTagName("span");
1887         var metrics = [];
1888         for(var i = 0; i < spans.length; ++i)
1889             metrics.push(new WebInspector.TextEditorMainPanel.ElementMetrics(spans[i]));
1890         return metrics;
1891     },
1892
1893     /**
1894      * @param {Element} lineRow
1895      * @param {WebInspector.TextEditorMainPanel.LineOverlayHighlight} highlight
1896      */
1897     _appendOverlayHighlight: function(lineRow, highlight)
1898     {
1899         const extraWidth = 1;
1900         var metrics = highlight.metrics;
1901         var cssClass = highlight.cssClass;
1902         for(var i = 0; i < metrics.length; ++i) {
1903             var highlightSpan = document.createElement("span");
1904             highlightSpan.addStyleClass(cssClass);
1905             highlightSpan.style.left = (metrics[i].left - extraWidth) + "px";
1906             highlightSpan.style.width = (metrics[i].width + extraWidth * 2) + "px";
1907             highlightSpan.textContent = " ";
1908             highlightSpan.addStyleClass("text-editor-overlay-highlight");
1909             lineRow.appendChild(highlightSpan);
1910         }
1911     },
1912
1913     /**
1914      * @param {Element} lineRow
1915      * @param {string} line
1916      * @param {Array.<{startColumn: number, endColumn: number, token: ?string}>} ranges
1917      */
1918     _renderRanges: function(lineRow, line, ranges)
1919     {
1920         var decorationsElement = lineRow.decorationsElement;
1921
1922         if (!decorationsElement)
1923             lineRow.removeChildren();
1924         else {
1925             while (true) {
1926                 var child = lineRow.firstChild;
1927                 if (!child || child === decorationsElement)
1928                     break;
1929                 lineRow.removeChild(child);
1930             }
1931         }
1932
1933         if (!line)
1934             lineRow.insertBefore(document.createElement("br"), decorationsElement);
1935
1936         var plainTextStart = 0;
1937         for(var i = 0; i < ranges.length; i++) {
1938             var rangeStart = ranges[i].startColumn;
1939             var rangeEnd = ranges[i].endColumn;
1940             var cssClass = ranges[i].token ? "webkit-" + ranges[i].token : "";
1941
1942             if (plainTextStart < rangeStart) {
1943                 this._insertTextNodeBefore(lineRow, decorationsElement, line.substring(plainTextStart, rangeStart));
1944             }
1945             this._insertSpanBefore(lineRow, decorationsElement, line.substring(rangeStart, rangeEnd + 1), cssClass);
1946             plainTextStart = rangeEnd + 1;
1947         }
1948         if (plainTextStart < line.length) {
1949             this._insertTextNodeBefore(lineRow, decorationsElement, line.substring(plainTextStart, line.length));
1950         }
1951     },
1952
1953     /**
1954      * @param {Element} lineRow
1955      * @param {Array.<WebInspector.TextEditorMainPanel.LineOverlayHighlight>} overlayHighlight
1956      */
1957     _paintLine: function(lineRow, overlayHighlight)
1958     {
1959         var lineNumber = lineRow.lineNumber;
1960
1961         this.beginDomUpdates();
1962         try {
1963             var syntaxHighlight = this._textModel.getAttribute(lineNumber, "highlight");
1964             if (!syntaxHighlight)
1965                 return;
1966
1967             var line = this._textModel.line(lineNumber);
1968             var ranges = syntaxHighlight.ranges;
1969             this._renderRanges(lineRow, line, ranges);
1970
1971             if (overlayHighlight)
1972                 for(var i = 0; i < overlayHighlight.length; ++i)
1973                     this._appendOverlayHighlight(lineRow, overlayHighlight[i]);
1974         } finally {
1975             if (this._rangeToMark && this._rangeToMark.startLine === lineNumber)
1976                 this._markedRangeElement = WebInspector.highlightSearchResult(lineRow, this._rangeToMark.startColumn, this._rangeToMark.endColumn - this._rangeToMark.startColumn);
1977             this.endDomUpdates();
1978         }
1979     },
1980
1981     /**
1982      * @param {Element} lineRow
1983      */
1984     _releaseLinesHighlight: function(lineRow)
1985     {
1986         if (!lineRow)
1987             return;
1988         if ("spans" in lineRow) {
1989             var spans = lineRow.spans;
1990             for (var j = 0; j < spans.length; ++j)
1991                 this._cachedSpans.push(spans[j]);
1992             delete lineRow.spans;
1993         }
1994         if ("textNodes" in lineRow) {
1995             var textNodes = lineRow.textNodes;
1996             for (var j = 0; j < textNodes.length; ++j)
1997                 this._cachedTextNodes.push(textNodes[j]);
1998             delete lineRow.textNodes;
1999         }
2000         this._cachedRows.push(lineRow);
2001     },
2002
2003     /**
2004      * @param {?Node=} lastUndamagedLineRow
2005      * @return {WebInspector.TextRange}
2006      */
2007     selection: function(lastUndamagedLineRow)
2008     {
2009         var selection = window.getSelection();
2010         if (!selection.rangeCount)
2011             return null;
2012         // Selection may be outside of the editor.
2013         if (!this._container.isAncestor(selection.anchorNode) || !this._container.isAncestor(selection.focusNode))
2014             return null;
2015         var start = this._selectionToPosition(selection.anchorNode, selection.anchorOffset, lastUndamagedLineRow);
2016         var end = selection.isCollapsed ? start : this._selectionToPosition(selection.focusNode, selection.focusOffset, lastUndamagedLineRow);
2017         return new WebInspector.TextRange(start.line, start.column, end.line, end.column);
2018     },
2019
2020     lastSelection: function()
2021     {
2022         return this._lastSelection;
2023     },
2024
2025     /**
2026      * @param {boolean=} scrollIntoView
2027      */
2028     _restoreSelection: function(range, scrollIntoView)
2029     {
2030         if (!range)
2031             return;
2032
2033         var start = this._positionToSelection(range.startLine, range.startColumn);
2034         var end = range.isEmpty() ? start : this._positionToSelection(range.endLine, range.endColumn);
2035         window.getSelection().setBaseAndExtent(start.container, start.offset, end.container, end.offset);
2036
2037         if (scrollIntoView) {
2038             for (var node = end.container; node; node = node.parentElement) {
2039                 if (node.scrollIntoViewIfNeeded) {
2040                     node.scrollIntoViewIfNeeded();
2041                     break;
2042                 }
2043             }
2044         }
2045         this._lastSelection = range;
2046     },
2047
2048     /**
2049      * @param {Node} container
2050      * @param {number} offset
2051      * @param {?Node=} lastUndamagedLineRow
2052      * @return {{line: number, column: number}}
2053      */
2054     _selectionToPosition: function(container, offset, lastUndamagedLineRow)
2055     {
2056         if (container === this._container && offset === 0)
2057             return { line: 0, column: 0 };
2058         if (container === this._container && offset === 1)
2059             return { line: this._textModel.linesCount - 1, column: this._textModel.lineLength(this._textModel.linesCount - 1) };
2060
2061         // This method can be called on the damaged DOM (when DOM does not match model).
2062         // We need to start counting lines from the first undamaged line if it is given.
2063         var lineNumber;
2064         var column = 0;
2065         var node;
2066         var scopeNode;
2067         if (lastUndamagedLineRow === null) {
2068              // Last undamaged row is given, but is null - force traverse from the beginning
2069             node = this._container.firstChild;
2070             scopeNode = this._container;
2071             lineNumber = 0;
2072         } else {
2073             var lineRow = this._enclosingLineRowOrSelf(container);
2074             if (!lastUndamagedLineRow || (typeof lineRow.lineNumber === "number" && lineRow.lineNumber <= lastUndamagedLineRow.lineNumber)) {
2075                 // DOM is consistent (or we belong to the first damaged row)- lookup the row we belong to and start with it.
2076                 node = lineRow;
2077                 scopeNode = node;
2078                 lineNumber = node.lineNumber;
2079             } else {
2080                 // Start with the node following undamaged row. It corresponds to lineNumber + 1.
2081                 node = lastUndamagedLineRow.nextSibling;
2082                 scopeNode = this._container;
2083                 lineNumber = lastUndamagedLineRow.lineNumber + 1;
2084             }
2085         }
2086
2087         // Fast return the line start.
2088         if (container === node && offset === 0)
2089             return { line: lineNumber, column: 0 };
2090
2091         // Traverse text and increment lineNumber / column.
2092         for (; node && node !== container; node = node.traverseNextNode(scopeNode)) {
2093             if (node.nodeName.toLowerCase() === "br") {
2094                 lineNumber++;
2095                 column = 0;
2096             } else if (node.nodeType === Node.TEXT_NODE) {
2097                 var text = node.textContent;
2098                 for (var i = 0; i < text.length; ++i) {
2099                     if (text.charAt(i) === "\n") {
2100                         lineNumber++;
2101                         column = 0;
2102                     } else
2103                         column++;
2104                 }
2105             }
2106         }
2107
2108         // We reached our container node, traverse within itself until we reach given offset.
2109         if (node === container && offset) {
2110             var text = node.textContent;
2111             // In case offset == 1 and lineRow is a chunk div, we need to traverse it all.
2112             var textOffset = (node._chunk && offset === 1) ? text.length : offset;
2113             for (var i = 0; i < textOffset; ++i) {
2114                 if (text.charAt(i) === "\n") {
2115                     lineNumber++;
2116                     column = 0;
2117                 } else
2118                     column++;
2119             }
2120         }
2121         return { line: lineNumber, column: column };
2122     },
2123
2124     /**
2125      * @param {number} line
2126      * @param {number} column
2127      * @return {{container: Element, offset: number}}
2128      */
2129     _positionToSelection: function(line, column)
2130     {
2131         var chunk = this.chunkForLine(line);
2132         // One-lined collapsed chunks may still stay highlighted.
2133         var lineRow = chunk.linesCount === 1 ? chunk.element : chunk.expandedLineRow(line);
2134         if (lineRow)
2135             var rangeBoundary = lineRow.rangeBoundaryForOffset(column);
2136         else {
2137             var offset = column;
2138             for (var i = chunk.startLine; i < line && i < this._textModel.linesCount; ++i)
2139                 offset += this._textModel.lineLength(i) + 1; // \n
2140             lineRow = chunk.element;
2141             if (lineRow.firstChild)
2142                 var rangeBoundary = { container: lineRow.firstChild, offset: offset };
2143             else
2144                 var rangeBoundary = { container: lineRow, offset: 0 };
2145         }
2146         return rangeBoundary;
2147     },
2148
2149     /**
2150      * @param {Node} element
2151      * @return {?Node}
2152      */
2153     _enclosingLineRowOrSelf: function(element)
2154     {
2155         var lineRow = element.enclosingNodeOrSelfWithClass("webkit-line-content");
2156         if (lineRow)
2157             return lineRow;
2158
2159         for (lineRow = element; lineRow; lineRow = lineRow.parentElement) {
2160             if (lineRow.parentElement === this._container)
2161                 return lineRow;
2162         }
2163         return null;
2164     },
2165
2166     /**
2167      * @param {Element} element
2168      * @param {Element} oldChild
2169      * @param {string} content
2170      * @param {string} className
2171      */
2172     _insertSpanBefore: function(element, oldChild, content, className)
2173     {
2174         if (className === "html-resource-link" || className === "html-external-link") {
2175             element.insertBefore(this._createLink(content, className === "html-external-link"), oldChild);
2176             return;
2177         }
2178
2179         var span = this._cachedSpans.pop() || document.createElement("span");
2180         span.className = className;
2181         if (WebInspector.FALSE) // For paint debugging.
2182             span.addStyleClass("debug-fadeout");
2183         span.textContent = content;
2184         element.insertBefore(span, oldChild);
2185         if (!("spans" in element))
2186             element.spans = [];
2187         element.spans.push(span);
2188     },
2189
2190     /**
2191      * @param {Element} element
2192      * @param {Element} oldChild
2193      * @param {string} text
2194      */
2195     _insertTextNodeBefore: function(element, oldChild, text)
2196     {
2197         var textNode = this._cachedTextNodes.pop();
2198         if (textNode)
2199             textNode.nodeValue = text;
2200         else
2201             textNode = document.createTextNode(text);
2202         element.insertBefore(textNode, oldChild);
2203         if (!("textNodes" in element))
2204             element.textNodes = [];
2205         element.textNodes.push(textNode);
2206     },
2207
2208     /**
2209      * @param {string} content
2210      * @param {boolean} isExternal
2211      * @return {Element}
2212      */
2213     _createLink: function(content, isExternal)
2214     {
2215         var quote = content.charAt(0);
2216         if (content.length > 1 && (quote === "\"" || quote === "'"))
2217             content = content.substring(1, content.length - 1);
2218         else
2219             quote = null;
2220
2221         var span = document.createElement("span");
2222         span.className = "webkit-html-attribute-value";
2223         if (quote)
2224             span.appendChild(document.createTextNode(quote));
2225         span.appendChild(this._delegate.createLink(content, isExternal));
2226         if (quote)
2227             span.appendChild(document.createTextNode(quote));
2228         return span;
2229     },
2230
2231     /**
2232      * @param {Array.<WebKitMutation>} mutations
2233      */
2234     _handleMutations: function(mutations)
2235     {
2236         if (this._readOnly) {
2237             delete this._keyDownCode;
2238             return;
2239         }
2240
2241         // Annihilate noop BR addition + removal that takes place upon line removal.
2242         var filteredMutations = mutations.slice();
2243         var addedBRs = new Map();
2244         for (var i = 0; i < mutations.length; ++i) {
2245             var mutation = mutations[i];
2246             if (mutation.type !== "childList")
2247                 continue;
2248             if (mutation.addedNodes.length === 1 && mutation.addedNodes[0].nodeName === "BR")
2249                 addedBRs.put(mutation.addedNodes[0], mutation);
2250             else if (mutation.removedNodes.length === 1 && mutation.removedNodes[0].nodeName === "BR") {
2251                 var noopMutation = addedBRs.get(mutation.removedNodes[0]);
2252                 if (noopMutation) {
2253                     filteredMutations.remove(mutation);
2254                     filteredMutations.remove(noopMutation);
2255                 }
2256             }
2257         }
2258
2259         var dirtyLines;
2260         for (var i = 0; i < filteredMutations.length; ++i) {
2261             var mutation = filteredMutations[i];
2262             var changedNodes = [];
2263             if (mutation.type === "childList" && mutation.addedNodes.length)
2264                 changedNodes = Array.prototype.slice.call(mutation.addedNodes);
2265             else if (mutation.type === "childList" && mutation.removedNodes.length)
2266                 changedNodes = Array.prototype.slice.call(mutation.removedNodes);
2267             changedNodes.push(mutation.target);
2268
2269             for (var j = 0; j < changedNodes.length; ++j) {
2270                 var lines = this._collectDirtyLines(mutation, changedNodes[j]);
2271                 if (!lines)
2272                     continue;
2273                 if (!dirtyLines) {
2274                     dirtyLines = lines;
2275                     continue;
2276                 }
2277                 dirtyLines.start = Math.min(dirtyLines.start, lines.start);
2278                 dirtyLines.end = Math.max(dirtyLines.end, lines.end);
2279             }
2280         }
2281         if (dirtyLines) {
2282             delete this._rangeToMark;
2283             this._applyDomUpdates(dirtyLines);
2284         }
2285
2286         this._assertDOMMatchesTextModel();
2287
2288         delete this._keyDownCode;
2289     },
2290
2291     /**
2292      * @param {WebKitMutation} mutation
2293      * @param {Node} target
2294      * @return {?Object}
2295      */
2296     _collectDirtyLines: function(mutation, target)
2297     {
2298         var lineRow = this._enclosingLineRowOrSelf(target);
2299         if (!lineRow)
2300             return null;
2301
2302         if (lineRow.decorationsElement && lineRow.decorationsElement.isSelfOrAncestor(target)) {
2303             if (this._syncDecorationsForLineListener)
2304                 this._syncDecorationsForLineListener(lineRow.lineNumber);
2305             return null;
2306         }
2307
2308         if (typeof lineRow.lineNumber !== "number")
2309             return null;
2310
2311         var startLine = lineRow.lineNumber;
2312         var endLine = lineRow._chunk ? lineRow._chunk.endLine - 1 : lineRow.lineNumber;
2313         return { start: startLine, end: endLine };
2314     },
2315
2316     /**
2317      * @param {Object} dirtyLines
2318      */
2319     _applyDomUpdates: function(dirtyLines)
2320     {
2321         var lastUndamagedLineNumber = dirtyLines.start - 1; // Can be -1
2322         var firstUndamagedLineNumber = dirtyLines.end + 1; // Can be this._textModel.linesCount
2323
2324         var lastUndamagedLineChunk = lastUndamagedLineNumber >= 0 ? this._textChunks[this.chunkNumberForLine(lastUndamagedLineNumber)] : null;
2325         var firstUndamagedLineChunk = firstUndamagedLineNumber < this._textModel.linesCount ? this._textChunks[this.chunkNumberForLine(firstUndamagedLineNumber)] : null;
2326
2327         var collectLinesFromNode = lastUndamagedLineChunk ? lastUndamagedLineChunk.lineRowContainingLine(lastUndamagedLineNumber) : null;
2328         var collectLinesToNode = firstUndamagedLineChunk ? firstUndamagedLineChunk.lineRowContainingLine(firstUndamagedLineNumber) : null;
2329         var lines = this._collectLinesFromDOM(collectLinesFromNode, collectLinesToNode);
2330
2331         var startLine = dirtyLines.start;
2332         var endLine = dirtyLines.end;
2333
2334         var editInfo = this._guessEditRangeBasedOnSelection(startLine, endLine, lines);
2335         if (!editInfo) {
2336             if (WebInspector.debugDefaultTextEditor)
2337                 console.warn("Falling back to expensive edit");
2338             var range = new WebInspector.TextRange(startLine, 0, endLine, this._textModel.lineLength(endLine));
2339             if (!lines.length) {
2340                 // Entire damaged area has collapsed. Replace everything between start and end lines with nothing.
2341                 editInfo = new WebInspector.DefaultTextEditor.EditInfo(this._textModel.growRangeRight(range), "");
2342             } else
2343                 editInfo = new WebInspector.DefaultTextEditor.EditInfo(range, lines.join("\n"));
2344         }
2345
2346         var selection = this.selection(collectLinesFromNode);
2347
2348         // Unindent after block
2349         if (editInfo.text === "}" && editInfo.range.isEmpty() && selection.isEmpty() && !this._textModel.line(editInfo.range.endLine).trim()) {
2350             var offset = this._closingBlockOffset(editInfo.range, selection);
2351             if (offset >= 0) {
2352                 editInfo.range.startColumn = offset;
2353                 selection.startColumn = offset + 1;
2354                 selection.endColumn = offset + 1;
2355             }
2356         }
2357
2358         this._textModel.editRange(editInfo.range, editInfo.text);
2359         this._restoreSelection(selection);
2360     },
2361
2362     /**
2363      * @param {number} startLine
2364      * @param {number} endLine
2365      * @param {Array.<string>} lines
2366      * @return {?WebInspector.DefaultTextEditor.EditInfo}
2367      */
2368     _guessEditRangeBasedOnSelection: function(startLine, endLine, lines)
2369     {
2370         // Analyze input data
2371         var textInputData = this._textInputData;
2372         delete this._textInputData;
2373         var isBackspace = this._keyDownCode === WebInspector.KeyboardShortcut.Keys.Backspace.code;
2374         var isDelete = this._keyDownCode === WebInspector.KeyboardShortcut.Keys.Delete.code;
2375
2376         if (!textInputData && (isDelete || isBackspace))
2377             textInputData = "";
2378
2379         // Return if there is no input data or selection
2380         if (typeof textInputData === "undefined" || !this._lastSelection)
2381             return null;
2382
2383         // Adjust selection based on the keyboard actions (grow for backspace, etc.).
2384         textInputData = textInputData || "";
2385         var range = this._lastSelection.normalize();
2386         if (isBackspace && range.isEmpty())
2387             range = this._textModel.growRangeLeft(range);
2388         else if (isDelete && range.isEmpty())
2389             range = this._textModel.growRangeRight(range);
2390
2391         // Test that selection intersects damaged lines
2392         if (startLine > range.endLine || endLine < range.startLine)
2393             return null;
2394
2395         var replacementLineCount = textInputData.split("\n").length - 1;
2396         var lineCountDelta = replacementLineCount - range.linesCount;
2397         if (startLine + lines.length - endLine - 1 !== lineCountDelta)
2398             return null;
2399
2400         // Clone text model of the size that fits both: selection before edit and the damaged lines after edit.
2401         var cloneFromLine = Math.min(range.startLine, startLine);
2402         var postLastLine = startLine + lines.length + lineCountDelta;
2403         var cloneToLine = Math.min(Math.max(postLastLine, range.endLine) + 1, this._textModel.linesCount);
2404         var domModel = this._textModel.slice(cloneFromLine, cloneToLine);
2405         domModel.editRange(range.shift(-cloneFromLine), textInputData);
2406
2407         // Then we'll test if this new model matches the DOM lines.
2408         for (var i = 0; i < lines.length; ++i) {
2409             if (domModel.line(i + startLine - cloneFromLine) !== lines[i])
2410                 return null;
2411         }
2412         return new WebInspector.DefaultTextEditor.EditInfo(range, textInputData);
2413     },
2414
2415     _assertDOMMatchesTextModel: function()
2416     {
2417         if (!WebInspector.debugDefaultTextEditor)
2418             return;
2419
2420         console.assert(this.element.innerText === this._textModel.text() + "\n", "DOM does not match model.");
2421         for (var lineRow = this._container.firstChild; lineRow; lineRow = lineRow.nextSibling) {
2422             var lineNumber = lineRow.lineNumber;
2423             if (typeof lineNumber !== "number") {
2424                 console.warn("No line number on line row");
2425                 continue;
2426             }
2427             if (lineRow._chunk) {
2428                 var chunk = lineRow._chunk;
2429                 console.assert(lineNumber === chunk.startLine);
2430                 var chunkText = this._textModel.copyRange(new WebInspector.TextRange(chunk.startLine, 0, chunk.endLine - 1, this._textModel.lineLength(chunk.endLine - 1)));
2431                 if (chunkText !== lineRow.textContent)
2432                     console.warn("Chunk is not matching: %d %O", lineNumber, lineRow);
2433             } else if (this._textModel.line(lineNumber) !== lineRow.textContent)
2434                 console.warn("Line is not matching: %d %O", lineNumber, lineRow);
2435         }
2436     },
2437
2438     /**
2439      * @param {WebInspector.TextRange} oldRange
2440      * @param {WebInspector.TextRange} selection
2441      * @return {number}
2442      */
2443     _closingBlockOffset: function(oldRange, selection)
2444     {
2445         var nestingLevel = 1;
2446         for (var i = oldRange.endLine; i >= 0; --i) {
2447             var attribute = this._textModel.getAttribute(i, "highlight");
2448             if (!attribute)
2449                 continue;
2450             var ranges = attribute.ranges;
2451             for (var j = ranges.length - 1; j >= 0; j--) {
2452                 var token = ranges[j].token;
2453                 if (token === "block-start") {
2454                     if (!(--nestingLevel)) {
2455                         var lineContent = this._textModel.line(i);
2456                         return lineContent.length - lineContent.trimLeft().length;
2457                     }
2458                 }
2459                 if (token === "block-end")
2460                     ++nestingLevel;
2461             }
2462         }
2463         return -1;
2464     },
2465
2466     /**
2467      * @param {WebInspector.TextRange} oldRange
2468      * @param {WebInspector.TextRange} newRange
2469      */
2470     textChanged: function(oldRange, newRange)
2471     {
2472         this.beginDomUpdates();
2473         this._removeDecorationsInRange(oldRange);
2474         this._updateChunksForRanges(oldRange, newRange);
2475         this._updateHighlightsForRange(newRange);
2476         this.endDomUpdates();
2477     },
2478
2479     /**
2480      * @param {WebInspector.TextRange} range
2481      */
2482     _removeDecorationsInRange: function(range)
2483     {
2484         for (var i = this.chunkNumberForLine(range.startLine); i < this._textChunks.length; ++i) {
2485             var chunk = this._textChunks[i];
2486             if (chunk.startLine > range.endLine)
2487                 break;
2488             chunk.removeAllDecorations();
2489         }
2490     },
2491
2492     /**
2493      * @param {WebInspector.TextRange} oldRange
2494      * @param {WebInspector.TextRange} newRange
2495      */
2496     _updateChunksForRanges: function(oldRange, newRange)
2497     {
2498         var firstDamagedChunkNumber = this.chunkNumberForLine(oldRange.startLine);
2499         var lastDamagedChunkNumber = firstDamagedChunkNumber;
2500         while (lastDamagedChunkNumber + 1 < this._textChunks.length) {
2501             if (this._textChunks[lastDamagedChunkNumber + 1].startLine > oldRange.endLine)
2502                 break;
2503             ++lastDamagedChunkNumber;
2504         }
2505
2506         var firstDamagedChunk = this._textChunks[firstDamagedChunkNumber];
2507         var lastDamagedChunk = this._textChunks[lastDamagedChunkNumber];
2508
2509         var linesDiff = newRange.linesCount - oldRange.linesCount;
2510
2511         // First, detect chunks that have not been modified and simply shift them.
2512         if (linesDiff) {
2513             for (var chunkNumber = lastDamagedChunkNumber + 1; chunkNumber < this._textChunks.length; ++chunkNumber)
2514                 this._textChunks[chunkNumber].startLine += linesDiff;
2515         }
2516
2517         // Remove damaged chunks from DOM and from textChunks model.
2518         var lastUndamagedChunk = firstDamagedChunkNumber > 0 ? this._textChunks[firstDamagedChunkNumber - 1] : null;
2519         var firstUndamagedChunk = lastDamagedChunkNumber + 1 < this._textChunks.length ? this._textChunks[lastDamagedChunkNumber + 1] : null;
2520
2521         var removeDOMFromNode = lastUndamagedChunk ? lastUndamagedChunk.lastElement().nextSibling : this._container.firstChild;
2522         var removeDOMToNode = firstUndamagedChunk ? firstUndamagedChunk.firstElement() : null;
2523
2524         // Fast case - patch single expanded chunk that did not grow / shrink during edit.
2525         if (!linesDiff && firstDamagedChunk === lastDamagedChunk && firstDamagedChunk._expandedLineRows) {
2526             var lastUndamagedLineRow = lastDamagedChunk.expandedLineRow(oldRange.startLine - 1);
2527             var firstUndamagedLineRow = firstDamagedChunk.expandedLineRow(oldRange.endLine + 1);
2528             var localRemoveDOMFromNode = lastUndamagedLineRow ? lastUndamagedLineRow.nextSibling : removeDOMFromNode;
2529             var localRemoveDOMToNode = firstUndamagedLineRow || removeDOMToNode;
2530             removeSubsequentNodes(localRemoveDOMFromNode, localRemoveDOMToNode);
2531             for (var i = newRange.startLine; i < newRange.endLine + 1; ++i) {
2532                 var row = firstDamagedChunk._createRow(i);
2533                 firstDamagedChunk._expandedLineRows[i - firstDamagedChunk.startLine] = row;
2534                 this._container.insertBefore(row, localRemoveDOMToNode);
2535             }
2536             firstDamagedChunk.updateCollapsedLineRow();
2537             this._assertDOMMatchesTextModel();
2538             return;
2539         }
2540
2541         removeSubsequentNodes(removeDOMFromNode, removeDOMToNode);
2542         this._textChunks.splice(firstDamagedChunkNumber, lastDamagedChunkNumber - firstDamagedChunkNumber + 1);
2543
2544         // Compute damaged chunks span
2545         var startLine = firstDamagedChunk.startLine;
2546         var endLine = lastDamagedChunk.endLine + linesDiff;
2547         var lineSpan = endLine - startLine;
2548
2549         // Re-create chunks for damaged area.
2550         var insertionIndex = firstDamagedChunkNumber;
2551         var chunkSize = Math.ceil(lineSpan / Math.ceil(lineSpan / this._defaultChunkSize));
2552
2553         for (var i = startLine; i < endLine; i += chunkSize) {
2554             var chunk = this.createNewChunk(i, Math.min(endLine, i + chunkSize));
2555             this._textChunks.splice(insertionIndex++, 0, chunk);
2556             this._container.insertBefore(chunk.element, removeDOMToNode);
2557         }
2558
2559         this._assertDOMMatchesTextModel();
2560     },
2561
2562     /**
2563      * @param {WebInspector.TextRange} range
2564      */
2565     _updateHighlightsForRange: function(range)
2566     {
2567         var visibleFrom = this.scrollTop();
2568         var visibleTo = visibleFrom + this.clientHeight();
2569
2570         var result = this.findVisibleChunks(visibleFrom, visibleTo);
2571         var chunk = this._textChunks[result.end - 1];
2572         var lastVisibleLine = chunk.startLine + chunk.linesCount;
2573
2574         lastVisibleLine = Math.max(lastVisibleLine, range.endLine + 1);
2575         lastVisibleLine = Math.min(lastVisibleLine, this._textModel.linesCount);
2576
2577         var updated = this._highlighter.updateHighlight(range.startLine, lastVisibleLine);
2578         if (!updated) {
2579             // Highlights for the chunks below are invalid, so just collapse them.
2580             for (var i = this.chunkNumberForLine(range.startLine); i < this._textChunks.length; ++i)
2581                 this._textChunks[i].collapse();
2582         }
2583
2584         this.repaintAll();
2585     },
2586
2587     /**
2588      * @param {Node} from
2589      * @param {Node} to
2590      * @return {Array.<string>}
2591      */
2592     _collectLinesFromDOM: function(from, to)
2593     {
2594         var textContents = [];
2595         var hasContent = false;
2596         for (var node = from ? from.nextSibling : this._container; node && node !== to; node = node.traverseNextNode(this._container)) {
2597             if (node._isDecorationsElement) {
2598                 // Skip all children of the decoration container.
2599                 node = node.nextSibling;
2600                 if (!node || node === to)
2601                     break;
2602             }
2603             hasContent = true;
2604             if (node.nodeName.toLowerCase() === "br")
2605                 textContents.push("\n");
2606             else if (node.nodeType === Node.TEXT_NODE)
2607                 textContents.push(node.textContent);
2608         }
2609         if (!hasContent)
2610             return [];
2611
2612         var textContent = textContents.join("");
2613         // The last \n (if any) does not "count" in a DIV.
2614         textContent = textContent.replace(/\n$/, "");
2615
2616         return textContent.split("\n");
2617     },
2618
2619     /**
2620      * @param {Event} event
2621      */
2622     _handleSelectionChange: function(event)
2623     {
2624         var textRange = this.selection();
2625         if (textRange)
2626             this._lastSelection = textRange;
2627
2628         this._tokenHighlighter.handleSelectionChange(textRange);
2629         this._delegate.selectionChanged(textRange);
2630     },
2631
2632     /**
2633      * @param {Event} event
2634      */
2635     _handleTextInput: function(event)
2636     {
2637         this._textInputData = event.data;
2638     },
2639
2640     /**
2641      * @param {Event} event
2642      */
2643     handleKeyDown: function(event)
2644     {
2645         this._keyDownCode = event.keyCode;
2646     },
2647
2648     /**
2649      * @param {Event} event
2650      */
2651     _handleCut: function(event)
2652     {
2653         this._keyDownCode = WebInspector.KeyboardShortcut.Keys.Delete.code;
2654     },
2655
2656     /**
2657      * @param {number} scrollTop
2658      * @param {number} clientHeight
2659      * @param {number} chunkSize
2660      */
2661     overrideViewportForTest: function(scrollTop, clientHeight, chunkSize)
2662     {
2663         this._scrollTopOverrideForTest = scrollTop;
2664         this._clientHeightOverrideForTest = clientHeight;
2665         this._defaultChunkSize = chunkSize;
2666     },
2667
2668     __proto__: WebInspector.TextEditorChunkedPanel.prototype
2669 }
2670
2671 /**
2672  * @constructor
2673  * @param {Element} element
2674  */
2675 WebInspector.TextEditorMainPanel.ElementMetrics = function(element)
2676 {
2677     this.width = element.offsetWidth;
2678     this.left = element.offsetLeft;
2679 }
2680
2681 /**
2682  * @constructor
2683  * @param {Array.<WebInspector.TextEditorMainPanel.ElementMetrics>} metrics
2684  * @param {string} cssClass
2685  */
2686 WebInspector.TextEditorMainPanel.LineOverlayHighlight = function(metrics, cssClass)
2687 {
2688     this.metrics = metrics;
2689     this.cssClass = cssClass;
2690 }
2691
2692 /**
2693  * @constructor
2694  * @param {WebInspector.TextEditorChunkedPanel} chunkedPanel
2695  * @param {number} startLine
2696  * @param {number} endLine
2697  */
2698 WebInspector.TextEditorMainChunk = function(chunkedPanel, startLine, endLine)
2699 {
2700     this._chunkedPanel = chunkedPanel;
2701     this._textModel = chunkedPanel._textModel;
2702
2703     this.element = document.createElement("div");
2704     this.element.lineNumber = startLine;
2705     this.element.className = "webkit-line-content";
2706     this.element._chunk = this;
2707
2708     this._startLine = startLine;
2709     endLine = Math.min(this._textModel.linesCount, endLine);
2710     this.linesCount = endLine - startLine;
2711
2712     this._expanded = false;
2713
2714     this.updateCollapsedLineRow();
2715 }
2716
2717 WebInspector.TextEditorMainChunk.prototype = {
2718     /**
2719      * @param {Element|string} decoration
2720      */
2721     addDecoration: function(decoration)
2722     {
2723         this._chunkedPanel.beginDomUpdates();
2724         if (typeof decoration === "string")
2725             this.element.addStyleClass(decoration);
2726         else {
2727             if (!this.element.decorationsElement) {
2728                 this.element.decorationsElement = document.createElement("div");
2729                 this.element.decorationsElement.className = "webkit-line-decorations";
2730                 this.element.decorationsElement._isDecorationsElement = true;
2731                 this.element.appendChild(this.element.decorationsElement);
2732             }
2733             this.element.decorationsElement.appendChild(decoration);
2734         }
2735         this._chunkedPanel.endDomUpdates();
2736     },
2737
2738     /**
2739      * @param {string|Element} decoration
2740      */
2741     removeDecoration: function(decoration)
2742     {
2743         this._chunkedPanel.beginDomUpdates();
2744         if (typeof decoration === "string")
2745             this.element.removeStyleClass(decoration);
2746         else if (this.element.decorationsElement)
2747             this.element.decorationsElement.removeChild(decoration);
2748         this._chunkedPanel.endDomUpdates();
2749     },
2750
2751     removeAllDecorations: function()
2752     {
2753         this._chunkedPanel.beginDomUpdates();
2754         this.element.className = "webkit-line-content";
2755         if (this.element.decorationsElement) {
2756             this.element.removeChild(this.element.decorationsElement);
2757             delete this.element.decorationsElement;
2758         }
2759         this._chunkedPanel.endDomUpdates();
2760     },
2761
2762     /**
2763      * @return {boolean}
2764      */
2765     isDecorated: function()
2766     {
2767         return this.element.className !== "webkit-line-content" || !!(this.element.decorationsElement && this.element.decorationsElement.firstChild);
2768     },
2769
2770     /**
2771      * @return {number}
2772      */
2773     get startLine()
2774     {
2775         return this._startLine;
2776     },
2777
2778     /**
2779      * @return {number}
2780      */
2781     get endLine()
2782     {
2783         return this._startLine + this.linesCount;
2784     },
2785
2786     set startLine(startLine)
2787     {
2788         this._startLine = startLine;
2789         this.element.lineNumber = startLine;
2790         if (this._expandedLineRows) {
2791             for (var i = 0; i < this._expandedLineRows.length; ++i)
2792                 this._expandedLineRows[i].lineNumber = startLine + i;
2793         }
2794     },
2795
2796     /**
2797      * @return {boolean}
2798      */
2799     expanded: function()
2800     {
2801         return this._expanded;
2802     },
2803
2804     expand: function()
2805     {
2806         if (this._expanded)
2807             return;
2808
2809         this._expanded = true;
2810
2811         if (this.linesCount === 1) {
2812             this._chunkedPanel._paintLines(this.startLine, this.startLine + 1);
2813             return;
2814         }
2815
2816         this._chunkedPanel.beginDomUpdates();
2817
2818         this._expandedLineRows = [];
2819         var parentElement = this.element.parentElement;
2820         for (var i = this.startLine; i < this.startLine + this.linesCount; ++i) {
2821             var lineRow = this._createRow(i);
2822             parentElement.insertBefore(lineRow, this.element);
2823             this._expandedLineRows.push(lineRow);
2824         }
2825         parentElement.removeChild(this.element);
2826         this._chunkedPanel._paintLines(this.startLine, this.startLine + this.linesCount);
2827
2828         this._chunkedPanel.endDomUpdates();
2829     },
2830
2831     collapse: function()
2832     {
2833         if (!this._expanded)
2834             return;
2835
2836         this._expanded = false;
2837         if (this.linesCount === 1)
2838             return;
2839
2840         this._chunkedPanel.beginDomUpdates();
2841
2842         var elementInserted = false;
2843         for (var i = 0; i < this._expandedLineRows.length; ++i) {
2844             var lineRow = this._expandedLineRows[i];
2845             var parentElement = lineRow.parentElement;
2846             if (parentElement) {
2847                 if (!elementInserted) {
2848                     elementInserted = true;
2849                     parentElement.insertBefore(this.element, lineRow);
2850                 }
2851                 parentElement.removeChild(lineRow);
2852             }
2853             this._chunkedPanel._releaseLinesHighlight(lineRow);
2854         }
2855         delete this._expandedLineRows;
2856
2857         this._chunkedPanel.endDomUpdates();
2858     },
2859
2860     /**
2861      * @return {number}
2862      */
2863     get height()
2864     {
2865         if (!this._expandedLineRows)
2866             return this._chunkedPanel.totalHeight(this.element);
2867         return this._chunkedPanel.totalHeight(this._expandedLineRows[0], this._expandedLineRows[this._expandedLineRows.length - 1]);
2868     },
2869
2870     /**
2871      * @return {number}
2872      */
2873     get offsetTop()
2874     {
2875         return (this._expandedLineRows && this._expandedLineRows.length) ? this._expandedLineRows[0].offsetTop : this.element.offsetTop;
2876     },
2877
2878     /**
2879      * @param {number} lineNumber
2880      * @return {Element}
2881      */
2882     _createRow: function(lineNumber)
2883     {
2884         var lineRow = this._chunkedPanel._cachedRows.pop() || document.createElement("div");
2885         lineRow.lineNumber = lineNumber;
2886         lineRow.className = "webkit-line-content";
2887         lineRow.textContent = this._textModel.line(lineNumber);
2888         if (!lineRow.textContent)
2889             lineRow.appendChild(document.createElement("br"));
2890         return lineRow;
2891     },
2892
2893     /**
2894      * Called on potentially damaged / inconsistent chunk
2895      * @param {number} lineNumber
2896      * @return {?Node}
2897      */
2898     lineRowContainingLine: function(lineNumber)
2899     {
2900         if (!this._expanded)
2901             return this.element;
2902         return this.expandedLineRow(lineNumber);
2903     },
2904
2905     /**
2906      * @param {number} lineNumber
2907      * @return {Element}
2908      */
2909     expandedLineRow: function(lineNumber)
2910     {
2911         if (!this._expanded || lineNumber < this.startLine || lineNumber >= this.startLine + this.linesCount)
2912             return null;
2913         if (!this._expandedLineRows)
2914             return this.element;
2915         return this._expandedLineRows[lineNumber - this.startLine];
2916     },
2917
2918     updateCollapsedLineRow: function()
2919     {
2920         if (this.linesCount === 1 && this._expanded)
2921             return;
2922
2923         var lines = [];
2924         for (var i = this.startLine; i < this.startLine + this.linesCount; ++i)
2925             lines.push(this._textModel.line(i));
2926
2927         if (WebInspector.FALSE)
2928             console.log("Rebuilding chunk with " + lines.length + " lines");
2929
2930         this.element.removeChildren();
2931         this.element.textContent = lines.join("\n");
2932         // The last empty line will get swallowed otherwise.
2933         if (!lines[lines.length - 1])
2934             this.element.appendChild(document.createElement("br"));
2935     },
2936
2937     firstElement: function()
2938     {
2939         return this._expandedLineRows ? this._expandedLineRows[0] : this.element;
2940     },
2941
2942     /**
2943      * @return {Element}
2944      */
2945     lastElement: function()
2946     {
2947         return this._expandedLineRows ? this._expandedLineRows[this._expandedLineRows.length - 1] : this.element;
2948     }
2949 }
2950
2951 /**
2952  * @constructor
2953  * @param {WebInspector.TextEditorMainPanel} mainPanel
2954  * @param {WebInspector.TextEditorModel} textModel
2955  */
2956 WebInspector.TextEditorMainPanel.TokenHighlighter = function(mainPanel, textModel)
2957 {
2958     this._mainPanel = mainPanel;
2959     this._textModel = textModel;
2960 }
2961
2962 WebInspector.TextEditorMainPanel.TokenHighlighter._NonWordCharRegex = /[^a-zA-Z0-9_]/;
2963 WebInspector.TextEditorMainPanel.TokenHighlighter._WordRegex = /^[a-zA-Z0-9_]+$/;
2964
2965 WebInspector.TextEditorMainPanel.TokenHighlighter.prototype = {
2966     /**
2967      * @param {WebInspector.TextRange} range
2968      */
2969     handleSelectionChange: function(range)
2970     {
2971         if (!range) {
2972             this._removeHighlight();
2973             return;
2974         }
2975
2976         if (range.startLine !== range.endLine) {
2977             this._removeHighlight();
2978             return;
2979         }
2980
2981         range = range.normalize();
2982         var selectedText = this._textModel.copyRange(range);
2983         if (selectedText === this._selectedWord)
2984             return;
2985
2986         if (selectedText === "") {
2987             this._removeHighlight();
2988             return;
2989         }
2990
2991         if (this._isWord(range, selectedText))
2992             this._highlight(selectedText);
2993         else
2994             this._removeHighlight();
2995     },
2996
2997     /**
2998      * @param {string} word
2999      */
3000     _regexString: function(word)
3001     {
3002         return "\\b" + word + "\\b";
3003     },
3004
3005     /**
3006      * @param {string} selectedWord
3007      */
3008     _highlight: function(selectedWord)
3009     {
3010         this._removeHighlight();
3011         this._selectedWord = selectedWord;
3012         this._mainPanel.highlightRegex(this._regexString(selectedWord), "text-editor-token-highlight")
3013     },
3014
3015     _removeHighlight: function()
3016     {
3017         if (this._selectedWord) {
3018             this._mainPanel.removeRegexHighlight(this._regexString(this._selectedWord));
3019             delete this._selectedWord;
3020         }
3021     },
3022
3023     /**
3024      * @param {WebInspector.TextRange} range
3025      * @param {string} selectedText
3026      * @return {boolean}
3027      */
3028     _isWord: function(range, selectedText)
3029     {
3030         const NonWordChar = WebInspector.TextEditorMainPanel.TokenHighlighter._NonWordCharRegex;
3031         const WordRegex = WebInspector.TextEditorMainPanel.TokenHighlighter._WordRegex;
3032         var line = this._textModel.line(range.startLine);
3033         var leftBound = range.startColumn === 0 || NonWordChar.test(line.charAt(range.startColumn - 1));
3034         var rightBound = range.endColumn === line.length - 1 || NonWordChar.test(line.charAt(range.endColumn));
3035         return leftBound && rightBound && WordRegex.test(selectedText);
3036     }
3037 }
3038
3039 WebInspector.debugDefaultTextEditor = false;