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