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