tizen beta release
[profile/ivi/webkit-efl.git] / debian / tmp / usr / share / ewebkit-0 / webinspector / TextViewer.js
1 /*
2  * Copyright (C) 2011 Google Inc. All rights reserved.
3  * Copyright (C) 2010 Apple Inc. All rights reserved.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions are
7  * met:
8  *
9  *     * Redistributions of source code must retain the above copyright
10  * notice, this list of conditions and the following disclaimer.
11  *     * Redistributions in binary form must reproduce the above
12  * copyright notice, this list of conditions and the following disclaimer
13  * in the documentation and/or other materials provided with the
14  * distribution.
15  *     * Neither the name of Google Inc. nor the names of its
16  * contributors may be used to endorse or promote products derived from
17  * this software without specific prior written permission.
18  *
19  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30  */
31
32 /**
33  * @extends {WebInspector.View}
34  * @constructor
35  */
36 WebInspector.TextViewer = function(textModel, platform, url, delegate)
37 {
38     WebInspector.View.call(this);
39     this.registerRequiredCSS("textViewer.css");
40
41     this._textModel = textModel;
42     this._textModel.changeListener = this._textChanged.bind(this);
43     this._textModel.resetUndoStack();
44     this._delegate = delegate;
45
46     this.element.className = "text-editor monospace";
47
48     var enterTextChangeMode = this._enterInternalTextChangeMode.bind(this);
49     var exitTextChangeMode = this._exitInternalTextChangeMode.bind(this);
50     var syncScrollListener = this._syncScroll.bind(this);
51     var syncDecorationsForLineListener = this._syncDecorationsForLine.bind(this);
52     var syncLineHeightListener = this._syncLineHeight.bind(this);
53     this._mainPanel = new WebInspector.TextEditorMainPanel(this._textModel, url, syncScrollListener, syncDecorationsForLineListener, enterTextChangeMode, exitTextChangeMode);
54     this._gutterPanel = new WebInspector.TextEditorGutterPanel(this._textModel, syncDecorationsForLineListener, syncLineHeightListener);
55     this.element.appendChild(this._mainPanel.element);
56     this.element.appendChild(this._gutterPanel.element);
57
58     // Forward mouse wheel events from the unscrollable gutter to the main panel.
59     function forwardWheelEvent(event)
60     {
61         var clone = document.createEvent("WheelEvent");
62         clone.initWebKitWheelEvent(event.wheelDeltaX, event.wheelDeltaY,
63                                    event.view,
64                                    event.screenX, event.screenY,
65                                    event.clientX, event.clientY,
66                                    event.ctrlKey, event.altKey, event.shiftKey, event.metaKey);
67         this._mainPanel.element.dispatchEvent(clone);
68     }
69     this._gutterPanel.element.addEventListener("mousewheel", forwardWheelEvent.bind(this), false);
70
71     this.element.addEventListener("dblclick", this._doubleClick.bind(this), true);
72     this.element.addEventListener("keydown", this._handleKeyDown.bind(this), false);
73     this.element.addEventListener("contextmenu", this._contextMenu.bind(this), true);
74
75     this._registerShortcuts();
76 }
77
78 WebInspector.TextViewer.prototype = {
79     set mimeType(mimeType)
80     {
81         this._mainPanel.mimeType = mimeType;
82     },
83
84     set readOnly(readOnly)
85     {
86         if (this._mainPanel.readOnly === readOnly)
87             return;
88         this._mainPanel.readOnly = readOnly;
89     },
90
91     get readOnly()
92     {
93         return this._mainPanel.readOnly;
94     },
95
96     get textModel()
97     {
98         return this._textModel;
99     },
100
101     focus: function()
102     {
103         this._mainPanel.element.focus();
104     },
105
106     revealLine: function(lineNumber)
107     {
108         this._mainPanel.revealLine(lineNumber);
109     },
110
111     addDecoration: function(lineNumber, decoration)
112     {
113         this._mainPanel.addDecoration(lineNumber, decoration);
114         this._gutterPanel.addDecoration(lineNumber, decoration);
115     },
116
117     removeDecoration: function(lineNumber, decoration)
118     {
119         this._mainPanel.removeDecoration(lineNumber, decoration);
120         this._gutterPanel.removeDecoration(lineNumber, decoration);
121     },
122
123     markAndRevealRange: function(range)
124     {
125         this._mainPanel.markAndRevealRange(range);
126     },
127
128     highlightLine: function(lineNumber)
129     {
130         if (typeof lineNumber !== "number" || lineNumber < 0)
131             return;
132
133         lineNumber = Math.min(lineNumber, this._textModel.linesCount - 1);
134         this._mainPanel.highlightLine(lineNumber);
135     },
136
137     clearLineHighlight: function()
138     {
139         this._mainPanel.clearLineHighlight();
140     },
141
142     freeCachedElements: function()
143     {
144         this._mainPanel.freeCachedElements();
145         this._gutterPanel.freeCachedElements();
146     },
147
148     elementsToRestoreScrollPositionsFor: function()
149     {
150         return [this._mainPanel.element];
151     },
152
153     inheritScrollPositions: function(textViewer)
154     {
155         this._mainPanel.element._scrollTop = textViewer._mainPanel.element.scrollTop;
156         this._mainPanel.element._scrollLeft = textViewer._mainPanel.element.scrollLeft;
157     },
158
159     beginUpdates: function()
160     {
161         this._mainPanel.beginUpdates();
162         this._gutterPanel.beginUpdates();
163     },
164
165     endUpdates: function()
166     {
167         this._mainPanel.endUpdates();
168         this._gutterPanel.endUpdates();
169         this._updatePanelOffsets();
170     },
171
172     onResize: function()
173     {
174         this._mainPanel.resize();
175         this._gutterPanel.resize();
176         this._updatePanelOffsets();
177     },
178
179     // WebInspector.TextModel listener
180     _textChanged: function(oldRange, newRange, oldText, newText)
181     {
182         if (!this._internalTextChangeMode)
183             this._textModel.resetUndoStack();
184         this._mainPanel.textChanged(oldRange, newRange);
185         this._gutterPanel.textChanged(oldRange, newRange);
186         this._updatePanelOffsets();
187     },
188
189     _enterInternalTextChangeMode: function()
190     {
191         this._internalTextChangeMode = true;
192         this._delegate.beforeTextChanged();
193     },
194
195     _exitInternalTextChangeMode: function(oldRange, newRange)
196     {
197         this._internalTextChangeMode = false;
198         this._delegate.afterTextChanged(oldRange, newRange);
199     },
200
201     _updatePanelOffsets: function()
202     {
203         var lineNumbersWidth = this._gutterPanel.element.offsetWidth;
204         if (lineNumbersWidth)
205             this._mainPanel.element.style.setProperty("left", lineNumbersWidth + "px");
206         else
207             this._mainPanel.element.style.removeProperty("left"); // Use default value set in CSS.
208     },
209
210     _syncScroll: function()
211     {
212         var mainElement = this._mainPanel.element;
213         var gutterElement = this._gutterPanel.element;
214         // Handle horizontal scroll bar at the bottom of the main panel.
215         this._gutterPanel.syncClientHeight(mainElement.clientHeight);
216         gutterElement.scrollTop = mainElement.scrollTop;
217     },
218
219     _syncDecorationsForLine: function(lineNumber)
220     {
221         if (lineNumber >= this._textModel.linesCount)
222             return;
223
224         var mainChunk = this._mainPanel.chunkForLine(lineNumber);
225         if (mainChunk.linesCount === 1 && mainChunk.decorated) {
226             var gutterChunk = this._gutterPanel.makeLineAChunk(lineNumber);
227             var height = mainChunk.height;
228             if (height)
229                 gutterChunk.element.style.setProperty("height", height + "px");
230             else
231                 gutterChunk.element.style.removeProperty("height");
232         } else {
233             var gutterChunk = this._gutterPanel.chunkForLine(lineNumber);
234             if (gutterChunk.linesCount === 1)
235                 gutterChunk.element.style.removeProperty("height");
236         }
237     },
238
239     _syncLineHeight: function(gutterRow) {
240         if (this._lineHeightSynced)
241             return;
242         if (gutterRow && gutterRow.offsetHeight) {
243             // Force equal line heights for the child panels.
244             this.element.style.setProperty("line-height", gutterRow.offsetHeight + "px");
245             this._lineHeightSynced = true;
246         }
247     },
248
249     _doubleClick: function(event)
250     {
251         if (!this.readOnly)
252             return;
253
254         var lineRow = event.target.enclosingNodeOrSelfWithClass("webkit-line-content");
255         if (!lineRow)
256             return;  // Do not trigger editing from line numbers.
257
258         this._delegate.doubleClick(lineRow.lineNumber);
259         window.getSelection().collapseToStart();
260     },
261
262     _registerShortcuts: function()
263     {
264         var keys = WebInspector.KeyboardShortcut.Keys;
265         var modifiers = WebInspector.KeyboardShortcut.Modifiers;
266
267         this._shortcuts = {};
268         var commitEditing = this._commitEditing.bind(this);
269         var cancelEditing = this._cancelEditing.bind(this);
270         this._shortcuts[WebInspector.KeyboardShortcut.makeKey("s", modifiers.CtrlOrMeta)] = commitEditing;
271         this._shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Enter.code, modifiers.CtrlOrMeta)] = commitEditing;
272         this._shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Esc.code)] = cancelEditing;
273
274         var handleEnterKey = this._mainPanel.handleEnterKey.bind(this._mainPanel);
275         this._shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Enter.code, WebInspector.KeyboardShortcut.Modifiers.None)] = handleEnterKey;
276
277         var handleUndo = this._mainPanel.handleUndoRedo.bind(this._mainPanel, false);
278         var handleRedo = this._mainPanel.handleUndoRedo.bind(this._mainPanel, true);
279         this._shortcuts[WebInspector.KeyboardShortcut.makeKey("z", modifiers.CtrlOrMeta)] = handleUndo;
280         this._shortcuts[WebInspector.KeyboardShortcut.makeKey("z", modifiers.Shift | modifiers.CtrlOrMeta)] = handleRedo;
281
282         var handleTabKey = this._mainPanel.handleTabKeyPress.bind(this._mainPanel, false);
283         var handleShiftTabKey = this._mainPanel.handleTabKeyPress.bind(this._mainPanel, true);
284         this._shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Tab.code)] = handleTabKey;
285         this._shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Tab.code, modifiers.Shift)] = handleShiftTabKey;
286     },
287
288     _handleKeyDown: function(e)
289     {
290         if (this.readOnly)
291             return;
292
293         var shortcutKey = WebInspector.KeyboardShortcut.makeKeyFromEvent(e);
294         var handler = this._shortcuts[shortcutKey];
295         if (handler && handler()) {
296             e.preventDefault();
297             e.stopPropagation();
298         }
299     },
300
301     _contextMenu: function(event)
302     {
303         var contextMenu = new WebInspector.ContextMenu();
304         var target = event.target.enclosingNodeOrSelfWithClass("webkit-line-number");
305         if (target)
306             this._delegate.populateLineGutterContextMenu(target.lineNumber, contextMenu);
307         else
308             this._delegate.populateTextAreaContextMenu(contextMenu);
309
310         var fileName = this._delegate.suggestedFileName();
311         if (fileName)
312             contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Save as..." : "Save As..."), InspectorFrontendHost.saveAs.bind(InspectorFrontendHost, fileName, this._textModel.text));
313
314         contextMenu.show(event);
315     },
316
317     _commitEditing: function()
318     {
319         if (this.readOnly)
320             return false;
321
322         this._delegate.commitEditing();
323         return true;
324     },
325
326     _cancelEditing: function()
327     {
328         if (this.readOnly)
329             return false;
330
331         this._delegate.cancelEditing();
332         return true;
333     }
334 }
335
336 WebInspector.TextViewer.prototype.__proto__ = WebInspector.View.prototype;
337
338 /**
339  * @interface
340  */
341 WebInspector.TextViewerDelegate = function()
342 {
343 }
344
345 WebInspector.TextViewerDelegate.prototype = {
346     doubleClick: function(lineNumber) { },
347
348     beforeTextChanged: function() { },
349
350     afterTextChanged: function(oldRange, newRange) { },
351
352     commitEditing: function() { },
353
354     cancelEditing: function() { },
355
356     populateLineGutterContextMenu: function(lineNumber, contextMenu) { },
357
358     populateTextAreaContextMenu: function(contextMenu) { },
359
360     suggestedFileName: function() { }
361 }
362
363 /**
364  * @constructor
365  */
366 WebInspector.TextEditorChunkedPanel = function(textModel)
367 {
368     this._textModel = textModel;
369
370     this._defaultChunkSize = 50;
371     this._paintCoalescingLevel = 0;
372     this._domUpdateCoalescingLevel = 0;
373 }
374
375 WebInspector.TextEditorChunkedPanel.prototype = {
376     get textModel()
377     {
378         return this._textModel;
379     },
380
381     revealLine: function(lineNumber)
382     {
383         if (lineNumber >= this._textModel.linesCount)
384             return;
385
386         var chunk = this.makeLineAChunk(lineNumber);
387         chunk.element.scrollIntoViewIfNeeded();
388     },
389
390     addDecoration: function(lineNumber, decoration)
391     {
392         if (lineNumber >= this._textModel.linesCount)
393             return;
394
395         var chunk = this.makeLineAChunk(lineNumber);
396         chunk.addDecoration(decoration);
397     },
398
399     removeDecoration: function(lineNumber, decoration)
400     {
401         if (lineNumber >= this._textModel.linesCount)
402             return;
403
404         var chunk = this.chunkForLine(lineNumber);
405         chunk.removeDecoration(decoration);
406     },
407
408     _buildChunks: function()
409     {
410         this.beginDomUpdates();
411
412         this._container.removeChildren();
413
414         this._textChunks = [];
415         for (var i = 0; i < this._textModel.linesCount; i += this._defaultChunkSize) {
416             var chunk = this._createNewChunk(i, i + this._defaultChunkSize);
417             this._textChunks.push(chunk);
418             this._container.appendChild(chunk.element);
419         }
420
421         this._repaintAll();
422
423         this.endDomUpdates();
424     },
425
426     makeLineAChunk: function(lineNumber)
427     {
428         var chunkNumber = this._chunkNumberForLine(lineNumber);
429         var oldChunk = this._textChunks[chunkNumber];
430
431         if (!oldChunk) {
432             console.error("No chunk for line number: " + lineNumber);
433             return;
434         }
435
436         if (oldChunk.linesCount === 1)
437             return oldChunk;
438
439         return this._splitChunkOnALine(lineNumber, chunkNumber, true);
440     },
441
442     _splitChunkOnALine: function(lineNumber, chunkNumber, createSuffixChunk)
443     {
444         this.beginDomUpdates();
445
446         var oldChunk = this._textChunks[chunkNumber];
447         var wasExpanded = oldChunk.expanded;
448         oldChunk.expanded = false;
449
450         var insertIndex = chunkNumber + 1;
451
452         // Prefix chunk.
453         if (lineNumber > oldChunk.startLine) {
454             var prefixChunk = this._createNewChunk(oldChunk.startLine, lineNumber);
455             prefixChunk.readOnly = oldChunk.readOnly;
456             this._textChunks.splice(insertIndex++, 0, prefixChunk);
457             this._container.insertBefore(prefixChunk.element, oldChunk.element);
458         }
459
460         // Line chunk.
461         var endLine = createSuffixChunk ? lineNumber + 1 : oldChunk.startLine + oldChunk.linesCount;
462         var lineChunk = this._createNewChunk(lineNumber, endLine);
463         lineChunk.readOnly = oldChunk.readOnly;
464         this._textChunks.splice(insertIndex++, 0, lineChunk);
465         this._container.insertBefore(lineChunk.element, oldChunk.element);
466
467         // Suffix chunk.
468         if (oldChunk.startLine + oldChunk.linesCount > endLine) {
469             var suffixChunk = this._createNewChunk(endLine, oldChunk.startLine + oldChunk.linesCount);
470             suffixChunk.readOnly = oldChunk.readOnly;
471             this._textChunks.splice(insertIndex, 0, suffixChunk);
472             this._container.insertBefore(suffixChunk.element, oldChunk.element);
473         }
474
475         // Remove enclosing chunk.
476         this._textChunks.splice(chunkNumber, 1);
477         this._container.removeChild(oldChunk.element);
478
479         if (wasExpanded) {
480             if (prefixChunk)
481                 prefixChunk.expanded = true;
482             lineChunk.expanded = true;
483             if (suffixChunk)
484                 suffixChunk.expanded = true;
485         }
486
487         this.endDomUpdates();
488
489         return lineChunk;
490     },
491
492     _scroll: function()
493     {
494         // FIXME: Replace the "2" with the padding-left value from CSS.
495         if (this.element.scrollLeft <= 2)
496             this.element.scrollLeft = 0;
497
498         this._scheduleRepaintAll();
499         if (this._syncScrollListener)
500             this._syncScrollListener();
501     },
502
503     _scheduleRepaintAll: function()
504     {
505         if (this._repaintAllTimer)
506             clearTimeout(this._repaintAllTimer);
507         this._repaintAllTimer = setTimeout(this._repaintAll.bind(this), 50);
508     },
509
510     beginUpdates: function()
511     {
512         this._paintCoalescingLevel++;
513     },
514
515     endUpdates: function()
516     {
517         this._paintCoalescingLevel--;
518         if (!this._paintCoalescingLevel)
519             this._repaintAll();
520     },
521
522     beginDomUpdates: function()
523     {
524         this._domUpdateCoalescingLevel++;
525     },
526
527     endDomUpdates: function()
528     {
529         this._domUpdateCoalescingLevel--;
530     },
531
532     _chunkNumberForLine: function(lineNumber)
533     {
534         function compareLineNumbers(value, chunk)
535         {
536             return value < chunk.startLine ? -1 : 1;
537         }
538         var insertBefore = insertionIndexForObjectInListSortedByFunction(lineNumber, this._textChunks, compareLineNumbers);
539         return insertBefore - 1;
540     },
541
542     chunkForLine: function(lineNumber)
543     {
544         return this._textChunks[this._chunkNumberForLine(lineNumber)];
545     },
546
547     _findFirstVisibleChunkNumber: function(visibleFrom)
548     {
549         function compareOffsetTops(value, chunk)
550         {
551             return value < chunk.offsetTop ? -1 : 1;
552         }
553         var insertBefore = insertionIndexForObjectInListSortedByFunction(visibleFrom, this._textChunks, compareOffsetTops);
554         return insertBefore - 1;
555     },
556
557     _findVisibleChunks: function(visibleFrom, visibleTo)
558     {
559         var from = this._findFirstVisibleChunkNumber(visibleFrom);
560         for (var to = from + 1; to < this._textChunks.length; ++to) {
561             if (this._textChunks[to].offsetTop >= visibleTo)
562                 break;
563         }
564         return { start: from, end: to };
565     },
566
567     _findFirstVisibleLineNumber: function(visibleFrom)
568     {
569         var chunk = this._textChunks[this._findFirstVisibleChunkNumber(visibleFrom)];
570         if (!chunk.expanded)
571             return chunk.startLine;
572
573         var lineNumbers = [];
574         for (var i = 0; i < chunk.linesCount; ++i) {
575             lineNumbers.push(chunk.startLine + i);
576         }
577
578         function compareLineRowOffsetTops(value, lineNumber)
579         {
580             var lineRow = chunk.getExpandedLineRow(lineNumber);
581             return value < lineRow.offsetTop ? -1 : 1;
582         }
583         var insertBefore = insertionIndexForObjectInListSortedByFunction(visibleFrom, lineNumbers, compareLineRowOffsetTops);
584         return lineNumbers[insertBefore - 1];
585     },
586
587     _repaintAll: function()
588     {
589         delete this._repaintAllTimer;
590
591         if (this._paintCoalescingLevel || this._dirtyLines)
592             return;
593
594         var visibleFrom = this.element.scrollTop;
595         var visibleTo = this.element.scrollTop + this.element.clientHeight;
596
597         if (visibleTo) {
598             var result = this._findVisibleChunks(visibleFrom, visibleTo);
599             this._expandChunks(result.start, result.end);
600         }
601     },
602
603     _expandChunks: function(fromIndex, toIndex)
604     {
605         // First collapse chunks to collect the DOM elements into a cache to reuse them later.
606         for (var i = 0; i < fromIndex; ++i)
607             this._textChunks[i].expanded = false;
608         for (var i = toIndex; i < this._textChunks.length; ++i)
609             this._textChunks[i].expanded = false;
610         for (var i = fromIndex; i < toIndex; ++i)
611             this._textChunks[i].expanded = true;
612     },
613
614     _totalHeight: function(firstElement, lastElement)
615     {
616         lastElement = (lastElement || firstElement).nextElementSibling;
617         if (lastElement)
618             return lastElement.offsetTop - firstElement.offsetTop;
619
620         var offsetParent = firstElement.offsetParent;
621         if (offsetParent && offsetParent.scrollHeight > offsetParent.clientHeight)
622             return offsetParent.scrollHeight - firstElement.offsetTop;
623
624         var total = 0;
625         while (firstElement && firstElement !== lastElement) {
626             total += firstElement.offsetHeight;
627             firstElement = firstElement.nextElementSibling;
628         }
629         return total;
630     },
631
632     resize: function()
633     {
634         this._repaintAll();
635     }
636 }
637
638 /**
639  * @constructor
640  * @extends {WebInspector.TextEditorChunkedPanel}
641  */
642 WebInspector.TextEditorGutterPanel = function(textModel, syncDecorationsForLineListener, syncLineHeightListener)
643 {
644     WebInspector.TextEditorChunkedPanel.call(this, textModel);
645
646     this._syncDecorationsForLineListener = syncDecorationsForLineListener;
647     this._syncLineHeightListener = syncLineHeightListener;
648
649     this.element = document.createElement("div");
650     this.element.className = "text-editor-lines";
651
652     this._container = document.createElement("div");
653     this._container.className = "inner-container";
654     this.element.appendChild(this._container);
655
656     this.element.addEventListener("scroll", this._scroll.bind(this), false);
657
658     this.freeCachedElements();
659     this._buildChunks();
660 }
661
662 WebInspector.TextEditorGutterPanel.prototype = {
663     freeCachedElements: function()
664     {
665         this._cachedRows = [];
666     },
667
668     _createNewChunk: function(startLine, endLine)
669     {
670         return new WebInspector.TextEditorGutterChunk(this, startLine, endLine);
671     },
672
673     textChanged: function(oldRange, newRange)
674     {
675         this.beginDomUpdates();
676
677         var linesDiff = newRange.linesCount - oldRange.linesCount;
678         if (linesDiff) {
679             // Remove old chunks (if needed).
680             for (var chunkNumber = this._textChunks.length - 1; chunkNumber >= 0 ; --chunkNumber) {
681                 var chunk = this._textChunks[chunkNumber];
682                 if (chunk.startLine + chunk.linesCount <= this._textModel.linesCount)
683                     break;
684                 chunk.expanded = false;
685                 this._container.removeChild(chunk.element);
686             }
687             this._textChunks.length = chunkNumber + 1;
688
689             // Add new chunks (if needed).
690             var totalLines = 0;
691             if (this._textChunks.length) {
692                 var lastChunk = this._textChunks[this._textChunks.length - 1];
693                 totalLines = lastChunk.startLine + lastChunk.linesCount;
694             }
695             for (var i = totalLines; i < this._textModel.linesCount; i += this._defaultChunkSize) {
696                 var chunk = this._createNewChunk(i, i + this._defaultChunkSize);
697                 this._textChunks.push(chunk);
698                 this._container.appendChild(chunk.element);
699             }
700             this._repaintAll();
701         } else {
702             // Decorations may have been removed, so we may have to sync those lines.
703             var chunkNumber = this._chunkNumberForLine(newRange.startLine);
704             var chunk = this._textChunks[chunkNumber];
705             while (chunk && chunk.startLine <= newRange.endLine) {
706                 if (chunk.linesCount === 1)
707                     this._syncDecorationsForLineListener(chunk.startLine);
708                 chunk = this._textChunks[++chunkNumber];
709             }
710         }
711
712         this.endDomUpdates();
713     },
714
715     syncClientHeight: function(clientHeight)
716     {
717         if (this.element.offsetHeight > clientHeight)
718             this._container.style.setProperty("padding-bottom", (this.element.offsetHeight - clientHeight) + "px");
719         else
720             this._container.style.removeProperty("padding-bottom");
721     }
722 }
723
724 WebInspector.TextEditorGutterPanel.prototype.__proto__ = WebInspector.TextEditorChunkedPanel.prototype;
725
726 /**
727  * @constructor
728  */
729 WebInspector.TextEditorGutterChunk = function(textViewer, startLine, endLine)
730 {
731     this._textViewer = textViewer;
732     this._textModel = textViewer._textModel;
733
734     this.startLine = startLine;
735     endLine = Math.min(this._textModel.linesCount, endLine);
736     this.linesCount = endLine - startLine;
737
738     this._expanded = false;
739
740     this.element = document.createElement("div");
741     this.element.lineNumber = startLine;
742     this.element.className = "webkit-line-number";
743
744     if (this.linesCount === 1) {
745         // Single line chunks are typically created for decorations. Host line number in
746         // the sub-element in order to allow flexible border / margin management.
747         var innerSpan = document.createElement("span");
748         innerSpan.className = "webkit-line-number-inner";
749         innerSpan.textContent = startLine + 1;
750         var outerSpan = document.createElement("div");
751         outerSpan.className = "webkit-line-number-outer";
752         outerSpan.appendChild(innerSpan);
753         this.element.appendChild(outerSpan);
754     } else {
755         var lineNumbers = [];
756         for (var i = startLine; i < endLine; ++i)
757             lineNumbers.push(i + 1);
758         this.element.textContent = lineNumbers.join("\n");
759     }
760 }
761
762 WebInspector.TextEditorGutterChunk.prototype = {
763     addDecoration: function(decoration)
764     {
765         this._textViewer.beginDomUpdates();
766         if (typeof decoration === "string")
767             this.element.addStyleClass(decoration);
768         this._textViewer.endDomUpdates();
769     },
770
771     removeDecoration: function(decoration)
772     {
773         this._textViewer.beginDomUpdates();
774         if (typeof decoration === "string")
775             this.element.removeStyleClass(decoration);
776         this._textViewer.endDomUpdates();
777     },
778
779     get expanded()
780     {
781         return this._expanded;
782     },
783
784     set expanded(expanded)
785     {
786         if (this.linesCount === 1)
787             this._textViewer._syncDecorationsForLineListener(this.startLine);
788
789         if (this._expanded === expanded)
790             return;
791
792         this._expanded = expanded;
793
794         if (this.linesCount === 1)
795             return;
796
797         this._textViewer.beginDomUpdates();
798
799         if (expanded) {
800             this._expandedLineRows = [];
801             var parentElement = this.element.parentElement;
802             for (var i = this.startLine; i < this.startLine + this.linesCount; ++i) {
803                 var lineRow = this._createRow(i);
804                 parentElement.insertBefore(lineRow, this.element);
805                 this._expandedLineRows.push(lineRow);
806             }
807             parentElement.removeChild(this.element);
808             this._textViewer._syncLineHeightListener(this._expandedLineRows[0]);
809         } else {
810             var elementInserted = false;
811             for (var i = 0; i < this._expandedLineRows.length; ++i) {
812                 var lineRow = this._expandedLineRows[i];
813                 var parentElement = lineRow.parentElement;
814                 if (parentElement) {
815                     if (!elementInserted) {
816                         elementInserted = true;
817                         parentElement.insertBefore(this.element, lineRow);
818                     }
819                     parentElement.removeChild(lineRow);
820                 }
821                 this._textViewer._cachedRows.push(lineRow);
822             }
823             delete this._expandedLineRows;
824         }
825
826         this._textViewer.endDomUpdates();
827     },
828
829     get height()
830     {
831         if (!this._expandedLineRows)
832             return this._textViewer._totalHeight(this.element);
833         return this._textViewer._totalHeight(this._expandedLineRows[0], this._expandedLineRows[this._expandedLineRows.length - 1]);
834     },
835
836     get offsetTop()
837     {
838         return (this._expandedLineRows && this._expandedLineRows.length) ? this._expandedLineRows[0].offsetTop : this.element.offsetTop;
839     },
840
841     _createRow: function(lineNumber)
842     {
843         var lineRow = this._textViewer._cachedRows.pop() || document.createElement("div");
844         lineRow.lineNumber = lineNumber;
845         lineRow.className = "webkit-line-number";
846         lineRow.textContent = lineNumber + 1;
847         return lineRow;
848     }
849 }
850
851 /**
852  * @constructor
853  * @extends {WebInspector.TextEditorChunkedPanel}
854  */
855 WebInspector.TextEditorMainPanel = function(textModel, url, syncScrollListener, syncDecorationsForLineListener, enterTextChangeMode, exitTextChangeMode)
856 {
857     WebInspector.TextEditorChunkedPanel.call(this, textModel);
858
859     this._syncScrollListener = syncScrollListener;
860     this._syncDecorationsForLineListener = syncDecorationsForLineListener;
861     this._enterTextChangeMode = enterTextChangeMode;
862     this._exitTextChangeMode = exitTextChangeMode;
863
864     this._url = url;
865     this._highlighter = new WebInspector.TextEditorHighlighter(textModel, this._highlightDataReady.bind(this));
866     this._readOnly = true;
867
868     this.element = document.createElement("div");
869     this.element.className = "text-editor-contents";
870     this.element.tabIndex = 0;
871
872     this._container = document.createElement("div");
873     this._container.className = "inner-container";
874     this._container.tabIndex = 0;
875     this.element.appendChild(this._container);
876
877     this.element.addEventListener("scroll", this._scroll.bind(this), false);
878
879     // In WebKit the DOMNodeRemoved event is fired AFTER the node is removed, thus it should be
880     // attached to all DOM nodes that we want to track. Instead, we attach the DOMNodeRemoved
881     // listeners only on the line rows, and use DOMSubtreeModified to track node removals inside
882     // the line rows. For more info see: https://bugs.webkit.org/show_bug.cgi?id=55666
883     //
884     // OPTIMIZATION. It is very expensive to listen to the DOM mutation events, thus we remove the
885     // listeners whenever we do any internal DOM manipulations (such as expand/collapse line rows)
886     // and set the listeners back when we are finished.
887     this._handleDOMUpdatesCallback = this._handleDOMUpdates.bind(this);
888     this._container.addEventListener("DOMCharacterDataModified", this._handleDOMUpdatesCallback, false);
889     this._container.addEventListener("DOMNodeInserted", this._handleDOMUpdatesCallback, false);
890     this._container.addEventListener("DOMSubtreeModified", this._handleDOMUpdatesCallback, false);
891
892     this.freeCachedElements();
893     this._buildChunks();
894 }
895
896 WebInspector.TextEditorMainPanel.prototype = {
897     set mimeType(mimeType)
898     {
899         this._highlighter.mimeType = mimeType;
900     },
901
902     set readOnly(readOnly)
903     {
904         if (this._readOnly === readOnly)
905             return;
906
907         this.beginDomUpdates();
908         this._readOnly = readOnly;
909         if (this._readOnly)
910             this._container.removeStyleClass("text-editor-editable");
911         else {
912             this._container.addStyleClass("text-editor-editable");
913             this._updateSelectionOnStartEditing();
914         }
915         this.endDomUpdates();
916     },
917
918     get readOnly()
919     {
920         return this._readOnly;
921     },
922
923     _updateSelectionOnStartEditing: function()
924     {
925         // focus() needs to go first for the case when the last selection was inside the editor and
926         // the "Edit" button was clicked. In this case we bail at the check below, but the
927         // editor does not receive the focus, thus "Esc" does not cancel editing until at least
928         // one change has been made to the editor contents.
929         this._container.focus();
930         var selection = window.getSelection();
931         if (selection.rangeCount) {
932             var commonAncestorContainer = selection.getRangeAt(0).commonAncestorContainer;
933             if (this._container === commonAncestorContainer || this._container.isAncestor(commonAncestorContainer))
934                 return;
935         }
936
937         selection.removeAllRanges();
938         var range = document.createRange();
939         range.setStart(this._container, 0);
940         range.setEnd(this._container, 0);
941         selection.addRange(range);
942     },
943
944     setEditableRange: function(startLine, endLine)
945     {
946         this.beginDomUpdates();
947
948         var firstChunkNumber = this._chunkNumberForLine(startLine);
949         var firstChunk = this._textChunks[firstChunkNumber];
950         if (firstChunk.startLine !== startLine) {
951             this._splitChunkOnALine(startLine, firstChunkNumber);
952             firstChunkNumber += 1;
953         }
954
955         var lastChunkNumber = this._textChunks.length;
956         if (endLine !== this._textModel.linesCount) {
957             lastChunkNumber = this._chunkNumberForLine(endLine);
958             var lastChunk = this._textChunks[lastChunkNumber];
959             if (lastChunk && lastChunk.startLine !== endLine) {
960                 this._splitChunkOnALine(endLine, lastChunkNumber);
961                 lastChunkNumber += 1;
962             }
963         }
964
965         for (var chunkNumber = 0; chunkNumber < firstChunkNumber; ++chunkNumber)
966             this._textChunks[chunkNumber].readOnly = true;
967         for (var chunkNumber = firstChunkNumber; chunkNumber < lastChunkNumber; ++chunkNumber)
968             this._textChunks[chunkNumber].readOnly = false;
969         for (var chunkNumber = lastChunkNumber; chunkNumber < this._textChunks.length; ++chunkNumber)
970             this._textChunks[chunkNumber].readOnly = true;
971
972         this.endDomUpdates();
973     },
974
975     clearEditableRange: function()
976     {
977         for (var chunkNumber = 0; chunkNumber < this._textChunks.length; ++chunkNumber)
978             this._textChunks[chunkNumber].readOnly = false;
979     },
980
981     markAndRevealRange: function(range)
982     {
983         if (this._rangeToMark) {
984             var markedLine = this._rangeToMark.startLine;
985             delete this._rangeToMark;
986             // Remove the marked region immediately.
987             if (!this._dirtyLines) {
988                 this.beginDomUpdates();
989                 var chunk = this.chunkForLine(markedLine);
990                 var wasExpanded = chunk.expanded;
991                 chunk.expanded = false;
992                 chunk.updateCollapsedLineRow();
993                 chunk.expanded = wasExpanded;
994                 this.endDomUpdates();
995             } else
996                 this._paintLines(markedLine, markedLine + 1);
997         }
998
999         if (range) {
1000             this._rangeToMark = range;
1001             this.revealLine(range.startLine);
1002             var chunk = this.makeLineAChunk(range.startLine);
1003             this._paintLine(chunk.element);
1004             if (this._markedRangeElement)
1005                 this._markedRangeElement.scrollIntoViewIfNeeded();
1006         }
1007         delete this._markedRangeElement;
1008     },
1009
1010     highlightLine: function(lineNumber)
1011     {
1012         this.clearLineHighlight();
1013         this._highlightedLine = lineNumber;
1014         this.revealLine(lineNumber);
1015         this.addDecoration(lineNumber, "webkit-highlighted-line");
1016     },
1017
1018     clearLineHighlight: function()
1019     {
1020         if (typeof this._highlightedLine === "number") {
1021             this.removeDecoration(this._highlightedLine, "webkit-highlighted-line");
1022             delete this._highlightedLine;
1023         }
1024     },
1025
1026     freeCachedElements: function()
1027     {
1028         this._cachedSpans = [];
1029         this._cachedTextNodes = [];
1030         this._cachedRows = [];
1031     },
1032
1033     handleUndoRedo: function(redo)
1034     {
1035         if (this._dirtyLines)
1036             return false;
1037
1038         this.beginUpdates();
1039         this._enterTextChangeMode();
1040
1041         var callback = function(oldRange, newRange) {
1042             this._exitTextChangeMode(oldRange, newRange);
1043             this._enterTextChangeMode();
1044         }.bind(this);
1045
1046         var range = redo ? this._textModel.redo(callback) : this._textModel.undo(callback);
1047         if (range)
1048             this._setCaretLocation(range.endLine, range.endColumn, true);
1049
1050         this._exitTextChangeMode(null, null);
1051         this.endUpdates();
1052
1053         return true;
1054     },
1055
1056     handleTabKeyPress: function(shiftKey)
1057     {
1058         if (this._dirtyLines)
1059             return false;
1060
1061         var selection = this._getSelection();
1062         if (!selection)
1063             return false;
1064
1065         var range = selection.normalize();
1066
1067         this.beginUpdates();
1068         this._enterTextChangeMode();
1069
1070         var newRange;
1071         if (shiftKey)
1072             newRange = this._unindentLines(range);
1073         else {
1074             if (range.isEmpty()) {
1075                 newRange = this._setText(range, WebInspector.settings.textEditorIndent.get());
1076                 newRange.startColumn = newRange.endColumn;
1077             } else
1078                 newRange = this._indentLines(range);
1079
1080         }
1081
1082         this._exitTextChangeMode(range, newRange);
1083         this.endUpdates();
1084         this._restoreSelection(newRange, true);
1085         return true;
1086     },
1087
1088     _indentLines: function(range)
1089     {
1090         var indent = WebInspector.settings.textEditorIndent.get();
1091
1092         if (this._lastEditedRange)
1093             this._textModel.markUndoableState();
1094
1095         for (var lineNumber = range.startLine; lineNumber <= range.endLine; lineNumber++)
1096             this._textModel.setText(new WebInspector.TextRange(lineNumber, 0, lineNumber, 0), indent);
1097
1098         var newRange = range.clone();
1099         newRange.startColumn += indent.length;
1100         newRange.endColumn += indent.length;
1101         this._lastEditedRange = newRange;
1102
1103         return newRange;
1104     },
1105
1106     _unindentLines: function(range)
1107     {
1108         if (this._lastEditedRange)
1109             this._textModel.markUndoableState();
1110
1111         var indent = WebInspector.settings.textEditorIndent.get();
1112         var indentLength = indent === WebInspector.TextEditorModel.Indent.TabCharacter ? 4 : indent.length;
1113         var lineIndentRegex = new RegExp("^ {1," + indentLength + "}");
1114         var newRange = range.clone();
1115
1116         for (var lineNumber = range.startLine; lineNumber <= range.endLine; lineNumber++) {
1117             var line = this._textModel.line(lineNumber);
1118             var firstCharacter = line.charAt(0);
1119             var lineIndentLength;
1120
1121             if (firstCharacter === " ")
1122                 lineIndentLength = line.match(lineIndentRegex)[0].length;
1123             else if (firstCharacter === "\t")
1124                 lineIndentLength = 1;
1125             else
1126                 continue;
1127
1128             this._textModel.setText(new WebInspector.TextRange(lineNumber, 0, lineNumber, lineIndentLength), "");
1129
1130             if (lineNumber === range.startLine)
1131                 newRange.startColumn = Math.max(0, newRange.startColumn - lineIndentLength);
1132         }
1133
1134         if (lineIndentLength)
1135             newRange.endColumn = Math.max(0, newRange.endColumn - lineIndentLength);
1136
1137         this._lastEditedRange = newRange;
1138
1139         return newRange;
1140     },
1141
1142     handleEnterKey: function()
1143     {
1144         if (this._dirtyLines)
1145             return false;
1146
1147         var range = this._getSelection();
1148         if (!range)
1149             return false;
1150
1151         range.normalize();
1152
1153         if (range.endColumn === 0)
1154             return false;
1155
1156         var line = this._textModel.line(range.startLine);
1157         var linePrefix = line.substring(0, range.startColumn);
1158         var indentMatch = linePrefix.match(/^\s+/);
1159         var currentIndent = indentMatch ? indentMatch[0] : "";
1160
1161         var textEditorIndent = WebInspector.settings.textEditorIndent.get();
1162         var indent = WebInspector.TextEditorModel.endsWithBracketRegex.test(linePrefix) ? currentIndent + textEditorIndent : currentIndent;
1163
1164         if (!indent)
1165             return false;
1166
1167         this.beginUpdates();
1168         this._enterTextChangeMode();
1169
1170         var lineBreak = this._textModel.lineBreak;
1171         var newRange;
1172         if (range.isEmpty() && line.substr(range.endColumn - 1, 2) === '{}') {
1173             // {|}
1174             // becomes
1175             // {
1176             //     |
1177             // }
1178             newRange = this._setText(range, lineBreak + indent + lineBreak + currentIndent);
1179             newRange.endLine--;
1180             newRange.endColumn += textEditorIndent.length;
1181         } else
1182             newRange = this._setText(range, lineBreak + indent);
1183
1184         newRange = newRange.collapseToEnd();
1185
1186         this._exitTextChangeMode(range, newRange);
1187         this.endUpdates();
1188         this._restoreSelection(newRange, true);
1189
1190         return true;
1191     },
1192
1193     _splitChunkOnALine: function(lineNumber, chunkNumber, createSuffixChunk)
1194     {
1195         var selection = this._getSelection();
1196         var chunk = WebInspector.TextEditorChunkedPanel.prototype._splitChunkOnALine.call(this, lineNumber, chunkNumber, createSuffixChunk);
1197         this._restoreSelection(selection);
1198         return chunk;
1199     },
1200
1201     beginDomUpdates: function()
1202     {
1203         WebInspector.TextEditorChunkedPanel.prototype.beginDomUpdates.call(this);
1204         if (this._domUpdateCoalescingLevel === 1) {
1205             this._container.removeEventListener("DOMCharacterDataModified", this._handleDOMUpdatesCallback, false);
1206             this._container.removeEventListener("DOMNodeInserted", this._handleDOMUpdatesCallback, false);
1207             this._container.removeEventListener("DOMSubtreeModified", this._handleDOMUpdatesCallback, false);
1208         }
1209     },
1210
1211     endDomUpdates: function()
1212     {
1213         WebInspector.TextEditorChunkedPanel.prototype.endDomUpdates.call(this);
1214         if (this._domUpdateCoalescingLevel === 0) {
1215             this._container.addEventListener("DOMCharacterDataModified", this._handleDOMUpdatesCallback, false);
1216             this._container.addEventListener("DOMNodeInserted", this._handleDOMUpdatesCallback, false);
1217             this._container.addEventListener("DOMSubtreeModified", this._handleDOMUpdatesCallback, false);
1218         }
1219     },
1220
1221     _enableDOMNodeRemovedListener: function(lineRow, enable)
1222     {
1223         if (enable)
1224             lineRow.addEventListener("DOMNodeRemoved", this._handleDOMUpdatesCallback, false);
1225         else
1226             lineRow.removeEventListener("DOMNodeRemoved", this._handleDOMUpdatesCallback, false);
1227     },
1228
1229     _buildChunks: function()
1230     {
1231         for (var i = 0; i < this._textModel.linesCount; ++i)
1232             this._textModel.removeAttribute(i, "highlight");
1233
1234         WebInspector.TextEditorChunkedPanel.prototype._buildChunks.call(this);
1235     },
1236
1237     _createNewChunk: function(startLine, endLine)
1238     {
1239         return new WebInspector.TextEditorMainChunk(this, startLine, endLine);
1240     },
1241
1242     _expandChunks: function(fromIndex, toIndex)
1243     {
1244         var lastChunk = this._textChunks[toIndex - 1];
1245         var lastVisibleLine = lastChunk.startLine + lastChunk.linesCount;
1246
1247         var selection = this._getSelection();
1248
1249         this._muteHighlightListener = true;
1250         this._highlighter.highlight(lastVisibleLine);
1251         delete this._muteHighlightListener;
1252
1253         this._restorePaintLinesOperationsCredit();
1254         WebInspector.TextEditorChunkedPanel.prototype._expandChunks.call(this, fromIndex, toIndex);
1255         this._adjustPaintLinesOperationsRefreshValue();
1256
1257         this._restoreSelection(selection);
1258     },
1259
1260     _highlightDataReady: function(fromLine, toLine)
1261     {
1262         if (this._muteHighlightListener)
1263             return;
1264         this._restorePaintLinesOperationsCredit();
1265         this._paintLines(fromLine, toLine, true /*restoreSelection*/);
1266     },
1267
1268     _schedulePaintLines: function(startLine, endLine)
1269     {
1270         if (startLine >= endLine)
1271             return;
1272
1273         if (!this._scheduledPaintLines) {
1274             this._scheduledPaintLines = [ { startLine: startLine, endLine: endLine } ];
1275             this._paintScheduledLinesTimer = setTimeout(this._paintScheduledLines.bind(this), 0);
1276         } else {
1277             for (var i = 0; i < this._scheduledPaintLines.length; ++i) {
1278                 var chunk = this._scheduledPaintLines[i];
1279                 if (chunk.startLine <= endLine && chunk.endLine >= startLine) {
1280                     chunk.startLine = Math.min(chunk.startLine, startLine);
1281                     chunk.endLine = Math.max(chunk.endLine, endLine);
1282                     return;
1283                 }
1284                 if (chunk.startLine > endLine) {
1285                     this._scheduledPaintLines.splice(i, 0, { startLine: startLine, endLine: endLine });
1286                     return;
1287                 }
1288             }
1289             this._scheduledPaintLines.push({ startLine: startLine, endLine: endLine });
1290         }
1291     },
1292
1293     _paintScheduledLines: function(skipRestoreSelection)
1294     {
1295         if (this._paintScheduledLinesTimer)
1296             clearTimeout(this._paintScheduledLinesTimer);
1297         delete this._paintScheduledLinesTimer;
1298
1299         if (!this._scheduledPaintLines)
1300             return;
1301
1302         // Reschedule the timer if we can not paint the lines yet, or the user is scrolling.
1303         if (this._dirtyLines || this._repaintAllTimer) {
1304             this._paintScheduledLinesTimer = setTimeout(this._paintScheduledLines.bind(this), 50);
1305             return;
1306         }
1307
1308         var scheduledPaintLines = this._scheduledPaintLines;
1309         delete this._scheduledPaintLines;
1310
1311         this._restorePaintLinesOperationsCredit();
1312         this._paintLineChunks(scheduledPaintLines, !skipRestoreSelection);
1313         this._adjustPaintLinesOperationsRefreshValue();
1314     },
1315
1316     _restorePaintLinesOperationsCredit: function()
1317     {
1318         if (!this._paintLinesOperationsRefreshValue)
1319             this._paintLinesOperationsRefreshValue = 250;
1320         this._paintLinesOperationsCredit = this._paintLinesOperationsRefreshValue;
1321         this._paintLinesOperationsLastRefresh = Date.now();
1322     },
1323
1324     _adjustPaintLinesOperationsRefreshValue: function()
1325     {
1326         var operationsDone = this._paintLinesOperationsRefreshValue - this._paintLinesOperationsCredit;
1327         if (operationsDone <= 0)
1328             return;
1329         var timePast = Date.now() - this._paintLinesOperationsLastRefresh;
1330         if (timePast <= 0)
1331             return;
1332         // Make the synchronous CPU chunk for painting the lines 50 msec.
1333         var value = Math.floor(operationsDone / timePast * 50);
1334         this._paintLinesOperationsRefreshValue = Number.constrain(value, 150, 1500);
1335     },
1336
1337     /**
1338      * @param {boolean=} restoreSelection
1339      */
1340     _paintLines: function(fromLine, toLine, restoreSelection)
1341     {
1342         this._paintLineChunks([ { startLine: fromLine, endLine: toLine } ], restoreSelection);
1343     },
1344
1345     _paintLineChunks: function(lineChunks, restoreSelection)
1346     {
1347         // First, paint visible lines, so that in case of long lines we should start highlighting
1348         // the visible area immediately, instead of waiting for the lines above the visible area.
1349         var visibleFrom = this.element.scrollTop;
1350         var firstVisibleLineNumber = this._findFirstVisibleLineNumber(visibleFrom);
1351
1352         var chunk;
1353         var selection;
1354         var invisibleLineRows = [];
1355         for (var i = 0; i < lineChunks.length; ++i) {
1356             var lineChunk = lineChunks[i];
1357             if (this._dirtyLines || this._scheduledPaintLines) {
1358                 this._schedulePaintLines(lineChunk.startLine, lineChunk.endLine);
1359                 continue;
1360             }
1361             for (var lineNumber = lineChunk.startLine; lineNumber < lineChunk.endLine; ++lineNumber) {
1362                 if (!chunk || lineNumber < chunk.startLine || lineNumber >= chunk.startLine + chunk.linesCount)
1363                     chunk = this.chunkForLine(lineNumber);
1364                 var lineRow = chunk.getExpandedLineRow(lineNumber);
1365                 if (!lineRow)
1366                     continue;
1367                 if (lineNumber < firstVisibleLineNumber) {
1368                     invisibleLineRows.push(lineRow);
1369                     continue;
1370                 }
1371                 if (restoreSelection && !selection)
1372                     selection = this._getSelection();
1373                 this._paintLine(lineRow);
1374                 if (this._paintLinesOperationsCredit < 0) {
1375                     this._schedulePaintLines(lineNumber + 1, lineChunk.endLine);
1376                     break;
1377                 }
1378             }
1379         }
1380
1381         for (var i = 0; i < invisibleLineRows.length; ++i) {
1382             if (restoreSelection && !selection)
1383                 selection = this._getSelection();
1384             this._paintLine(invisibleLineRows[i]);
1385         }
1386
1387         if (restoreSelection)
1388             this._restoreSelection(selection);
1389     },
1390
1391     _paintLine: function(lineRow)
1392     {
1393         var lineNumber = lineRow.lineNumber;
1394         if (this._dirtyLines) {
1395             this._schedulePaintLines(lineNumber, lineNumber + 1);
1396             return;
1397         }
1398
1399         this.beginDomUpdates();
1400         try {
1401             if (this._scheduledPaintLines || this._paintLinesOperationsCredit < 0) {
1402                 this._schedulePaintLines(lineNumber, lineNumber + 1);
1403                 return;
1404             }
1405
1406             var highlight = this._textModel.getAttribute(lineNumber, "highlight");
1407             if (!highlight)
1408                 return;
1409
1410             lineRow.removeChildren();
1411             var line = this._textModel.line(lineNumber);
1412             if (!line)
1413                 lineRow.appendChild(document.createElement("br"));
1414
1415             var plainTextStart = -1;
1416             for (var j = 0; j < line.length;) {
1417                 if (j > 1000) {
1418                     // This line is too long - do not waste cycles on minified js highlighting.
1419                     if (plainTextStart === -1)
1420                         plainTextStart = j;
1421                     break;
1422                 }
1423                 var attribute = highlight[j];
1424                 if (!attribute || !attribute.tokenType) {
1425                     if (plainTextStart === -1)
1426                         plainTextStart = j;
1427                     j++;
1428                 } else {
1429                     if (plainTextStart !== -1) {
1430                         this._appendTextNode(lineRow, line.substring(plainTextStart, j));
1431                         plainTextStart = -1;
1432                         --this._paintLinesOperationsCredit;
1433                     }
1434                     this._appendSpan(lineRow, line.substring(j, j + attribute.length), attribute.tokenType);
1435                     j += attribute.length;
1436                     --this._paintLinesOperationsCredit;
1437                 }
1438             }
1439             if (plainTextStart !== -1) {
1440                 this._appendTextNode(lineRow, line.substring(plainTextStart, line.length));
1441                 --this._paintLinesOperationsCredit;
1442             }
1443             if (lineRow.decorationsElement)
1444                 lineRow.appendChild(lineRow.decorationsElement);
1445         } finally {
1446             if (this._rangeToMark && this._rangeToMark.startLine === lineNumber)
1447                 this._markedRangeElement = highlightSearchResult(lineRow, this._rangeToMark.startColumn, this._rangeToMark.endColumn - this._rangeToMark.startColumn);
1448             this.endDomUpdates();
1449         }
1450     },
1451
1452     _releaseLinesHighlight: function(lineRow)
1453     {
1454         if (!lineRow)
1455             return;
1456         if ("spans" in lineRow) {
1457             var spans = lineRow.spans;
1458             for (var j = 0; j < spans.length; ++j)
1459                 this._cachedSpans.push(spans[j]);
1460             delete lineRow.spans;
1461         }
1462         if ("textNodes" in lineRow) {
1463             var textNodes = lineRow.textNodes;
1464             for (var j = 0; j < textNodes.length; ++j)
1465                 this._cachedTextNodes.push(textNodes[j]);
1466             delete lineRow.textNodes;
1467         }
1468         this._cachedRows.push(lineRow);
1469     },
1470
1471     _getSelection: function()
1472     {
1473         var selection = window.getSelection();
1474         if (!selection.rangeCount)
1475             return null;
1476         // Selection may be outside of the viewer.
1477         if (!this._container.isAncestor(selection.anchorNode) || !this._container.isAncestor(selection.focusNode))
1478             return null;
1479         var start = this._selectionToPosition(selection.anchorNode, selection.anchorOffset);
1480         var end = selection.isCollapsed ? start : this._selectionToPosition(selection.focusNode, selection.focusOffset);
1481         return new WebInspector.TextRange(start.line, start.column, end.line, end.column);
1482     },
1483
1484     /**
1485      * @param {boolean=} scrollIntoView
1486      */
1487     _restoreSelection: function(range, scrollIntoView)
1488     {
1489         if (!range)
1490             return;
1491         var start = this._positionToSelection(range.startLine, range.startColumn);
1492         var end = range.isEmpty() ? start : this._positionToSelection(range.endLine, range.endColumn);
1493         window.getSelection().setBaseAndExtent(start.container, start.offset, end.container, end.offset);
1494
1495         if (scrollIntoView) {
1496             for (var node = end.container; node; node = node.parentElement) {
1497                 if (node.scrollIntoViewIfNeeded) {
1498                     node.scrollIntoViewIfNeeded();
1499                     break;
1500                 }
1501             }
1502         }
1503     },
1504
1505     _setCaretLocation: function(line, column, scrollIntoView)
1506     {
1507         var range = new WebInspector.TextRange(line, column, line, column);
1508         this._restoreSelection(range, scrollIntoView);
1509     },
1510
1511     _selectionToPosition: function(container, offset)
1512     {
1513         if (container === this._container && offset === 0)
1514             return { line: 0, column: 0 };
1515         if (container === this._container && offset === 1)
1516             return { line: this._textModel.linesCount - 1, column: this._textModel.lineLength(this._textModel.linesCount - 1) };
1517
1518         var lineRow = this._enclosingLineRowOrSelf(container);
1519         var lineNumber = lineRow.lineNumber;
1520         if (container === lineRow && offset === 0)
1521             return { line: lineNumber, column: 0 };
1522
1523         // This may be chunk and chunks may contain \n.
1524         var column = 0;
1525         var node = lineRow.nodeType === Node.TEXT_NODE ? lineRow : lineRow.traverseNextTextNode(lineRow);
1526         while (node && node !== container) {
1527             var text = node.textContent;
1528             for (var i = 0; i < text.length; ++i) {
1529                 if (text.charAt(i) === "\n") {
1530                     lineNumber++;
1531                     column = 0;
1532                 } else
1533                     column++;
1534             }
1535             node = node.traverseNextTextNode(lineRow);
1536         }
1537
1538         if (node === container && offset) {
1539             var text = node.textContent;
1540             for (var i = 0; i < offset; ++i) {
1541                 if (text.charAt(i) === "\n") {
1542                     lineNumber++;
1543                     column = 0;
1544                 } else
1545                     column++;
1546             }
1547         }
1548         return { line: lineNumber, column: column };
1549     },
1550
1551     _positionToSelection: function(line, column)
1552     {
1553         var chunk = this.chunkForLine(line);
1554         // One-lined collapsed chunks may still stay highlighted.
1555         var lineRow = chunk.linesCount === 1 ? chunk.element : chunk.getExpandedLineRow(line);
1556         if (lineRow)
1557             var rangeBoundary = lineRow.rangeBoundaryForOffset(column);
1558         else {
1559             var offset = column;
1560             for (var i = chunk.startLine; i < line; ++i)
1561                 offset += this._textModel.lineLength(i) + 1; // \n
1562             lineRow = chunk.element;
1563             if (lineRow.firstChild)
1564                 var rangeBoundary = { container: lineRow.firstChild, offset: offset };
1565             else
1566                 var rangeBoundary = { container: lineRow, offset: 0 };
1567         }
1568         return rangeBoundary;
1569     },
1570
1571     _enclosingLineRowOrSelf: function(element)
1572     {
1573         var lineRow = element.enclosingNodeOrSelfWithClass("webkit-line-content");
1574         if (lineRow)
1575             return lineRow;
1576
1577         for (lineRow = element; lineRow; lineRow = lineRow.parentElement) {
1578             if (lineRow.parentElement === this._container)
1579                 return lineRow;
1580         }
1581         return null;
1582     },
1583
1584     _appendSpan: function(element, content, className)
1585     {
1586         if (className === "html-resource-link" || className === "html-external-link") {
1587             element.appendChild(this._createLink(content, className === "html-external-link"));
1588             return;
1589         }
1590
1591         var span = this._cachedSpans.pop() || document.createElement("span");
1592         span.className = "webkit-" + className;
1593         span.textContent = content;
1594         element.appendChild(span);
1595         if (!("spans" in element))
1596             element.spans = [];
1597         element.spans.push(span);
1598     },
1599
1600     _appendTextNode: function(element, text)
1601     {
1602         var textNode = this._cachedTextNodes.pop();
1603         if (textNode)
1604             textNode.nodeValue = text;
1605         else
1606             textNode = document.createTextNode(text);
1607         element.appendChild(textNode);
1608         if (!("textNodes" in element))
1609             element.textNodes = [];
1610         element.textNodes.push(textNode);
1611     },
1612
1613     _createLink: function(content, isExternal)
1614     {
1615         var quote = content.charAt(0);
1616         if (content.length > 1 && (quote === "\"" ||   quote === "'"))
1617             content = content.substring(1, content.length - 1);
1618         else
1619             quote = null;
1620
1621         var a = WebInspector.linkifyURLAsNode(this._rewriteHref(content), content, undefined, isExternal);
1622         var span = document.createElement("span");
1623         span.className = "webkit-html-attribute-value";
1624         if (quote)
1625             span.appendChild(document.createTextNode(quote));
1626         span.appendChild(a);
1627         if (quote)
1628             span.appendChild(document.createTextNode(quote));
1629         return span;
1630     },
1631
1632     /**
1633      * @param {boolean=} isExternal
1634      */
1635     _rewriteHref: function(hrefValue, isExternal)
1636     {
1637         if (!this._url || !hrefValue || hrefValue.indexOf("://") > 0)
1638             return hrefValue;
1639         return WebInspector.completeURL(this._url, hrefValue);
1640     },
1641
1642     _handleDOMUpdates: function(e)
1643     {
1644         if (this._domUpdateCoalescingLevel)
1645             return;
1646
1647         var target = e.target;
1648         if (target === this._container)
1649             return;
1650
1651         var lineRow = this._enclosingLineRowOrSelf(target);
1652         if (!lineRow)
1653             return;
1654
1655         if (lineRow.decorationsElement && (lineRow.decorationsElement === target || lineRow.decorationsElement.isAncestor(target))) {
1656             if (this._syncDecorationsForLineListener)
1657                 this._syncDecorationsForLineListener(lineRow.lineNumber);
1658             return;
1659         }
1660
1661         if (this._readOnly)
1662             return;
1663
1664         if (target === lineRow && e.type === "DOMNodeInserted") {
1665             // Ensure that the newly inserted line row has no lineNumber.
1666             delete lineRow.lineNumber;
1667         }
1668
1669         var startLine = 0;
1670         for (var row = lineRow; row; row = row.previousSibling) {
1671             if (typeof row.lineNumber === "number") {
1672                 startLine = row.lineNumber;
1673                 break;
1674             }
1675         }
1676
1677         var endLine = startLine + 1;
1678         for (var row = lineRow.nextSibling; row; row = row.nextSibling) {
1679             if (typeof row.lineNumber === "number" && row.lineNumber > startLine) {
1680                 endLine = row.lineNumber;
1681                 break;
1682             }
1683         }
1684
1685         if (target === lineRow && e.type === "DOMNodeRemoved") {
1686             // Now this will no longer be valid.
1687             delete lineRow.lineNumber;
1688         }
1689
1690         if (this._dirtyLines) {
1691             this._dirtyLines.start = Math.min(this._dirtyLines.start, startLine);
1692             this._dirtyLines.end = Math.max(this._dirtyLines.end, endLine);
1693         } else {
1694             this._dirtyLines = { start: startLine, end: endLine };
1695             setTimeout(this._applyDomUpdates.bind(this), 0);
1696             // Remove marked ranges, if any.
1697             this.markAndRevealRange(null);
1698         }
1699     },
1700
1701     _applyDomUpdates: function()
1702     {
1703         if (!this._dirtyLines)
1704             return;
1705
1706         // Check if the editor had been set readOnly by the moment when this async callback got executed.
1707         if (this._readOnly) {
1708             delete this._dirtyLines;
1709             return;
1710         }
1711
1712         // This is a "foreign" call outside of this class. Should be before we delete the dirty lines flag.
1713         this._enterTextChangeMode();
1714
1715         var dirtyLines = this._dirtyLines;
1716         delete this._dirtyLines;
1717
1718         var firstChunkNumber = this._chunkNumberForLine(dirtyLines.start);
1719         var startLine = this._textChunks[firstChunkNumber].startLine;
1720         var endLine = this._textModel.linesCount;
1721
1722         // Collect lines.
1723         var firstLineRow;
1724         if (firstChunkNumber) {
1725             var chunk = this._textChunks[firstChunkNumber - 1];
1726             firstLineRow = chunk.expanded ? chunk.getExpandedLineRow(chunk.startLine + chunk.linesCount - 1) : chunk.element;
1727             firstLineRow = firstLineRow.nextSibling;
1728         } else
1729             firstLineRow = this._container.firstChild;
1730
1731         var lines = [];
1732         for (var lineRow = firstLineRow; lineRow; lineRow = lineRow.nextSibling) {
1733             if (typeof lineRow.lineNumber === "number" && lineRow.lineNumber >= dirtyLines.end) {
1734                 endLine = lineRow.lineNumber;
1735                 break;
1736             }
1737             // Update with the newest lineNumber, so that the call to the _getSelection method below should work.
1738             lineRow.lineNumber = startLine + lines.length;
1739             this._collectLinesFromDiv(lines, lineRow);
1740         }
1741
1742         // Try to decrease the range being replaced, if possible.
1743         var startOffset = 0;
1744         while (startLine < dirtyLines.start && startOffset < lines.length) {
1745             if (this._textModel.line(startLine) !== lines[startOffset])
1746                 break;
1747             ++startOffset;
1748             ++startLine;
1749         }
1750
1751         var endOffset = lines.length;
1752         while (endLine > dirtyLines.end && endOffset > startOffset) {
1753             if (this._textModel.line(endLine - 1) !== lines[endOffset - 1])
1754                 break;
1755             --endOffset;
1756             --endLine;
1757         }
1758
1759         lines = lines.slice(startOffset, endOffset);
1760
1761         // Try to decrease the range being replaced by column offsets, if possible.
1762         var startColumn = 0;
1763         var endColumn = this._textModel.lineLength(endLine - 1);
1764         if (lines.length > 0) {
1765             var line1 = this._textModel.line(startLine);
1766             var line2 = lines[0];
1767             while (line1[startColumn] && line1[startColumn] === line2[startColumn])
1768                 ++startColumn;
1769             lines[0] = line2.substring(startColumn);
1770
1771             line1 = this._textModel.line(endLine - 1);
1772             line2 = lines[lines.length - 1];
1773             for (var i = 0; i < endColumn && i < line2.length; ++i) {
1774                 if (startLine === endLine - 1 && endColumn - i <= startColumn)
1775                     break;
1776                 if (line1[endColumn - i - 1] !== line2[line2.length - i - 1])
1777                     break;
1778             }
1779             if (i) {
1780                 endColumn -= i;
1781                 lines[lines.length - 1] = line2.substring(0, line2.length - i);
1782             }
1783         }
1784
1785         var selection = this._getSelection();
1786
1787         if (lines.length === 0 && endLine < this._textModel.linesCount)
1788             var oldRange = new WebInspector.TextRange(startLine, 0, endLine, 0);
1789         else if (lines.length === 0 && startLine > 0)
1790             var oldRange = new WebInspector.TextRange(startLine - 1, this._textModel.lineLength(startLine - 1), endLine - 1, this._textModel.lineLength(endLine - 1));
1791         else
1792             var oldRange = new WebInspector.TextRange(startLine, startColumn, endLine - 1, endColumn);
1793
1794         var newRange = this._setText(oldRange, lines.join("\n"));
1795
1796         this._paintScheduledLines(true);
1797         this._restoreSelection(selection);
1798
1799         this._exitTextChangeMode(oldRange, newRange);
1800     },
1801
1802     textChanged: function(oldRange, newRange)
1803     {
1804         this.beginDomUpdates();
1805         this._removeDecorationsInRange(oldRange);
1806         this._updateChunksForRanges(oldRange, newRange);
1807         this._updateHighlightsForRange(newRange);
1808         this.endDomUpdates();
1809     },
1810
1811     _setText: function(range, text)
1812     {
1813         if (this._lastEditedRange && (!text || text.indexOf("\n") !== -1 || this._lastEditedRange.endLine !== range.startLine || this._lastEditedRange.endColumn !== range.startColumn))
1814             this._textModel.markUndoableState();
1815
1816         var newRange = this._textModel.setText(range, text);
1817         this._lastEditedRange = newRange;
1818
1819         return newRange;
1820     },
1821
1822     _removeDecorationsInRange: function(range)
1823     {
1824         for (var i = this._chunkNumberForLine(range.startLine); i < this._textChunks.length; ++i) {
1825             var chunk = this._textChunks[i];
1826             if (chunk.startLine > range.endLine)
1827                 break;
1828             chunk.removeAllDecorations();
1829         }
1830     },
1831
1832     _updateChunksForRanges: function(oldRange, newRange)
1833     {
1834         // Update the chunks in range: firstChunkNumber <= index <= lastChunkNumber
1835         var firstChunkNumber = this._chunkNumberForLine(oldRange.startLine);
1836         var lastChunkNumber = firstChunkNumber;
1837         while (lastChunkNumber + 1 < this._textChunks.length) {
1838             if (this._textChunks[lastChunkNumber + 1].startLine > oldRange.endLine)
1839                 break;
1840             ++lastChunkNumber;
1841         }
1842
1843         var startLine = this._textChunks[firstChunkNumber].startLine;
1844         var linesCount = this._textChunks[lastChunkNumber].startLine + this._textChunks[lastChunkNumber].linesCount - startLine;
1845         var linesDiff = newRange.linesCount - oldRange.linesCount;
1846         linesCount += linesDiff;
1847
1848         if (linesDiff) {
1849             // Lines shifted, update the line numbers of the chunks below.
1850             for (var chunkNumber = lastChunkNumber + 1; chunkNumber < this._textChunks.length; ++chunkNumber)
1851                 this._textChunks[chunkNumber].startLine += linesDiff;
1852         }
1853
1854         var firstLineRow;
1855         if (firstChunkNumber) {
1856             var chunk = this._textChunks[firstChunkNumber - 1];
1857             firstLineRow = chunk.expanded ? chunk.getExpandedLineRow(chunk.startLine + chunk.linesCount - 1) : chunk.element;
1858             firstLineRow = firstLineRow.nextSibling;
1859         } else
1860             firstLineRow = this._container.firstChild;
1861
1862         // Most frequent case: a chunk remained the same.
1863         for (var chunkNumber = firstChunkNumber; chunkNumber <= lastChunkNumber; ++chunkNumber) {
1864             var chunk = this._textChunks[chunkNumber];
1865             if (chunk.startLine + chunk.linesCount > this._textModel.linesCount)
1866                 break;
1867             var lineNumber = chunk.startLine;
1868             for (var lineRow = firstLineRow; lineRow && lineNumber < chunk.startLine + chunk.linesCount; lineRow = lineRow.nextSibling) {
1869                 if (lineRow.lineNumber !== lineNumber || lineRow !== chunk.getExpandedLineRow(lineNumber) || lineRow.textContent !== this._textModel.line(lineNumber) || !lineRow.firstChild)
1870                     break;
1871                 ++lineNumber;
1872             }
1873             if (lineNumber < chunk.startLine + chunk.linesCount)
1874                 break;
1875             chunk.updateCollapsedLineRow();
1876             ++firstChunkNumber;
1877             firstLineRow = lineRow;
1878             startLine += chunk.linesCount;
1879             linesCount -= chunk.linesCount;
1880         }
1881
1882         if (firstChunkNumber > lastChunkNumber && linesCount === 0)
1883             return;
1884
1885         // Maybe merge with the next chunk, so that we should not create 1-sized chunks when appending new lines one by one.
1886         var chunk = this._textChunks[lastChunkNumber + 1];
1887         var linesInLastChunk = linesCount % this._defaultChunkSize;
1888         if (chunk && !chunk.decorated && linesInLastChunk > 0 && linesInLastChunk + chunk.linesCount <= this._defaultChunkSize) {
1889             ++lastChunkNumber;
1890             linesCount += chunk.linesCount;
1891         }
1892
1893         var scrollTop = this.element.scrollTop;
1894         var scrollLeft = this.element.scrollLeft;
1895
1896         // Delete all DOM elements that were either controlled by the old chunks, or have just been inserted.
1897         var firstUnmodifiedLineRow = null;
1898         chunk = this._textChunks[lastChunkNumber + 1];
1899         if (chunk)
1900             firstUnmodifiedLineRow = chunk.expanded ? chunk.getExpandedLineRow(chunk.startLine) : chunk.element;
1901
1902         while (firstLineRow && firstLineRow !== firstUnmodifiedLineRow) {
1903             var lineRow = firstLineRow;
1904             firstLineRow = firstLineRow.nextSibling;
1905             this._container.removeChild(lineRow);
1906         }
1907
1908         // Replace old chunks with the new ones.
1909         for (var chunkNumber = firstChunkNumber; linesCount > 0; ++chunkNumber) {
1910             var chunkLinesCount = Math.min(this._defaultChunkSize, linesCount);
1911             var newChunk = this._createNewChunk(startLine, startLine + chunkLinesCount);
1912             this._container.insertBefore(newChunk.element, firstUnmodifiedLineRow);
1913
1914             if (chunkNumber <= lastChunkNumber)
1915                 this._textChunks[chunkNumber] = newChunk;
1916             else
1917                 this._textChunks.splice(chunkNumber, 0, newChunk);
1918             startLine += chunkLinesCount;
1919             linesCount -= chunkLinesCount;
1920         }
1921         if (chunkNumber <= lastChunkNumber)
1922             this._textChunks.splice(chunkNumber, lastChunkNumber - chunkNumber + 1);
1923
1924         this.element.scrollTop = scrollTop;
1925         this.element.scrollLeft = scrollLeft;
1926     },
1927
1928     _updateHighlightsForRange: function(range)
1929     {
1930         var visibleFrom = this.element.scrollTop;
1931         var visibleTo = this.element.scrollTop + this.element.clientHeight;
1932
1933         var result = this._findVisibleChunks(visibleFrom, visibleTo);
1934         var chunk = this._textChunks[result.end - 1];
1935         var lastVisibleLine = chunk.startLine + chunk.linesCount;
1936
1937         lastVisibleLine = Math.max(lastVisibleLine, range.endLine + 1);
1938         lastVisibleLine = Math.min(lastVisibleLine, this._textModel.linesCount);
1939
1940         var updated = this._highlighter.updateHighlight(range.startLine, lastVisibleLine);
1941         if (!updated) {
1942             // Highlights for the chunks below are invalid, so just collapse them.
1943             for (var i = this._chunkNumberForLine(range.startLine); i < this._textChunks.length; ++i)
1944                 this._textChunks[i].expanded = false;
1945         }
1946
1947         this._repaintAll();
1948     },
1949
1950     _collectLinesFromDiv: function(lines, element)
1951     {
1952         var textContents = [];
1953         var node = element.nodeType === Node.TEXT_NODE ? element : element.traverseNextNode(element);
1954         while (node) {
1955             if (element.decorationsElement === node) {
1956                 node = node.nextSibling;
1957                 continue;
1958             }
1959             if (node.nodeName.toLowerCase() === "br")
1960                 textContents.push("\n");
1961             else if (node.nodeType === Node.TEXT_NODE)
1962                 textContents.push(node.textContent);
1963             node = node.traverseNextNode(element);
1964         }
1965
1966         var textContent = textContents.join("");
1967         // The last \n (if any) does not "count" in a DIV.
1968         textContent = textContent.replace(/\n$/, "");
1969
1970         textContents = textContent.split("\n");
1971         for (var i = 0; i < textContents.length; ++i)
1972             lines.push(textContents[i]);
1973     }
1974 }
1975
1976 WebInspector.TextEditorMainPanel.prototype.__proto__ = WebInspector.TextEditorChunkedPanel.prototype;
1977
1978 /**
1979  * @constructor
1980  */
1981 WebInspector.TextEditorMainChunk = function(textViewer, startLine, endLine)
1982 {
1983     this._textViewer = textViewer;
1984     this._textModel = textViewer._textModel;
1985
1986     this.element = document.createElement("div");
1987     this.element.lineNumber = startLine;
1988     this.element.className = "webkit-line-content";
1989     this._textViewer._enableDOMNodeRemovedListener(this.element, true);
1990
1991     this._startLine = startLine;
1992     endLine = Math.min(this._textModel.linesCount, endLine);
1993     this.linesCount = endLine - startLine;
1994
1995     this._expanded = false;
1996     this._readOnly = false;
1997
1998     this.updateCollapsedLineRow();
1999 }
2000
2001 WebInspector.TextEditorMainChunk.prototype = {
2002     addDecoration: function(decoration)
2003     {
2004         this._textViewer.beginDomUpdates();
2005         if (typeof decoration === "string")
2006             this.element.addStyleClass(decoration);
2007         else {
2008             if (!this.element.decorationsElement) {
2009                 this.element.decorationsElement = document.createElement("div");
2010                 this.element.decorationsElement.className = "webkit-line-decorations";
2011                 this.element.appendChild(this.element.decorationsElement);
2012             }
2013             this.element.decorationsElement.appendChild(decoration);
2014         }
2015         this._textViewer.endDomUpdates();
2016     },
2017
2018     removeDecoration: function(decoration)
2019     {
2020         this._textViewer.beginDomUpdates();
2021         if (typeof decoration === "string")
2022             this.element.removeStyleClass(decoration);
2023         else if (this.element.decorationsElement)
2024             this.element.decorationsElement.removeChild(decoration);
2025         this._textViewer.endDomUpdates();
2026     },
2027
2028     removeAllDecorations: function()
2029     {
2030         this._textViewer.beginDomUpdates();
2031         this.element.className = "webkit-line-content";
2032         if (this.element.decorationsElement) {
2033             this.element.removeChild(this.element.decorationsElement);
2034             delete this.element.decorationsElement;
2035         }
2036         this._textViewer.endDomUpdates();
2037     },
2038
2039     get decorated()
2040     {
2041         return this.element.className !== "webkit-line-content" || !!(this.element.decorationsElement && this.element.decorationsElement.firstChild);
2042     },
2043
2044     get startLine()
2045     {
2046         return this._startLine;
2047     },
2048
2049     set startLine(startLine)
2050     {
2051         this._startLine = startLine;
2052         this.element.lineNumber = startLine;
2053         if (this._expandedLineRows) {
2054             for (var i = 0; i < this._expandedLineRows.length; ++i)
2055                 this._expandedLineRows[i].lineNumber = startLine + i;
2056         }
2057     },
2058
2059     get expanded()
2060     {
2061         return this._expanded;
2062     },
2063
2064     set expanded(expanded)
2065     {
2066         if (this._expanded === expanded)
2067             return;
2068
2069         this._expanded = expanded;
2070
2071         if (this.linesCount === 1) {
2072             if (expanded)
2073                 this._textViewer._paintLine(this.element);
2074             return;
2075         }
2076
2077         this._textViewer.beginDomUpdates();
2078
2079         if (expanded) {
2080             this._expandedLineRows = [];
2081             var parentElement = this.element.parentElement;
2082             for (var i = this.startLine; i < this.startLine + this.linesCount; ++i) {
2083                 var lineRow = this._createRow(i);
2084                 this._textViewer._enableDOMNodeRemovedListener(lineRow, true);
2085                 this._updateElementReadOnlyState(lineRow);
2086                 parentElement.insertBefore(lineRow, this.element);
2087                 this._expandedLineRows.push(lineRow);
2088             }
2089             this._textViewer._enableDOMNodeRemovedListener(this.element, false);
2090             parentElement.removeChild(this.element);
2091             this._textViewer._paintLines(this.startLine, this.startLine + this.linesCount);
2092         } else {
2093             var elementInserted = false;
2094             for (var i = 0; i < this._expandedLineRows.length; ++i) {
2095                 var lineRow = this._expandedLineRows[i];
2096                 this._textViewer._enableDOMNodeRemovedListener(lineRow, false);
2097                 var parentElement = lineRow.parentElement;
2098                 if (parentElement) {
2099                     if (!elementInserted) {
2100                         elementInserted = true;
2101                         this._textViewer._enableDOMNodeRemovedListener(this.element, true);
2102                         parentElement.insertBefore(this.element, lineRow);
2103                     }
2104                     parentElement.removeChild(lineRow);
2105                 }
2106                 this._textViewer._releaseLinesHighlight(lineRow);
2107             }
2108             delete this._expandedLineRows;
2109         }
2110
2111         this._textViewer.endDomUpdates();
2112     },
2113
2114     set readOnly(readOnly)
2115     {
2116         if (this._readOnly === readOnly)
2117             return;
2118
2119         this._readOnly = readOnly;
2120         this._updateElementReadOnlyState(this.element);
2121         if (this._expandedLineRows) {
2122             for (var i = 0; i < this._expandedLineRows.length; ++i)
2123                 this._updateElementReadOnlyState(this._expandedLineRows[i]);
2124         }
2125     },
2126
2127     get readOnly()
2128     {
2129         return this._readOnly;
2130     },
2131
2132     _updateElementReadOnlyState: function(element)
2133     {
2134         if (this._readOnly)
2135             element.addStyleClass("text-editor-read-only");
2136         else
2137             element.removeStyleClass("text-editor-read-only");
2138     },
2139
2140     get height()
2141     {
2142         if (!this._expandedLineRows)
2143             return this._textViewer._totalHeight(this.element);
2144         return this._textViewer._totalHeight(this._expandedLineRows[0], this._expandedLineRows[this._expandedLineRows.length - 1]);
2145     },
2146
2147     get offsetTop()
2148     {
2149         return (this._expandedLineRows && this._expandedLineRows.length) ? this._expandedLineRows[0].offsetTop : this.element.offsetTop;
2150     },
2151
2152     _createRow: function(lineNumber)
2153     {
2154         var lineRow = this._textViewer._cachedRows.pop() || document.createElement("div");
2155         lineRow.lineNumber = lineNumber;
2156         lineRow.className = "webkit-line-content";
2157         lineRow.textContent = this._textModel.line(lineNumber);
2158         if (!lineRow.textContent)
2159             lineRow.appendChild(document.createElement("br"));
2160         return lineRow;
2161     },
2162
2163     getExpandedLineRow: function(lineNumber)
2164     {
2165         if (!this._expanded || lineNumber < this.startLine || lineNumber >= this.startLine + this.linesCount)
2166             return null;
2167         if (!this._expandedLineRows)
2168             return this.element;
2169         return this._expandedLineRows[lineNumber - this.startLine];
2170     },
2171
2172     updateCollapsedLineRow: function()
2173     {
2174         if (this.linesCount === 1 && this._expanded)
2175             return;
2176
2177         var lines = [];
2178         for (var i = this.startLine; i < this.startLine + this.linesCount; ++i)
2179             lines.push(this._textModel.line(i));
2180
2181         this.element.removeChildren();
2182         this.element.textContent = lines.join("\n");
2183
2184         // The last empty line will get swallowed otherwise.
2185         if (!lines[lines.length - 1])
2186             this.element.appendChild(document.createElement("br"));
2187     }
2188 }