2 * Copyright (C) 2011 Google Inc. All rights reserved.
3 * Copyright (C) 2010 Apple Inc. All rights reserved.
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are
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
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.
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.
33 * @extends {WebInspector.View}
36 WebInspector.TextViewer = function(textModel, platform, url, delegate)
38 WebInspector.View.call(this);
39 this.registerRequiredCSS("textViewer.css");
41 this._textModel = textModel;
42 this._textModel.changeListener = this._textChanged.bind(this);
43 this._textModel.resetUndoStack();
44 this._delegate = delegate;
46 this.element.className = "text-editor monospace";
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);
58 // Forward mouse wheel events from the unscrollable gutter to the main panel.
59 function forwardWheelEvent(event)
61 var clone = document.createEvent("WheelEvent");
62 clone.initWebKitWheelEvent(event.wheelDeltaX, event.wheelDeltaY,
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);
69 this._gutterPanel.element.addEventListener("mousewheel", forwardWheelEvent.bind(this), false);
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);
75 this._registerShortcuts();
78 WebInspector.TextViewer.prototype = {
79 set mimeType(mimeType)
81 this._mainPanel.mimeType = mimeType;
84 set readOnly(readOnly)
86 if (this._mainPanel.readOnly === readOnly)
88 this._mainPanel.readOnly = readOnly;
93 return this._mainPanel.readOnly;
98 return this._textModel;
103 this._mainPanel.element.focus();
106 revealLine: function(lineNumber)
108 this._mainPanel.revealLine(lineNumber);
111 addDecoration: function(lineNumber, decoration)
113 this._mainPanel.addDecoration(lineNumber, decoration);
114 this._gutterPanel.addDecoration(lineNumber, decoration);
117 removeDecoration: function(lineNumber, decoration)
119 this._mainPanel.removeDecoration(lineNumber, decoration);
120 this._gutterPanel.removeDecoration(lineNumber, decoration);
123 markAndRevealRange: function(range)
125 this._mainPanel.markAndRevealRange(range);
128 highlightLine: function(lineNumber)
130 if (typeof lineNumber !== "number" || lineNumber < 0)
133 lineNumber = Math.min(lineNumber, this._textModel.linesCount - 1);
134 this._mainPanel.highlightLine(lineNumber);
137 clearLineHighlight: function()
139 this._mainPanel.clearLineHighlight();
142 freeCachedElements: function()
144 this._mainPanel.freeCachedElements();
145 this._gutterPanel.freeCachedElements();
148 elementsToRestoreScrollPositionsFor: function()
150 return [this._mainPanel.element];
153 inheritScrollPositions: function(textViewer)
155 this._mainPanel.element._scrollTop = textViewer._mainPanel.element.scrollTop;
156 this._mainPanel.element._scrollLeft = textViewer._mainPanel.element.scrollLeft;
159 beginUpdates: function()
161 this._mainPanel.beginUpdates();
162 this._gutterPanel.beginUpdates();
165 endUpdates: function()
167 this._mainPanel.endUpdates();
168 this._gutterPanel.endUpdates();
169 this._updatePanelOffsets();
174 this._mainPanel.resize();
175 this._gutterPanel.resize();
176 this._updatePanelOffsets();
179 // WebInspector.TextModel listener
180 _textChanged: function(oldRange, newRange, oldText, newText)
182 if (!this._internalTextChangeMode)
183 this._textModel.resetUndoStack();
184 this._mainPanel.textChanged(oldRange, newRange);
185 this._gutterPanel.textChanged(oldRange, newRange);
186 this._updatePanelOffsets();
189 _enterInternalTextChangeMode: function()
191 this._internalTextChangeMode = true;
192 this._delegate.beforeTextChanged();
195 _exitInternalTextChangeMode: function(oldRange, newRange)
197 this._internalTextChangeMode = false;
198 this._delegate.afterTextChanged(oldRange, newRange);
201 _updatePanelOffsets: function()
203 var lineNumbersWidth = this._gutterPanel.element.offsetWidth;
204 if (lineNumbersWidth)
205 this._mainPanel.element.style.setProperty("left", lineNumbersWidth + "px");
207 this._mainPanel.element.style.removeProperty("left"); // Use default value set in CSS.
210 _syncScroll: function()
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;
219 _syncDecorationsForLine: function(lineNumber)
221 if (lineNumber >= this._textModel.linesCount)
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;
229 gutterChunk.element.style.setProperty("height", height + "px");
231 gutterChunk.element.style.removeProperty("height");
233 var gutterChunk = this._gutterPanel.chunkForLine(lineNumber);
234 if (gutterChunk.linesCount === 1)
235 gutterChunk.element.style.removeProperty("height");
239 _syncLineHeight: function(gutterRow) {
240 if (this._lineHeightSynced)
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;
249 _doubleClick: function(event)
254 var lineRow = event.target.enclosingNodeOrSelfWithClass("webkit-line-content");
256 return; // Do not trigger editing from line numbers.
258 this._delegate.doubleClick(lineRow.lineNumber);
259 window.getSelection().collapseToStart();
262 _registerShortcuts: function()
264 var keys = WebInspector.KeyboardShortcut.Keys;
265 var modifiers = WebInspector.KeyboardShortcut.Modifiers;
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;
274 var handleEnterKey = this._mainPanel.handleEnterKey.bind(this._mainPanel);
275 this._shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Enter.code, WebInspector.KeyboardShortcut.Modifiers.None)] = handleEnterKey;
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;
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;
288 _handleKeyDown: function(e)
293 var shortcutKey = WebInspector.KeyboardShortcut.makeKeyFromEvent(e);
294 var handler = this._shortcuts[shortcutKey];
295 if (handler && handler()) {
301 _contextMenu: function(event)
303 var contextMenu = new WebInspector.ContextMenu();
304 var target = event.target.enclosingNodeOrSelfWithClass("webkit-line-number");
306 this._delegate.populateLineGutterContextMenu(target.lineNumber, contextMenu);
308 this._delegate.populateTextAreaContextMenu(contextMenu);
310 var fileName = this._delegate.suggestedFileName();
312 contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Save as..." : "Save As..."), InspectorFrontendHost.saveAs.bind(InspectorFrontendHost, fileName, this._textModel.text));
314 contextMenu.show(event);
317 _commitEditing: function()
322 this._delegate.commitEditing();
326 _cancelEditing: function()
331 this._delegate.cancelEditing();
336 WebInspector.TextViewer.prototype.__proto__ = WebInspector.View.prototype;
341 WebInspector.TextViewerDelegate = function()
345 WebInspector.TextViewerDelegate.prototype = {
346 doubleClick: function(lineNumber) { },
348 beforeTextChanged: function() { },
350 afterTextChanged: function(oldRange, newRange) { },
352 commitEditing: function() { },
354 cancelEditing: function() { },
356 populateLineGutterContextMenu: function(lineNumber, contextMenu) { },
358 populateTextAreaContextMenu: function(contextMenu) { },
360 suggestedFileName: function() { }
366 WebInspector.TextEditorChunkedPanel = function(textModel)
368 this._textModel = textModel;
370 this._defaultChunkSize = 50;
371 this._paintCoalescingLevel = 0;
372 this._domUpdateCoalescingLevel = 0;
375 WebInspector.TextEditorChunkedPanel.prototype = {
378 return this._textModel;
381 revealLine: function(lineNumber)
383 if (lineNumber >= this._textModel.linesCount)
386 var chunk = this.makeLineAChunk(lineNumber);
387 chunk.element.scrollIntoViewIfNeeded();
390 addDecoration: function(lineNumber, decoration)
392 if (lineNumber >= this._textModel.linesCount)
395 var chunk = this.makeLineAChunk(lineNumber);
396 chunk.addDecoration(decoration);
399 removeDecoration: function(lineNumber, decoration)
401 if (lineNumber >= this._textModel.linesCount)
404 var chunk = this.chunkForLine(lineNumber);
405 chunk.removeDecoration(decoration);
408 _buildChunks: function()
410 this.beginDomUpdates();
412 this._container.removeChildren();
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);
423 this.endDomUpdates();
426 makeLineAChunk: function(lineNumber)
428 var chunkNumber = this._chunkNumberForLine(lineNumber);
429 var oldChunk = this._textChunks[chunkNumber];
432 console.error("No chunk for line number: " + lineNumber);
436 if (oldChunk.linesCount === 1)
439 return this._splitChunkOnALine(lineNumber, chunkNumber, true);
442 _splitChunkOnALine: function(lineNumber, chunkNumber, createSuffixChunk)
444 this.beginDomUpdates();
446 var oldChunk = this._textChunks[chunkNumber];
447 var wasExpanded = oldChunk.expanded;
448 oldChunk.expanded = false;
450 var insertIndex = chunkNumber + 1;
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);
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);
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);
475 // Remove enclosing chunk.
476 this._textChunks.splice(chunkNumber, 1);
477 this._container.removeChild(oldChunk.element);
481 prefixChunk.expanded = true;
482 lineChunk.expanded = true;
484 suffixChunk.expanded = true;
487 this.endDomUpdates();
494 // FIXME: Replace the "2" with the padding-left value from CSS.
495 if (this.element.scrollLeft <= 2)
496 this.element.scrollLeft = 0;
498 this._scheduleRepaintAll();
499 if (this._syncScrollListener)
500 this._syncScrollListener();
503 _scheduleRepaintAll: function()
505 if (this._repaintAllTimer)
506 clearTimeout(this._repaintAllTimer);
507 this._repaintAllTimer = setTimeout(this._repaintAll.bind(this), 50);
510 beginUpdates: function()
512 this._paintCoalescingLevel++;
515 endUpdates: function()
517 this._paintCoalescingLevel--;
518 if (!this._paintCoalescingLevel)
522 beginDomUpdates: function()
524 this._domUpdateCoalescingLevel++;
527 endDomUpdates: function()
529 this._domUpdateCoalescingLevel--;
532 _chunkNumberForLine: function(lineNumber)
534 function compareLineNumbers(value, chunk)
536 return value < chunk.startLine ? -1 : 1;
538 var insertBefore = insertionIndexForObjectInListSortedByFunction(lineNumber, this._textChunks, compareLineNumbers);
539 return insertBefore - 1;
542 chunkForLine: function(lineNumber)
544 return this._textChunks[this._chunkNumberForLine(lineNumber)];
547 _findFirstVisibleChunkNumber: function(visibleFrom)
549 function compareOffsetTops(value, chunk)
551 return value < chunk.offsetTop ? -1 : 1;
553 var insertBefore = insertionIndexForObjectInListSortedByFunction(visibleFrom, this._textChunks, compareOffsetTops);
554 return insertBefore - 1;
557 _findVisibleChunks: function(visibleFrom, visibleTo)
559 var from = this._findFirstVisibleChunkNumber(visibleFrom);
560 for (var to = from + 1; to < this._textChunks.length; ++to) {
561 if (this._textChunks[to].offsetTop >= visibleTo)
564 return { start: from, end: to };
567 _findFirstVisibleLineNumber: function(visibleFrom)
569 var chunk = this._textChunks[this._findFirstVisibleChunkNumber(visibleFrom)];
571 return chunk.startLine;
573 var lineNumbers = [];
574 for (var i = 0; i < chunk.linesCount; ++i) {
575 lineNumbers.push(chunk.startLine + i);
578 function compareLineRowOffsetTops(value, lineNumber)
580 var lineRow = chunk.getExpandedLineRow(lineNumber);
581 return value < lineRow.offsetTop ? -1 : 1;
583 var insertBefore = insertionIndexForObjectInListSortedByFunction(visibleFrom, lineNumbers, compareLineRowOffsetTops);
584 return lineNumbers[insertBefore - 1];
587 _repaintAll: function()
589 delete this._repaintAllTimer;
591 if (this._paintCoalescingLevel || this._dirtyLines)
594 var visibleFrom = this.element.scrollTop;
595 var visibleTo = this.element.scrollTop + this.element.clientHeight;
598 var result = this._findVisibleChunks(visibleFrom, visibleTo);
599 this._expandChunks(result.start, result.end);
603 _expandChunks: function(fromIndex, toIndex)
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;
614 _totalHeight: function(firstElement, lastElement)
616 lastElement = (lastElement || firstElement).nextElementSibling;
618 return lastElement.offsetTop - firstElement.offsetTop;
620 var offsetParent = firstElement.offsetParent;
621 if (offsetParent && offsetParent.scrollHeight > offsetParent.clientHeight)
622 return offsetParent.scrollHeight - firstElement.offsetTop;
625 while (firstElement && firstElement !== lastElement) {
626 total += firstElement.offsetHeight;
627 firstElement = firstElement.nextElementSibling;
640 * @extends {WebInspector.TextEditorChunkedPanel}
642 WebInspector.TextEditorGutterPanel = function(textModel, syncDecorationsForLineListener, syncLineHeightListener)
644 WebInspector.TextEditorChunkedPanel.call(this, textModel);
646 this._syncDecorationsForLineListener = syncDecorationsForLineListener;
647 this._syncLineHeightListener = syncLineHeightListener;
649 this.element = document.createElement("div");
650 this.element.className = "text-editor-lines";
652 this._container = document.createElement("div");
653 this._container.className = "inner-container";
654 this.element.appendChild(this._container);
656 this.element.addEventListener("scroll", this._scroll.bind(this), false);
658 this.freeCachedElements();
662 WebInspector.TextEditorGutterPanel.prototype = {
663 freeCachedElements: function()
665 this._cachedRows = [];
668 _createNewChunk: function(startLine, endLine)
670 return new WebInspector.TextEditorGutterChunk(this, startLine, endLine);
673 textChanged: function(oldRange, newRange)
675 this.beginDomUpdates();
677 var linesDiff = newRange.linesCount - oldRange.linesCount;
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)
684 chunk.expanded = false;
685 this._container.removeChild(chunk.element);
687 this._textChunks.length = chunkNumber + 1;
689 // Add new chunks (if needed).
691 if (this._textChunks.length) {
692 var lastChunk = this._textChunks[this._textChunks.length - 1];
693 totalLines = lastChunk.startLine + lastChunk.linesCount;
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);
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];
712 this.endDomUpdates();
715 syncClientHeight: function(clientHeight)
717 if (this.element.offsetHeight > clientHeight)
718 this._container.style.setProperty("padding-bottom", (this.element.offsetHeight - clientHeight) + "px");
720 this._container.style.removeProperty("padding-bottom");
724 WebInspector.TextEditorGutterPanel.prototype.__proto__ = WebInspector.TextEditorChunkedPanel.prototype;
729 WebInspector.TextEditorGutterChunk = function(textViewer, startLine, endLine)
731 this._textViewer = textViewer;
732 this._textModel = textViewer._textModel;
734 this.startLine = startLine;
735 endLine = Math.min(this._textModel.linesCount, endLine);
736 this.linesCount = endLine - startLine;
738 this._expanded = false;
740 this.element = document.createElement("div");
741 this.element.lineNumber = startLine;
742 this.element.className = "webkit-line-number";
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);
755 var lineNumbers = [];
756 for (var i = startLine; i < endLine; ++i)
757 lineNumbers.push(i + 1);
758 this.element.textContent = lineNumbers.join("\n");
762 WebInspector.TextEditorGutterChunk.prototype = {
763 addDecoration: function(decoration)
765 this._textViewer.beginDomUpdates();
766 if (typeof decoration === "string")
767 this.element.addStyleClass(decoration);
768 this._textViewer.endDomUpdates();
771 removeDecoration: function(decoration)
773 this._textViewer.beginDomUpdates();
774 if (typeof decoration === "string")
775 this.element.removeStyleClass(decoration);
776 this._textViewer.endDomUpdates();
781 return this._expanded;
784 set expanded(expanded)
786 if (this.linesCount === 1)
787 this._textViewer._syncDecorationsForLineListener(this.startLine);
789 if (this._expanded === expanded)
792 this._expanded = expanded;
794 if (this.linesCount === 1)
797 this._textViewer.beginDomUpdates();
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);
807 parentElement.removeChild(this.element);
808 this._textViewer._syncLineHeightListener(this._expandedLineRows[0]);
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;
815 if (!elementInserted) {
816 elementInserted = true;
817 parentElement.insertBefore(this.element, lineRow);
819 parentElement.removeChild(lineRow);
821 this._textViewer._cachedRows.push(lineRow);
823 delete this._expandedLineRows;
826 this._textViewer.endDomUpdates();
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]);
838 return (this._expandedLineRows && this._expandedLineRows.length) ? this._expandedLineRows[0].offsetTop : this.element.offsetTop;
841 _createRow: function(lineNumber)
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;
853 * @extends {WebInspector.TextEditorChunkedPanel}
855 WebInspector.TextEditorMainPanel = function(textModel, url, syncScrollListener, syncDecorationsForLineListener, enterTextChangeMode, exitTextChangeMode)
857 WebInspector.TextEditorChunkedPanel.call(this, textModel);
859 this._syncScrollListener = syncScrollListener;
860 this._syncDecorationsForLineListener = syncDecorationsForLineListener;
861 this._enterTextChangeMode = enterTextChangeMode;
862 this._exitTextChangeMode = exitTextChangeMode;
865 this._highlighter = new WebInspector.TextEditorHighlighter(textModel, this._highlightDataReady.bind(this));
866 this._readOnly = true;
868 this.element = document.createElement("div");
869 this.element.className = "text-editor-contents";
870 this.element.tabIndex = 0;
872 this._container = document.createElement("div");
873 this._container.className = "inner-container";
874 this._container.tabIndex = 0;
875 this.element.appendChild(this._container);
877 this.element.addEventListener("scroll", this._scroll.bind(this), false);
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
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);
892 this.freeCachedElements();
896 WebInspector.TextEditorMainPanel.prototype = {
897 set mimeType(mimeType)
899 this._highlighter.mimeType = mimeType;
902 set readOnly(readOnly)
904 if (this._readOnly === readOnly)
907 this.beginDomUpdates();
908 this._readOnly = readOnly;
910 this._container.removeStyleClass("text-editor-editable");
912 this._container.addStyleClass("text-editor-editable");
913 this._updateSelectionOnStartEditing();
915 this.endDomUpdates();
920 return this._readOnly;
923 _updateSelectionOnStartEditing: function()
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))
937 selection.removeAllRanges();
938 var range = document.createRange();
939 range.setStart(this._container, 0);
940 range.setEnd(this._container, 0);
941 selection.addRange(range);
944 setEditableRange: function(startLine, endLine)
946 this.beginDomUpdates();
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;
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;
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;
972 this.endDomUpdates();
975 clearEditableRange: function()
977 for (var chunkNumber = 0; chunkNumber < this._textChunks.length; ++chunkNumber)
978 this._textChunks[chunkNumber].readOnly = false;
981 markAndRevealRange: function(range)
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();
996 this._paintLines(markedLine, markedLine + 1);
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();
1007 delete this._markedRangeElement;
1010 highlightLine: function(lineNumber)
1012 this.clearLineHighlight();
1013 this._highlightedLine = lineNumber;
1014 this.revealLine(lineNumber);
1015 this.addDecoration(lineNumber, "webkit-highlighted-line");
1018 clearLineHighlight: function()
1020 if (typeof this._highlightedLine === "number") {
1021 this.removeDecoration(this._highlightedLine, "webkit-highlighted-line");
1022 delete this._highlightedLine;
1026 freeCachedElements: function()
1028 this._cachedSpans = [];
1029 this._cachedTextNodes = [];
1030 this._cachedRows = [];
1033 handleUndoRedo: function(redo)
1035 if (this._dirtyLines)
1038 this.beginUpdates();
1039 this._enterTextChangeMode();
1041 var callback = function(oldRange, newRange) {
1042 this._exitTextChangeMode(oldRange, newRange);
1043 this._enterTextChangeMode();
1046 var range = redo ? this._textModel.redo(callback) : this._textModel.undo(callback);
1048 this._setCaretLocation(range.endLine, range.endColumn, true);
1050 this._exitTextChangeMode(null, null);
1056 handleTabKeyPress: function(shiftKey)
1058 if (this._dirtyLines)
1061 var selection = this._getSelection();
1065 var range = selection.normalize();
1067 this.beginUpdates();
1068 this._enterTextChangeMode();
1072 newRange = this._unindentLines(range);
1074 if (range.isEmpty()) {
1075 newRange = this._setText(range, WebInspector.settings.textEditorIndent.get());
1076 newRange.startColumn = newRange.endColumn;
1078 newRange = this._indentLines(range);
1082 this._exitTextChangeMode(range, newRange);
1084 this._restoreSelection(newRange, true);
1088 _indentLines: function(range)
1090 var indent = WebInspector.settings.textEditorIndent.get();
1092 if (this._lastEditedRange)
1093 this._textModel.markUndoableState();
1095 for (var lineNumber = range.startLine; lineNumber <= range.endLine; lineNumber++)
1096 this._textModel.setText(new WebInspector.TextRange(lineNumber, 0, lineNumber, 0), indent);
1098 var newRange = range.clone();
1099 newRange.startColumn += indent.length;
1100 newRange.endColumn += indent.length;
1101 this._lastEditedRange = newRange;
1106 _unindentLines: function(range)
1108 if (this._lastEditedRange)
1109 this._textModel.markUndoableState();
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();
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;
1121 if (firstCharacter === " ")
1122 lineIndentLength = line.match(lineIndentRegex)[0].length;
1123 else if (firstCharacter === "\t")
1124 lineIndentLength = 1;
1128 this._textModel.setText(new WebInspector.TextRange(lineNumber, 0, lineNumber, lineIndentLength), "");
1130 if (lineNumber === range.startLine)
1131 newRange.startColumn = Math.max(0, newRange.startColumn - lineIndentLength);
1134 if (lineIndentLength)
1135 newRange.endColumn = Math.max(0, newRange.endColumn - lineIndentLength);
1137 this._lastEditedRange = newRange;
1142 handleEnterKey: function()
1144 if (this._dirtyLines)
1147 var range = this._getSelection();
1153 if (range.endColumn === 0)
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] : "";
1161 var textEditorIndent = WebInspector.settings.textEditorIndent.get();
1162 var indent = WebInspector.TextEditorModel.endsWithBracketRegex.test(linePrefix) ? currentIndent + textEditorIndent : currentIndent;
1167 this.beginUpdates();
1168 this._enterTextChangeMode();
1170 var lineBreak = this._textModel.lineBreak;
1172 if (range.isEmpty() && line.substr(range.endColumn - 1, 2) === '{}') {
1178 newRange = this._setText(range, lineBreak + indent + lineBreak + currentIndent);
1180 newRange.endColumn += textEditorIndent.length;
1182 newRange = this._setText(range, lineBreak + indent);
1184 newRange = newRange.collapseToEnd();
1186 this._exitTextChangeMode(range, newRange);
1188 this._restoreSelection(newRange, true);
1193 _splitChunkOnALine: function(lineNumber, chunkNumber, createSuffixChunk)
1195 var selection = this._getSelection();
1196 var chunk = WebInspector.TextEditorChunkedPanel.prototype._splitChunkOnALine.call(this, lineNumber, chunkNumber, createSuffixChunk);
1197 this._restoreSelection(selection);
1201 beginDomUpdates: function()
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);
1211 endDomUpdates: function()
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);
1221 _enableDOMNodeRemovedListener: function(lineRow, enable)
1224 lineRow.addEventListener("DOMNodeRemoved", this._handleDOMUpdatesCallback, false);
1226 lineRow.removeEventListener("DOMNodeRemoved", this._handleDOMUpdatesCallback, false);
1229 _buildChunks: function()
1231 for (var i = 0; i < this._textModel.linesCount; ++i)
1232 this._textModel.removeAttribute(i, "highlight");
1234 WebInspector.TextEditorChunkedPanel.prototype._buildChunks.call(this);
1237 _createNewChunk: function(startLine, endLine)
1239 return new WebInspector.TextEditorMainChunk(this, startLine, endLine);
1242 _expandChunks: function(fromIndex, toIndex)
1244 var lastChunk = this._textChunks[toIndex - 1];
1245 var lastVisibleLine = lastChunk.startLine + lastChunk.linesCount;
1247 var selection = this._getSelection();
1249 this._muteHighlightListener = true;
1250 this._highlighter.highlight(lastVisibleLine);
1251 delete this._muteHighlightListener;
1253 this._restorePaintLinesOperationsCredit();
1254 WebInspector.TextEditorChunkedPanel.prototype._expandChunks.call(this, fromIndex, toIndex);
1255 this._adjustPaintLinesOperationsRefreshValue();
1257 this._restoreSelection(selection);
1260 _highlightDataReady: function(fromLine, toLine)
1262 if (this._muteHighlightListener)
1264 this._restorePaintLinesOperationsCredit();
1265 this._paintLines(fromLine, toLine, true /*restoreSelection*/);
1268 _schedulePaintLines: function(startLine, endLine)
1270 if (startLine >= endLine)
1273 if (!this._scheduledPaintLines) {
1274 this._scheduledPaintLines = [ { startLine: startLine, endLine: endLine } ];
1275 this._paintScheduledLinesTimer = setTimeout(this._paintScheduledLines.bind(this), 0);
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);
1284 if (chunk.startLine > endLine) {
1285 this._scheduledPaintLines.splice(i, 0, { startLine: startLine, endLine: endLine });
1289 this._scheduledPaintLines.push({ startLine: startLine, endLine: endLine });
1293 _paintScheduledLines: function(skipRestoreSelection)
1295 if (this._paintScheduledLinesTimer)
1296 clearTimeout(this._paintScheduledLinesTimer);
1297 delete this._paintScheduledLinesTimer;
1299 if (!this._scheduledPaintLines)
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);
1308 var scheduledPaintLines = this._scheduledPaintLines;
1309 delete this._scheduledPaintLines;
1311 this._restorePaintLinesOperationsCredit();
1312 this._paintLineChunks(scheduledPaintLines, !skipRestoreSelection);
1313 this._adjustPaintLinesOperationsRefreshValue();
1316 _restorePaintLinesOperationsCredit: function()
1318 if (!this._paintLinesOperationsRefreshValue)
1319 this._paintLinesOperationsRefreshValue = 250;
1320 this._paintLinesOperationsCredit = this._paintLinesOperationsRefreshValue;
1321 this._paintLinesOperationsLastRefresh = Date.now();
1324 _adjustPaintLinesOperationsRefreshValue: function()
1326 var operationsDone = this._paintLinesOperationsRefreshValue - this._paintLinesOperationsCredit;
1327 if (operationsDone <= 0)
1329 var timePast = Date.now() - this._paintLinesOperationsLastRefresh;
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);
1338 * @param {boolean=} restoreSelection
1340 _paintLines: function(fromLine, toLine, restoreSelection)
1342 this._paintLineChunks([ { startLine: fromLine, endLine: toLine } ], restoreSelection);
1345 _paintLineChunks: function(lineChunks, restoreSelection)
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);
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);
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);
1367 if (lineNumber < firstVisibleLineNumber) {
1368 invisibleLineRows.push(lineRow);
1371 if (restoreSelection && !selection)
1372 selection = this._getSelection();
1373 this._paintLine(lineRow);
1374 if (this._paintLinesOperationsCredit < 0) {
1375 this._schedulePaintLines(lineNumber + 1, lineChunk.endLine);
1381 for (var i = 0; i < invisibleLineRows.length; ++i) {
1382 if (restoreSelection && !selection)
1383 selection = this._getSelection();
1384 this._paintLine(invisibleLineRows[i]);
1387 if (restoreSelection)
1388 this._restoreSelection(selection);
1391 _paintLine: function(lineRow)
1393 var lineNumber = lineRow.lineNumber;
1394 if (this._dirtyLines) {
1395 this._schedulePaintLines(lineNumber, lineNumber + 1);
1399 this.beginDomUpdates();
1401 if (this._scheduledPaintLines || this._paintLinesOperationsCredit < 0) {
1402 this._schedulePaintLines(lineNumber, lineNumber + 1);
1406 var highlight = this._textModel.getAttribute(lineNumber, "highlight");
1410 lineRow.removeChildren();
1411 var line = this._textModel.line(lineNumber);
1413 lineRow.appendChild(document.createElement("br"));
1415 var plainTextStart = -1;
1416 for (var j = 0; j < line.length;) {
1418 // This line is too long - do not waste cycles on minified js highlighting.
1419 if (plainTextStart === -1)
1423 var attribute = highlight[j];
1424 if (!attribute || !attribute.tokenType) {
1425 if (plainTextStart === -1)
1429 if (plainTextStart !== -1) {
1430 this._appendTextNode(lineRow, line.substring(plainTextStart, j));
1431 plainTextStart = -1;
1432 --this._paintLinesOperationsCredit;
1434 this._appendSpan(lineRow, line.substring(j, j + attribute.length), attribute.tokenType);
1435 j += attribute.length;
1436 --this._paintLinesOperationsCredit;
1439 if (plainTextStart !== -1) {
1440 this._appendTextNode(lineRow, line.substring(plainTextStart, line.length));
1441 --this._paintLinesOperationsCredit;
1443 if (lineRow.decorationsElement)
1444 lineRow.appendChild(lineRow.decorationsElement);
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();
1452 _releaseLinesHighlight: function(lineRow)
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;
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;
1468 this._cachedRows.push(lineRow);
1471 _getSelection: function()
1473 var selection = window.getSelection();
1474 if (!selection.rangeCount)
1476 // Selection may be outside of the viewer.
1477 if (!this._container.isAncestor(selection.anchorNode) || !this._container.isAncestor(selection.focusNode))
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);
1485 * @param {boolean=} scrollIntoView
1487 _restoreSelection: function(range, scrollIntoView)
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);
1495 if (scrollIntoView) {
1496 for (var node = end.container; node; node = node.parentElement) {
1497 if (node.scrollIntoViewIfNeeded) {
1498 node.scrollIntoViewIfNeeded();
1505 _setCaretLocation: function(line, column, scrollIntoView)
1507 var range = new WebInspector.TextRange(line, column, line, column);
1508 this._restoreSelection(range, scrollIntoView);
1511 _selectionToPosition: function(container, offset)
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) };
1518 var lineRow = this._enclosingLineRowOrSelf(container);
1519 var lineNumber = lineRow.lineNumber;
1520 if (container === lineRow && offset === 0)
1521 return { line: lineNumber, column: 0 };
1523 // This may be chunk and chunks may contain \n.
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") {
1535 node = node.traverseNextTextNode(lineRow);
1538 if (node === container && offset) {
1539 var text = node.textContent;
1540 for (var i = 0; i < offset; ++i) {
1541 if (text.charAt(i) === "\n") {
1548 return { line: lineNumber, column: column };
1551 _positionToSelection: function(line, column)
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);
1557 var rangeBoundary = lineRow.rangeBoundaryForOffset(column);
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 };
1566 var rangeBoundary = { container: lineRow, offset: 0 };
1568 return rangeBoundary;
1571 _enclosingLineRowOrSelf: function(element)
1573 var lineRow = element.enclosingNodeOrSelfWithClass("webkit-line-content");
1577 for (lineRow = element; lineRow; lineRow = lineRow.parentElement) {
1578 if (lineRow.parentElement === this._container)
1584 _appendSpan: function(element, content, className)
1586 if (className === "html-resource-link" || className === "html-external-link") {
1587 element.appendChild(this._createLink(content, className === "html-external-link"));
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))
1597 element.spans.push(span);
1600 _appendTextNode: function(element, text)
1602 var textNode = this._cachedTextNodes.pop();
1604 textNode.nodeValue = text;
1606 textNode = document.createTextNode(text);
1607 element.appendChild(textNode);
1608 if (!("textNodes" in element))
1609 element.textNodes = [];
1610 element.textNodes.push(textNode);
1613 _createLink: function(content, isExternal)
1615 var quote = content.charAt(0);
1616 if (content.length > 1 && (quote === "\"" || quote === "'"))
1617 content = content.substring(1, content.length - 1);
1621 var a = WebInspector.linkifyURLAsNode(this._rewriteHref(content), content, undefined, isExternal);
1622 var span = document.createElement("span");
1623 span.className = "webkit-html-attribute-value";
1625 span.appendChild(document.createTextNode(quote));
1626 span.appendChild(a);
1628 span.appendChild(document.createTextNode(quote));
1633 * @param {boolean=} isExternal
1635 _rewriteHref: function(hrefValue, isExternal)
1637 if (!this._url || !hrefValue || hrefValue.indexOf("://") > 0)
1639 return WebInspector.completeURL(this._url, hrefValue);
1642 _handleDOMUpdates: function(e)
1644 if (this._domUpdateCoalescingLevel)
1647 var target = e.target;
1648 if (target === this._container)
1651 var lineRow = this._enclosingLineRowOrSelf(target);
1655 if (lineRow.decorationsElement && (lineRow.decorationsElement === target || lineRow.decorationsElement.isAncestor(target))) {
1656 if (this._syncDecorationsForLineListener)
1657 this._syncDecorationsForLineListener(lineRow.lineNumber);
1664 if (target === lineRow && e.type === "DOMNodeInserted") {
1665 // Ensure that the newly inserted line row has no lineNumber.
1666 delete lineRow.lineNumber;
1670 for (var row = lineRow; row; row = row.previousSibling) {
1671 if (typeof row.lineNumber === "number") {
1672 startLine = row.lineNumber;
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;
1685 if (target === lineRow && e.type === "DOMNodeRemoved") {
1686 // Now this will no longer be valid.
1687 delete lineRow.lineNumber;
1690 if (this._dirtyLines) {
1691 this._dirtyLines.start = Math.min(this._dirtyLines.start, startLine);
1692 this._dirtyLines.end = Math.max(this._dirtyLines.end, endLine);
1694 this._dirtyLines = { start: startLine, end: endLine };
1695 setTimeout(this._applyDomUpdates.bind(this), 0);
1696 // Remove marked ranges, if any.
1697 this.markAndRevealRange(null);
1701 _applyDomUpdates: function()
1703 if (!this._dirtyLines)
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;
1712 // This is a "foreign" call outside of this class. Should be before we delete the dirty lines flag.
1713 this._enterTextChangeMode();
1715 var dirtyLines = this._dirtyLines;
1716 delete this._dirtyLines;
1718 var firstChunkNumber = this._chunkNumberForLine(dirtyLines.start);
1719 var startLine = this._textChunks[firstChunkNumber].startLine;
1720 var endLine = this._textModel.linesCount;
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;
1729 firstLineRow = this._container.firstChild;
1732 for (var lineRow = firstLineRow; lineRow; lineRow = lineRow.nextSibling) {
1733 if (typeof lineRow.lineNumber === "number" && lineRow.lineNumber >= dirtyLines.end) {
1734 endLine = lineRow.lineNumber;
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);
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])
1751 var endOffset = lines.length;
1752 while (endLine > dirtyLines.end && endOffset > startOffset) {
1753 if (this._textModel.line(endLine - 1) !== lines[endOffset - 1])
1759 lines = lines.slice(startOffset, endOffset);
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])
1769 lines[0] = line2.substring(startColumn);
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)
1776 if (line1[endColumn - i - 1] !== line2[line2.length - i - 1])
1781 lines[lines.length - 1] = line2.substring(0, line2.length - i);
1785 var selection = this._getSelection();
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));
1792 var oldRange = new WebInspector.TextRange(startLine, startColumn, endLine - 1, endColumn);
1794 var newRange = this._setText(oldRange, lines.join("\n"));
1796 this._paintScheduledLines(true);
1797 this._restoreSelection(selection);
1799 this._exitTextChangeMode(oldRange, newRange);
1802 textChanged: function(oldRange, newRange)
1804 this.beginDomUpdates();
1805 this._removeDecorationsInRange(oldRange);
1806 this._updateChunksForRanges(oldRange, newRange);
1807 this._updateHighlightsForRange(newRange);
1808 this.endDomUpdates();
1811 _setText: function(range, text)
1813 if (this._lastEditedRange && (!text || text.indexOf("\n") !== -1 || this._lastEditedRange.endLine !== range.startLine || this._lastEditedRange.endColumn !== range.startColumn))
1814 this._textModel.markUndoableState();
1816 var newRange = this._textModel.setText(range, text);
1817 this._lastEditedRange = newRange;
1822 _removeDecorationsInRange: function(range)
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)
1828 chunk.removeAllDecorations();
1832 _updateChunksForRanges: function(oldRange, newRange)
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)
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;
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;
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;
1860 firstLineRow = this._container.firstChild;
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)
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)
1873 if (lineNumber < chunk.startLine + chunk.linesCount)
1875 chunk.updateCollapsedLineRow();
1877 firstLineRow = lineRow;
1878 startLine += chunk.linesCount;
1879 linesCount -= chunk.linesCount;
1882 if (firstChunkNumber > lastChunkNumber && linesCount === 0)
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) {
1890 linesCount += chunk.linesCount;
1893 var scrollTop = this.element.scrollTop;
1894 var scrollLeft = this.element.scrollLeft;
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];
1900 firstUnmodifiedLineRow = chunk.expanded ? chunk.getExpandedLineRow(chunk.startLine) : chunk.element;
1902 while (firstLineRow && firstLineRow !== firstUnmodifiedLineRow) {
1903 var lineRow = firstLineRow;
1904 firstLineRow = firstLineRow.nextSibling;
1905 this._container.removeChild(lineRow);
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);
1914 if (chunkNumber <= lastChunkNumber)
1915 this._textChunks[chunkNumber] = newChunk;
1917 this._textChunks.splice(chunkNumber, 0, newChunk);
1918 startLine += chunkLinesCount;
1919 linesCount -= chunkLinesCount;
1921 if (chunkNumber <= lastChunkNumber)
1922 this._textChunks.splice(chunkNumber, lastChunkNumber - chunkNumber + 1);
1924 this.element.scrollTop = scrollTop;
1925 this.element.scrollLeft = scrollLeft;
1928 _updateHighlightsForRange: function(range)
1930 var visibleFrom = this.element.scrollTop;
1931 var visibleTo = this.element.scrollTop + this.element.clientHeight;
1933 var result = this._findVisibleChunks(visibleFrom, visibleTo);
1934 var chunk = this._textChunks[result.end - 1];
1935 var lastVisibleLine = chunk.startLine + chunk.linesCount;
1937 lastVisibleLine = Math.max(lastVisibleLine, range.endLine + 1);
1938 lastVisibleLine = Math.min(lastVisibleLine, this._textModel.linesCount);
1940 var updated = this._highlighter.updateHighlight(range.startLine, lastVisibleLine);
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;
1950 _collectLinesFromDiv: function(lines, element)
1952 var textContents = [];
1953 var node = element.nodeType === Node.TEXT_NODE ? element : element.traverseNextNode(element);
1955 if (element.decorationsElement === node) {
1956 node = node.nextSibling;
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);
1966 var textContent = textContents.join("");
1967 // The last \n (if any) does not "count" in a DIV.
1968 textContent = textContent.replace(/\n$/, "");
1970 textContents = textContent.split("\n");
1971 for (var i = 0; i < textContents.length; ++i)
1972 lines.push(textContents[i]);
1976 WebInspector.TextEditorMainPanel.prototype.__proto__ = WebInspector.TextEditorChunkedPanel.prototype;
1981 WebInspector.TextEditorMainChunk = function(textViewer, startLine, endLine)
1983 this._textViewer = textViewer;
1984 this._textModel = textViewer._textModel;
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);
1991 this._startLine = startLine;
1992 endLine = Math.min(this._textModel.linesCount, endLine);
1993 this.linesCount = endLine - startLine;
1995 this._expanded = false;
1996 this._readOnly = false;
1998 this.updateCollapsedLineRow();
2001 WebInspector.TextEditorMainChunk.prototype = {
2002 addDecoration: function(decoration)
2004 this._textViewer.beginDomUpdates();
2005 if (typeof decoration === "string")
2006 this.element.addStyleClass(decoration);
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);
2013 this.element.decorationsElement.appendChild(decoration);
2015 this._textViewer.endDomUpdates();
2018 removeDecoration: function(decoration)
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();
2028 removeAllDecorations: function()
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;
2036 this._textViewer.endDomUpdates();
2041 return this.element.className !== "webkit-line-content" || !!(this.element.decorationsElement && this.element.decorationsElement.firstChild);
2046 return this._startLine;
2049 set startLine(startLine)
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;
2061 return this._expanded;
2064 set expanded(expanded)
2066 if (this._expanded === expanded)
2069 this._expanded = expanded;
2071 if (this.linesCount === 1) {
2073 this._textViewer._paintLine(this.element);
2077 this._textViewer.beginDomUpdates();
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);
2089 this._textViewer._enableDOMNodeRemovedListener(this.element, false);
2090 parentElement.removeChild(this.element);
2091 this._textViewer._paintLines(this.startLine, this.startLine + this.linesCount);
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);
2104 parentElement.removeChild(lineRow);
2106 this._textViewer._releaseLinesHighlight(lineRow);
2108 delete this._expandedLineRows;
2111 this._textViewer.endDomUpdates();
2114 set readOnly(readOnly)
2116 if (this._readOnly === readOnly)
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]);
2129 return this._readOnly;
2132 _updateElementReadOnlyState: function(element)
2135 element.addStyleClass("text-editor-read-only");
2137 element.removeStyleClass("text-editor-read-only");
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]);
2149 return (this._expandedLineRows && this._expandedLineRows.length) ? this._expandedLineRows[0].offsetTop : this.element.offsetTop;
2152 _createRow: function(lineNumber)
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"));
2163 getExpandedLineRow: function(lineNumber)
2165 if (!this._expanded || lineNumber < this.startLine || lineNumber >= this.startLine + this.linesCount)
2167 if (!this._expandedLineRows)
2168 return this.element;
2169 return this._expandedLineRows[lineNumber - this.startLine];
2172 updateCollapsedLineRow: function()
2174 if (this.linesCount === 1 && this._expanded)
2178 for (var i = this.startLine; i < this.startLine + this.linesCount; ++i)
2179 lines.push(this._textModel.line(i));
2181 this.element.removeChildren();
2182 this.element.textContent = lines.join("\n");
2184 // The last empty line will get swallowed otherwise.
2185 if (!lines[lines.length - 1])
2186 this.element.appendChild(document.createElement("br"));