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