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.
34 * @extends {WebInspector.View}
35 * @implements {WebInspector.TextEditor}
36 * @param {?string} url
37 * @param {WebInspector.TextEditorDelegate} delegate
39 WebInspector.DefaultTextEditor = function(url, delegate)
41 WebInspector.View.call(this);
42 this._delegate = delegate;
45 this.registerRequiredCSS("textEditor.css");
47 this.element.className = "text-editor monospace";
49 // Prevent middle-click pasting in the editor unless it is explicitly enabled for certain component.
50 this.element.addEventListener("mouseup", preventDefaultOnMouseUp.bind(this), false);
51 function preventDefaultOnMouseUp(event)
53 if (event.button === 1)
57 this._textModel = new WebInspector.TextEditorModel();
58 this._textModel.addEventListener(WebInspector.TextEditorModel.Events.TextChanged, this._textChanged, this);
60 var syncScrollListener = this._syncScroll.bind(this);
61 var syncDecorationsForLineListener = this._syncDecorationsForLine.bind(this);
62 var syncLineHeightListener = this._syncLineHeight.bind(this);
63 this._mainPanel = new WebInspector.TextEditorMainPanel(this._delegate, this._textModel, url, syncScrollListener, syncDecorationsForLineListener);
64 this._gutterPanel = new WebInspector.TextEditorGutterPanel(this._textModel, syncDecorationsForLineListener, syncLineHeightListener);
66 this._mainPanel.element.addEventListener("scroll", this._handleScrollChanged.bind(this), false);
67 this._mainPanel._container.addEventListener("focus", this._handleFocused.bind(this), false);
69 this._gutterPanel.element.addEventListener("mousedown", this._onMouseDown.bind(this), true);
71 // Explicitly enable middle-click pasting in the editor main panel.
72 this._mainPanel.element.addEventListener("mouseup", consumeMouseUp.bind(this), false);
73 function consumeMouseUp(event)
75 if (event.button === 1)
79 this.element.appendChild(this._mainPanel.element);
80 this.element.appendChild(this._gutterPanel.element);
82 // Forward mouse wheel events from the unscrollable gutter to the main panel.
83 function forwardWheelEvent(event)
85 var clone = document.createEvent("WheelEvent");
86 clone.initWebKitWheelEvent(event.wheelDeltaX, event.wheelDeltaY,
88 event.screenX, event.screenY,
89 event.clientX, event.clientY,
90 event.ctrlKey, event.altKey, event.shiftKey, event.metaKey);
91 this._mainPanel.element.dispatchEvent(clone);
93 this._gutterPanel.element.addEventListener("mousewheel", forwardWheelEvent.bind(this), false);
95 this.element.addEventListener("keydown", this._handleKeyDown.bind(this), false);
96 this.element.addEventListener("cut", this._handleCut.bind(this), false);
97 this.element.addEventListener("textInput", this._handleTextInput.bind(this), false);
98 this.element.addEventListener("contextmenu", this._contextMenu.bind(this), true);
100 this._registerShortcuts();
105 * @param {WebInspector.TextRange} range
106 * @param {string} text
108 WebInspector.DefaultTextEditor.EditInfo = function(range, text)
114 WebInspector.DefaultTextEditor.prototype = {
116 * @param {string} mimeType
118 set mimeType(mimeType)
120 this._mainPanel.mimeType = mimeType;
124 * @param {boolean} readOnly
126 setReadOnly: function(readOnly)
128 if (this._mainPanel.readOnly() === readOnly)
130 this._mainPanel.setReadOnly(readOnly, this.isShowing());
131 WebInspector.markBeingEdited(this.element, !readOnly);
139 return this._mainPanel.readOnly();
145 defaultFocusedElement: function()
147 return this._mainPanel.defaultFocusedElement();
151 * @param {number} lineNumber
153 revealLine: function(lineNumber)
155 this._mainPanel.revealLine(lineNumber);
158 _onMouseDown: function(event)
160 var target = event.target.enclosingNodeOrSelfWithClass("webkit-line-number");
163 this.dispatchEventToListeners(WebInspector.TextEditor.Events.GutterClick, { lineNumber: target.lineNumber, event: event });
167 * @param {number} lineNumber
168 * @param {boolean} disabled
169 * @param {boolean} conditional
171 addBreakpoint: function(lineNumber, disabled, conditional)
174 this._gutterPanel.addDecoration(lineNumber, "webkit-breakpoint");
176 this._gutterPanel.addDecoration(lineNumber, "webkit-breakpoint-disabled");
178 this._gutterPanel.removeDecoration(lineNumber, "webkit-breakpoint-disabled");
180 this._gutterPanel.addDecoration(lineNumber, "webkit-breakpoint-conditional");
182 this._gutterPanel.removeDecoration(lineNumber, "webkit-breakpoint-conditional");
187 * @param {number} lineNumber
189 removeBreakpoint: function(lineNumber)
192 this._gutterPanel.removeDecoration(lineNumber, "webkit-breakpoint");
193 this._gutterPanel.removeDecoration(lineNumber, "webkit-breakpoint-disabled");
194 this._gutterPanel.removeDecoration(lineNumber, "webkit-breakpoint-conditional");
199 * @param {number} lineNumber
201 setExecutionLine: function(lineNumber)
203 this._executionLineNumber = lineNumber;
204 this._mainPanel.addDecoration(lineNumber, "webkit-execution-line");
205 this._gutterPanel.addDecoration(lineNumber, "webkit-execution-line");
208 clearExecutionLine: function()
210 if (typeof this._executionLineNumber === "number") {
211 this._mainPanel.removeDecoration(this._executionLineNumber, "webkit-execution-line");
212 this._gutterPanel.removeDecoration(this._executionLineNumber, "webkit-execution-line");
214 delete this._executionLineNumber;
218 * @param {number} lineNumber
219 * @param {Element} element
221 addDecoration: function(lineNumber, element)
223 this._mainPanel.addDecoration(lineNumber, element);
224 this._gutterPanel.addDecoration(lineNumber, element);
225 this._syncDecorationsForLine(lineNumber);
229 * @param {number} lineNumber
230 * @param {Element} element
232 removeDecoration: function(lineNumber, element)
234 this._mainPanel.removeDecoration(lineNumber, element);
235 this._gutterPanel.removeDecoration(lineNumber, element);
236 this._syncDecorationsForLine(lineNumber);
240 * @param {WebInspector.TextRange} range
242 markAndRevealRange: function(range)
245 this.setSelection(range);
246 this._mainPanel.markAndRevealRange(range);
250 * @param {number} lineNumber
252 highlightLine: function(lineNumber)
254 if (typeof lineNumber !== "number" || lineNumber < 0)
257 lineNumber = Math.min(lineNumber, this._textModel.linesCount - 1);
258 this._mainPanel.highlightLine(lineNumber);
261 clearLineHighlight: function()
263 this._mainPanel.clearLineHighlight();
266 _freeCachedElements: function()
268 this._mainPanel._freeCachedElements();
269 this._gutterPanel._freeCachedElements();
273 * @return {Array.<Element>}
275 elementsToRestoreScrollPositionsFor: function()
277 return [this._mainPanel.element];
281 * @param {WebInspector.TextEditor} textEditor
283 inheritScrollPositions: function(textEditor)
285 this._mainPanel.element._scrollTop = textEditor._mainPanel.element.scrollTop;
286 this._mainPanel.element._scrollLeft = textEditor._mainPanel.element.scrollLeft;
289 beginUpdates: function()
291 this._mainPanel.beginUpdates();
292 this._gutterPanel.beginUpdates();
295 endUpdates: function()
297 this._mainPanel.endUpdates();
298 this._gutterPanel.endUpdates();
299 this._updatePanelOffsets();
304 this._mainPanel.resize();
305 this._gutterPanel.resize();
306 this._updatePanelOffsets();
309 _textChanged: function(event)
311 this._mainPanel.textChanged(event.data.oldRange, event.data.newRange);
312 this._gutterPanel.textChanged(event.data.oldRange, event.data.newRange);
313 this._updatePanelOffsets();
314 if (event.data.editRange)
315 this._delegate.onTextChanged(event.data.oldRange, event.data.newRange);
319 * @param {WebInspector.TextRange} range
320 * @param {string} text
321 * @return {WebInspector.TextRange}
323 editRange: function(range, text)
325 return this._textModel.editRange(range, text);
328 _updatePanelOffsets: function()
330 var lineNumbersWidth = this._gutterPanel.element.offsetWidth;
331 if (lineNumbersWidth)
332 this._mainPanel.element.style.setProperty("left", (lineNumbersWidth + 2) + "px");
334 this._mainPanel.element.style.removeProperty("left"); // Use default value set in CSS.
337 _syncScroll: function()
339 var mainElement = this._mainPanel.element;
340 var gutterElement = this._gutterPanel.element;
341 // Handle horizontal scroll bar at the bottom of the main panel.
342 this._gutterPanel.syncClientHeight(mainElement.clientHeight);
343 gutterElement.scrollTop = mainElement.scrollTop;
347 * @param {number} lineNumber
349 _syncDecorationsForLine: function(lineNumber)
351 if (lineNumber >= this._textModel.linesCount)
354 var mainChunk = this._mainPanel.chunkForLine(lineNumber);
355 if (mainChunk.linesCount === 1 && mainChunk.isDecorated()) {
356 var gutterChunk = this._gutterPanel.makeLineAChunk(lineNumber);
357 var height = mainChunk.height;
359 gutterChunk.element.style.setProperty("height", height + "px");
361 gutterChunk.element.style.removeProperty("height");
363 var gutterChunk = this._gutterPanel.chunkForLine(lineNumber);
364 if (gutterChunk.linesCount === 1)
365 gutterChunk.element.style.removeProperty("height");
370 * @param {Element} gutterRow
372 _syncLineHeight: function(gutterRow)
374 if (this._lineHeightSynced)
376 if (gutterRow && gutterRow.offsetHeight) {
377 // Force equal line heights for the child panels.
378 this.element.style.setProperty("line-height", gutterRow.offsetHeight + "px");
379 this._lineHeightSynced = true;
383 _registerShortcuts: function()
385 var keys = WebInspector.KeyboardShortcut.Keys;
386 var modifiers = WebInspector.KeyboardShortcut.Modifiers;
388 this._shortcuts = {};
390 var handleEnterKey = this._mainPanel.handleEnterKey.bind(this._mainPanel);
391 this._shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Enter.code, WebInspector.KeyboardShortcut.Modifiers.None)] = handleEnterKey;
393 this._shortcuts[WebInspector.KeyboardShortcut.makeKey("z", modifiers.CtrlOrMeta)] = this._mainPanel.handleUndoRedo.bind(this._mainPanel, false);
394 this._shortcuts[WebInspector.KeyboardShortcut.SelectAll] = this._handleSelectAll.bind(this);
396 var handleRedo = this._mainPanel.handleUndoRedo.bind(this._mainPanel, true);
397 this._shortcuts[WebInspector.KeyboardShortcut.makeKey("z", modifiers.Shift | modifiers.CtrlOrMeta)] = handleRedo;
398 if (!WebInspector.isMac())
399 this._shortcuts[WebInspector.KeyboardShortcut.makeKey("y", modifiers.CtrlOrMeta)] = handleRedo;
401 var handleTabKey = this._mainPanel.handleTabKeyPress.bind(this._mainPanel, false);
402 var handleShiftTabKey = this._mainPanel.handleTabKeyPress.bind(this._mainPanel, true);
403 this._shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Tab.code)] = handleTabKey;
404 this._shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Tab.code, modifiers.Shift)] = handleShiftTabKey;
407 _handleSelectAll: function()
409 this.setSelection(this._textModel.range());
413 _handleTextInput: function(e)
415 this._mainPanel._textInputData = e.data;
418 _handleKeyDown: function(e)
420 // If the event was not triggered from the entire editor, then
421 // ignore it. https://bugs.webkit.org/show_bug.cgi?id=102906
422 if (e.target.enclosingNodeOrSelfWithClass("webkit-line-decorations"))
425 var shortcutKey = WebInspector.KeyboardShortcut.makeKeyFromEvent(e);
427 var handler = this._shortcuts[shortcutKey];
428 if (handler && handler()) {
432 this._mainPanel._keyDownCode = e.keyCode;
435 _handleCut: function(e)
437 this._mainPanel._keyDownCode = WebInspector.KeyboardShortcut.Keys.Delete.code;
440 _contextMenu: function(event)
442 var anchor = event.target.enclosingNodeOrSelfWithNodeName("a");
445 var contextMenu = new WebInspector.ContextMenu(event);
446 var target = event.target.enclosingNodeOrSelfWithClass("webkit-line-number");
448 this._delegate.populateLineGutterContextMenu(contextMenu, target.lineNumber);
450 target = this._mainPanel._enclosingLineRowOrSelf(event.target);
451 this._delegate.populateTextAreaContextMenu(contextMenu, target && target.lineNumber);
456 _handleScrollChanged: function(event)
458 var visibleFrom = this._mainPanel._scrollTop();
459 var firstVisibleLineNumber = this._mainPanel._findFirstVisibleLineNumber(visibleFrom);
460 this._delegate.scrollChanged(firstVisibleLineNumber);
464 * @param {number} lineNumber
466 scrollToLine: function(lineNumber)
468 this._mainPanel.scrollToLine(lineNumber);
472 * @return {WebInspector.TextRange}
474 selection: function(textRange)
476 return this._mainPanel._getSelection();
480 * @return {WebInspector.TextRange?}
482 lastSelection: function()
484 return this._mainPanel._lastSelection;
488 * @param {WebInspector.TextRange} textRange
490 setSelection: function(textRange)
492 this._mainPanel._lastSelection = textRange;
493 if (this.element.isAncestor(document.activeElement))
494 this._mainPanel._restoreSelection(textRange);
498 * @param {string} text
500 setText: function(text)
502 this._textModel.setText(text);
510 return this._textModel.text();
514 * @return {WebInspector.TextRange}
518 return this._textModel.range();
522 * @param {number} lineNumber
525 line: function(lineNumber)
527 return this._textModel.line(lineNumber);
535 return this._textModel.linesCount;
539 * @param {number} line
540 * @param {string} name
541 * @param {Object?} value
543 setAttribute: function(line, name, value)
545 this._textModel.setAttribute(line, name, value);
549 * @param {number} line
550 * @param {string} name
551 * @return {Object|null} value
553 getAttribute: function(line, name)
555 return this._textModel.getAttribute(line, name);
559 * @param {number} line
560 * @param {string} name
562 removeAttribute: function(line, name)
564 this._textModel.removeAttribute(line, name);
569 if (!this.readOnly())
570 WebInspector.markBeingEdited(this.element, true);
572 this._boundSelectionChangeListener = this._mainPanel._handleSelectionChange.bind(this._mainPanel);
573 document.addEventListener("selectionchange", this._boundSelectionChangeListener, false);
574 this._mainPanel._wasShown();
577 _handleFocused: function()
579 if (this._mainPanel._lastSelection)
580 this.setSelection(this._mainPanel._lastSelection);
585 this._mainPanel._willHide();
586 document.removeEventListener("selectionchange", this._boundSelectionChangeListener, false);
587 delete this._boundSelectionChangeListener;
589 if (!this.readOnly())
590 WebInspector.markBeingEdited(this.element, false);
591 this._freeCachedElements();
595 * @param {Element} element
596 * @param {Array.<Object>} resultRanges
597 * @param {string} styleClass
598 * @param {Array.<Object>=} changes
600 highlightRangesWithStyleClass: function(element, resultRanges, styleClass, changes)
602 this._mainPanel.beginDomUpdates();
603 WebInspector.highlightRangesWithStyleClass(element, resultRanges, styleClass, changes);
604 this._mainPanel.endDomUpdates();
608 * @param {Element} element
609 * @param {Object} skipClasses
610 * @param {Object} skipTokens
613 highlightExpression: function(element, skipClasses, skipTokens)
615 // Collect tokens belonging to evaluated expression.
616 var tokens = [ element ];
617 var token = element.previousSibling;
618 while (token && (skipClasses[token.className] || skipTokens[token.textContent.trim()])) {
620 token = token.previousSibling;
624 // Wrap them with highlight element.
625 this._mainPanel.beginDomUpdates();
626 var parentElement = element.parentElement;
627 var nextElement = element.nextSibling;
628 var container = document.createElement("span");
629 for (var i = 0; i < tokens.length; ++i)
630 container.appendChild(tokens[i]);
631 parentElement.insertBefore(container, nextElement);
632 this._mainPanel.endDomUpdates();
637 * @param {Element} highlightElement
639 hideHighlightedExpression: function(highlightElement)
641 this._mainPanel.beginDomUpdates();
642 var parentElement = highlightElement.parentElement;
644 var child = highlightElement.firstChild;
646 var nextSibling = child.nextSibling;
647 parentElement.insertBefore(child, highlightElement);
650 parentElement.removeChild(highlightElement);
652 this._mainPanel.endDomUpdates();
656 * @param {number} scrollTop
657 * @param {number} clientHeight
658 * @param {number} chunkSize
660 overrideViewportForTest: function(scrollTop, clientHeight, chunkSize)
662 this._mainPanel._scrollTopOverrideForTest = scrollTop;
663 this._mainPanel._clientHeightOverrideForTest = clientHeight;
664 this._mainPanel._defaultChunkSize = chunkSize;
667 __proto__: WebInspector.View.prototype
672 * @param {WebInspector.TextEditorModel} textModel
674 WebInspector.TextEditorChunkedPanel = function(textModel)
676 this._textModel = textModel;
678 this._defaultChunkSize = 50;
679 this._paintCoalescingLevel = 0;
680 this._domUpdateCoalescingLevel = 0;
683 WebInspector.TextEditorChunkedPanel.prototype = {
685 * @param {number} lineNumber
687 scrollToLine: function(lineNumber)
689 if (lineNumber >= this._textModel.linesCount)
692 var chunk = this.makeLineAChunk(lineNumber);
693 this.element.scrollTop = chunk.offsetTop;
697 * @param {number} lineNumber
699 revealLine: function(lineNumber)
701 if (lineNumber >= this._textModel.linesCount)
704 var chunk = this.makeLineAChunk(lineNumber);
705 chunk.element.scrollIntoViewIfNeeded();
709 * @param {number} lineNumber
710 * @param {string|Element} decoration
712 addDecoration: function(lineNumber, decoration)
714 if (lineNumber >= this._textModel.linesCount)
717 var chunk = this.makeLineAChunk(lineNumber);
718 chunk.addDecoration(decoration);
722 * @param {number} lineNumber
723 * @param {string|Element} decoration
725 removeDecoration: function(lineNumber, decoration)
727 if (lineNumber >= this._textModel.linesCount)
730 var chunk = this.chunkForLine(lineNumber);
731 chunk.removeDecoration(decoration);
734 _buildChunks: function()
736 this.beginDomUpdates();
738 this._container.removeChildren();
740 this._textChunks = [];
741 for (var i = 0; i < this._textModel.linesCount; i += this._defaultChunkSize) {
742 var chunk = this._createNewChunk(i, i + this._defaultChunkSize);
743 this._textChunks.push(chunk);
744 this._container.appendChild(chunk.element);
749 this.endDomUpdates();
753 * @param {number} lineNumber
755 makeLineAChunk: function(lineNumber)
757 var chunkNumber = this._chunkNumberForLine(lineNumber);
758 var oldChunk = this._textChunks[chunkNumber];
761 console.error("No chunk for line number: " + lineNumber);
765 if (oldChunk.linesCount === 1)
768 return this._splitChunkOnALine(lineNumber, chunkNumber, true);
772 * @param {number} lineNumber
773 * @param {number} chunkNumber
774 * @param {boolean=} createSuffixChunk
776 _splitChunkOnALine: function(lineNumber, chunkNumber, createSuffixChunk)
778 this.beginDomUpdates();
780 var oldChunk = this._textChunks[chunkNumber];
781 var wasExpanded = oldChunk.expanded;
782 oldChunk.expanded = false;
784 var insertIndex = chunkNumber + 1;
787 if (lineNumber > oldChunk.startLine) {
788 var prefixChunk = this._createNewChunk(oldChunk.startLine, lineNumber);
789 this._textChunks.splice(insertIndex++, 0, prefixChunk);
790 this._container.insertBefore(prefixChunk.element, oldChunk.element);
794 var endLine = createSuffixChunk ? lineNumber + 1 : oldChunk.startLine + oldChunk.linesCount;
795 var lineChunk = this._createNewChunk(lineNumber, endLine);
796 this._textChunks.splice(insertIndex++, 0, lineChunk);
797 this._container.insertBefore(lineChunk.element, oldChunk.element);
800 if (oldChunk.startLine + oldChunk.linesCount > endLine) {
801 var suffixChunk = this._createNewChunk(endLine, oldChunk.startLine + oldChunk.linesCount);
802 this._textChunks.splice(insertIndex, 0, suffixChunk);
803 this._container.insertBefore(suffixChunk.element, oldChunk.element);
806 // Remove enclosing chunk.
807 this._textChunks.splice(chunkNumber, 1);
808 this._container.removeChild(oldChunk.element);
812 prefixChunk.expanded = true;
813 lineChunk.expanded = true;
815 suffixChunk.expanded = true;
818 this.endDomUpdates();
825 this._scheduleRepaintAll();
826 if (this._syncScrollListener)
827 this._syncScrollListener();
830 _scheduleRepaintAll: function()
832 if (this._repaintAllTimer)
833 clearTimeout(this._repaintAllTimer);
834 this._repaintAllTimer = setTimeout(this._repaintAll.bind(this), 50);
837 beginUpdates: function()
839 this._paintCoalescingLevel++;
842 endUpdates: function()
844 this._paintCoalescingLevel--;
845 if (!this._paintCoalescingLevel)
849 beginDomUpdates: function()
851 this._domUpdateCoalescingLevel++;
854 endDomUpdates: function()
856 this._domUpdateCoalescingLevel--;
860 * @param {number} lineNumber
862 _chunkNumberForLine: function(lineNumber)
864 function compareLineNumbers(value, chunk)
866 return value < chunk.startLine ? -1 : 1;
868 var insertBefore = insertionIndexForObjectInListSortedByFunction(lineNumber, this._textChunks, compareLineNumbers);
869 return insertBefore - 1;
873 * @param {number} lineNumber
875 chunkForLine: function(lineNumber)
877 return this._textChunks[this._chunkNumberForLine(lineNumber)];
881 * @param {number} visibleFrom
883 _findFirstVisibleChunkNumber: function(visibleFrom)
885 function compareOffsetTops(value, chunk)
887 return value < chunk.offsetTop ? -1 : 1;
889 var insertBefore = insertionIndexForObjectInListSortedByFunction(visibleFrom, this._textChunks, compareOffsetTops);
890 return insertBefore - 1;
894 * @param {number} visibleFrom
895 * @param {number} visibleTo
897 _findVisibleChunks: function(visibleFrom, visibleTo)
899 var from = this._findFirstVisibleChunkNumber(visibleFrom);
900 for (var to = from + 1; to < this._textChunks.length; ++to) {
901 if (this._textChunks[to].offsetTop >= visibleTo)
904 return { start: from, end: to };
908 * @param {number} visibleFrom
910 _findFirstVisibleLineNumber: function(visibleFrom)
912 var chunk = this._textChunks[this._findFirstVisibleChunkNumber(visibleFrom)];
914 return chunk.startLine;
916 var lineNumbers = [];
917 for (var i = 0; i < chunk.linesCount; ++i) {
918 lineNumbers.push(chunk.startLine + i);
921 function compareLineRowOffsetTops(value, lineNumber)
923 var lineRow = chunk.expandedLineRow(lineNumber);
924 return value < lineRow.offsetTop ? -1 : 1;
926 var insertBefore = insertionIndexForObjectInListSortedByFunction(visibleFrom, lineNumbers, compareLineRowOffsetTops);
927 return lineNumbers[insertBefore - 1];
930 _repaintAll: function()
932 delete this._repaintAllTimer;
934 if (this._paintCoalescingLevel)
937 var visibleFrom = this._scrollTop();
938 var visibleTo = visibleFrom + this._clientHeight();
941 var result = this._findVisibleChunks(visibleFrom, visibleTo);
942 this._expandChunks(result.start, result.end);
946 _scrollTop: function()
948 return typeof this._scrollTopOverrideForTest === "number" ? this._scrollTopOverrideForTest : this.element.scrollTop;
951 _clientHeight: function()
953 return typeof this._clientHeightOverrideForTest === "number" ? this._clientHeightOverrideForTest : this.element.clientHeight;
957 * @param {number} fromIndex
958 * @param {number} toIndex
960 _expandChunks: function(fromIndex, toIndex)
962 // First collapse chunks to collect the DOM elements into a cache to reuse them later.
963 for (var i = 0; i < fromIndex; ++i)
964 this._textChunks[i].expanded = false;
965 for (var i = toIndex; i < this._textChunks.length; ++i)
966 this._textChunks[i].expanded = false;
967 for (var i = fromIndex; i < toIndex; ++i)
968 this._textChunks[i].expanded = true;
972 * @param {Element} firstElement
973 * @param {Element=} lastElement
975 _totalHeight: function(firstElement, lastElement)
977 lastElement = (lastElement || firstElement).nextElementSibling;
979 return lastElement.offsetTop - firstElement.offsetTop;
981 var offsetParent = firstElement.offsetParent;
982 if (offsetParent && offsetParent.scrollHeight > offsetParent.clientHeight)
983 return offsetParent.scrollHeight - firstElement.offsetTop;
986 while (firstElement && firstElement !== lastElement) {
987 total += firstElement.offsetHeight;
988 firstElement = firstElement.nextElementSibling;
1001 * @extends {WebInspector.TextEditorChunkedPanel}
1002 * @param {WebInspector.TextEditorModel} textModel
1004 WebInspector.TextEditorGutterPanel = function(textModel, syncDecorationsForLineListener, syncLineHeightListener)
1006 WebInspector.TextEditorChunkedPanel.call(this, textModel);
1008 this._syncDecorationsForLineListener = syncDecorationsForLineListener;
1009 this._syncLineHeightListener = syncLineHeightListener;
1011 this.element = document.createElement("div");
1012 this.element.className = "text-editor-lines";
1014 this._container = document.createElement("div");
1015 this._container.className = "inner-container";
1016 this.element.appendChild(this._container);
1018 this.element.addEventListener("scroll", this._scroll.bind(this), false);
1020 this._freeCachedElements();
1021 this._buildChunks();
1022 this._decorations = {};
1025 WebInspector.TextEditorGutterPanel.prototype = {
1026 _freeCachedElements: function()
1028 this._cachedRows = [];
1032 * @param {number} startLine
1033 * @param {number} endLine
1035 _createNewChunk: function(startLine, endLine)
1037 return new WebInspector.TextEditorGutterChunk(this, startLine, endLine);
1041 * @param {WebInspector.TextRange} oldRange
1042 * @param {WebInspector.TextRange} newRange
1044 textChanged: function(oldRange, newRange)
1046 this.beginDomUpdates();
1048 var linesDiff = newRange.linesCount - oldRange.linesCount;
1050 // Remove old chunks (if needed).
1051 for (var chunkNumber = this._textChunks.length - 1; chunkNumber >= 0 ; --chunkNumber) {
1052 var chunk = this._textChunks[chunkNumber];
1053 if (chunk.startLine + chunk.linesCount <= this._textModel.linesCount)
1055 chunk.expanded = false;
1056 this._container.removeChild(chunk.element);
1058 this._textChunks.length = chunkNumber + 1;
1060 // Add new chunks (if needed).
1062 if (this._textChunks.length) {
1063 var lastChunk = this._textChunks[this._textChunks.length - 1];
1064 totalLines = lastChunk.startLine + lastChunk.linesCount;
1067 for (var i = totalLines; i < this._textModel.linesCount; i += this._defaultChunkSize) {
1068 var chunk = this._createNewChunk(i, i + this._defaultChunkSize);
1069 this._textChunks.push(chunk);
1070 this._container.appendChild(chunk.element);
1073 // Shift decorations if necessary
1074 for (var lineNumber in this._decorations) {
1075 lineNumber = parseInt(lineNumber, 10);
1077 // Do not move decorations before the start position.
1078 if (lineNumber < oldRange.startLine)
1080 // Decorations follow the first character of line.
1081 if (lineNumber === oldRange.startLine && oldRange.startColumn)
1084 var lineDecorationsCopy = this._decorations[lineNumber].slice();
1085 for (var i = 0; i < lineDecorationsCopy.length; ++i) {
1086 var decoration = lineDecorationsCopy[i];
1087 this.removeDecoration(lineNumber, decoration);
1089 // Do not restore the decorations before the end position.
1090 if (lineNumber < oldRange.endLine)
1093 this.addDecoration(lineNumber + linesDiff, decoration);
1099 // Decorations may have been removed, so we may have to sync those lines.
1100 var chunkNumber = this._chunkNumberForLine(newRange.startLine);
1101 var chunk = this._textChunks[chunkNumber];
1102 while (chunk && chunk.startLine <= newRange.endLine) {
1103 if (chunk.linesCount === 1)
1104 this._syncDecorationsForLineListener(chunk.startLine);
1105 chunk = this._textChunks[++chunkNumber];
1109 this.endDomUpdates();
1113 * @param {number} clientHeight
1115 syncClientHeight: function(clientHeight)
1117 if (this.element.offsetHeight > clientHeight)
1118 this._container.style.setProperty("padding-bottom", (this.element.offsetHeight - clientHeight) + "px");
1120 this._container.style.removeProperty("padding-bottom");
1124 * @param {number} lineNumber
1125 * @param {string|Element} decoration
1127 addDecoration: function(lineNumber, decoration)
1129 WebInspector.TextEditorChunkedPanel.prototype.addDecoration.call(this, lineNumber, decoration);
1130 var decorations = this._decorations[lineNumber];
1133 this._decorations[lineNumber] = decorations;
1135 decorations.push(decoration);
1139 * @param {number} lineNumber
1140 * @param {string|Element} decoration
1142 removeDecoration: function(lineNumber, decoration)
1144 WebInspector.TextEditorChunkedPanel.prototype.removeDecoration.call(this, lineNumber, decoration);
1145 var decorations = this._decorations[lineNumber];
1147 decorations.remove(decoration);
1148 if (!decorations.length)
1149 delete this._decorations[lineNumber];
1153 __proto__: WebInspector.TextEditorChunkedPanel.prototype
1159 WebInspector.TextEditorGutterChunk = function(textEditor, startLine, endLine)
1161 this._textEditor = textEditor;
1162 this._textModel = textEditor._textModel;
1164 this.startLine = startLine;
1165 endLine = Math.min(this._textModel.linesCount, endLine);
1166 this.linesCount = endLine - startLine;
1168 this._expanded = false;
1170 this.element = document.createElement("div");
1171 this.element.lineNumber = startLine;
1172 this.element.className = "webkit-line-number";
1174 if (this.linesCount === 1) {
1175 // Single line chunks are typically created for decorations. Host line number in
1176 // the sub-element in order to allow flexible border / margin management.
1177 var innerSpan = document.createElement("span");
1178 innerSpan.className = "webkit-line-number-inner";
1179 innerSpan.textContent = startLine + 1;
1180 var outerSpan = document.createElement("div");
1181 outerSpan.className = "webkit-line-number-outer";
1182 outerSpan.appendChild(innerSpan);
1183 this.element.appendChild(outerSpan);
1185 var lineNumbers = [];
1186 for (var i = startLine; i < endLine; ++i)
1187 lineNumbers.push(i + 1);
1188 this.element.textContent = lineNumbers.join("\n");
1192 WebInspector.TextEditorGutterChunk.prototype = {
1194 * @param {string} decoration
1196 addDecoration: function(decoration)
1198 this._textEditor.beginDomUpdates();
1199 if (typeof decoration === "string")
1200 this.element.addStyleClass(decoration);
1201 this._textEditor.endDomUpdates();
1205 * @param {string} decoration
1207 removeDecoration: function(decoration)
1209 this._textEditor.beginDomUpdates();
1210 if (typeof decoration === "string")
1211 this.element.removeStyleClass(decoration);
1212 this._textEditor.endDomUpdates();
1220 return this._expanded;
1223 set expanded(expanded)
1225 if (this.linesCount === 1)
1226 this._textEditor._syncDecorationsForLineListener(this.startLine);
1228 if (this._expanded === expanded)
1231 this._expanded = expanded;
1233 if (this.linesCount === 1)
1236 this._textEditor.beginDomUpdates();
1239 this._expandedLineRows = [];
1240 var parentElement = this.element.parentElement;
1241 for (var i = this.startLine; i < this.startLine + this.linesCount; ++i) {
1242 var lineRow = this._createRow(i);
1243 parentElement.insertBefore(lineRow, this.element);
1244 this._expandedLineRows.push(lineRow);
1246 parentElement.removeChild(this.element);
1247 this._textEditor._syncLineHeightListener(this._expandedLineRows[0]);
1249 var elementInserted = false;
1250 for (var i = 0; i < this._expandedLineRows.length; ++i) {
1251 var lineRow = this._expandedLineRows[i];
1252 var parentElement = lineRow.parentElement;
1253 if (parentElement) {
1254 if (!elementInserted) {
1255 elementInserted = true;
1256 parentElement.insertBefore(this.element, lineRow);
1258 parentElement.removeChild(lineRow);
1260 this._textEditor._cachedRows.push(lineRow);
1262 delete this._expandedLineRows;
1265 this._textEditor.endDomUpdates();
1273 if (!this._expandedLineRows)
1274 return this._textEditor._totalHeight(this.element);
1275 return this._textEditor._totalHeight(this._expandedLineRows[0], this._expandedLineRows[this._expandedLineRows.length - 1]);
1283 return (this._expandedLineRows && this._expandedLineRows.length) ? this._expandedLineRows[0].offsetTop : this.element.offsetTop;
1287 * @param {number} lineNumber
1290 _createRow: function(lineNumber)
1292 var lineRow = this._textEditor._cachedRows.pop() || document.createElement("div");
1293 lineRow.lineNumber = lineNumber;
1294 lineRow.className = "webkit-line-number";
1295 lineRow.textContent = lineNumber + 1;
1302 * @extends {WebInspector.TextEditorChunkedPanel}
1303 * @param {WebInspector.TextEditorDelegate} delegate
1304 * @param {WebInspector.TextEditorModel} textModel
1305 * @param {?string} url
1307 WebInspector.TextEditorMainPanel = function(delegate, textModel, url, syncScrollListener, syncDecorationsForLineListener)
1309 WebInspector.TextEditorChunkedPanel.call(this, textModel);
1311 this._delegate = delegate;
1312 this._syncScrollListener = syncScrollListener;
1313 this._syncDecorationsForLineListener = syncDecorationsForLineListener;
1316 this._highlighter = new WebInspector.TextEditorHighlighter(textModel, this._highlightDataReady.bind(this));
1317 this._readOnly = true;
1319 this.element = document.createElement("div");
1320 this.element.className = "text-editor-contents";
1321 this.element.tabIndex = 0;
1323 this._container = document.createElement("div");
1324 this._container.className = "inner-container";
1325 this._container.tabIndex = 0;
1326 this.element.appendChild(this._container);
1328 this.element.addEventListener("scroll", this._scroll.bind(this), false);
1329 this.element.addEventListener("focus", this._handleElementFocus.bind(this), false);
1331 this._freeCachedElements();
1332 this._buildChunks();
1335 WebInspector.TextEditorMainPanel.prototype = {
1336 _wasShown: function()
1338 this._isShowing = true;
1339 this._attachMutationObserver();
1342 _willHide: function()
1344 this._detachMutationObserver();
1345 this._isShowing = false;
1348 _attachMutationObserver: function()
1350 if (!this._isShowing)
1353 if (this._mutationObserver)
1354 this._mutationObserver.disconnect();
1355 this._mutationObserver = new NonLeakingMutationObserver(this._handleMutations.bind(this));
1356 this._mutationObserver.observe(this._container, { subtree: true, childList: true, characterData: true });
1359 _detachMutationObserver: function()
1361 if (!this._isShowing)
1364 if (this._mutationObserver) {
1365 this._mutationObserver.disconnect();
1366 delete this._mutationObserver;
1371 * @param {string} mimeType
1373 set mimeType(mimeType)
1375 this._highlighter.mimeType = mimeType;
1379 * @param {boolean} readOnly
1380 * @param {boolean} requestFocus
1382 setReadOnly: function(readOnly, requestFocus)
1384 if (this._readOnly === readOnly)
1387 this.beginDomUpdates();
1388 this._readOnly = readOnly;
1390 this._container.removeStyleClass("text-editor-editable");
1392 this._container.addStyleClass("text-editor-editable");
1394 this._updateSelectionOnStartEditing();
1396 this.endDomUpdates();
1402 readOnly: function()
1404 return this._readOnly;
1407 _handleElementFocus: function()
1409 if (!this._readOnly)
1410 this._container.focus();
1416 defaultFocusedElement: function()
1419 return this.element;
1420 return this._container;
1423 _updateSelectionOnStartEditing: function()
1425 // focus() needs to go first for the case when the last selection was inside the editor and
1426 // the "Edit" button was clicked. In this case we bail at the check below, but the
1427 // editor does not receive the focus, thus "Esc" does not cancel editing until at least
1428 // one change has been made to the editor contents.
1429 this._container.focus();
1430 var selection = window.getSelection();
1431 if (selection.rangeCount) {
1432 var commonAncestorContainer = selection.getRangeAt(0).commonAncestorContainer;
1433 if (this._container.isSelfOrAncestor(commonAncestorContainer))
1437 selection.removeAllRanges();
1438 var range = document.createRange();
1439 range.setStart(this._container, 0);
1440 range.setEnd(this._container, 0);
1441 selection.addRange(range);
1445 * @param {WebInspector.TextRange} range
1447 markAndRevealRange: function(range)
1449 if (this._rangeToMark) {
1450 var markedLine = this._rangeToMark.startLine;
1451 delete this._rangeToMark;
1452 // Remove the marked region immediately.
1453 this.beginDomUpdates();
1454 var chunk = this.chunkForLine(markedLine);
1455 var wasExpanded = chunk.expanded;
1456 chunk.expanded = false;
1457 chunk.updateCollapsedLineRow();
1458 chunk.expanded = wasExpanded;
1459 this.endDomUpdates();
1463 this._rangeToMark = range;
1464 this.revealLine(range.startLine);
1465 var chunk = this.makeLineAChunk(range.startLine);
1466 this._paintLine(chunk.element);
1467 if (this._markedRangeElement)
1468 this._markedRangeElement.scrollIntoViewIfNeeded();
1470 delete this._markedRangeElement;
1474 * @param {number} lineNumber
1476 highlightLine: function(lineNumber)
1478 this.clearLineHighlight();
1479 this._highlightedLine = lineNumber;
1480 this.revealLine(lineNumber);
1482 if (!this._readOnly)
1483 this._restoreSelection(WebInspector.TextRange.createFromLocation(lineNumber, 0), false);
1485 this.addDecoration(lineNumber, "webkit-highlighted-line");
1488 clearLineHighlight: function()
1490 if (typeof this._highlightedLine === "number") {
1491 this.removeDecoration(this._highlightedLine, "webkit-highlighted-line");
1492 delete this._highlightedLine;
1496 _freeCachedElements: function()
1498 this._cachedSpans = [];
1499 this._cachedTextNodes = [];
1500 this._cachedRows = [];
1504 * @param {boolean} redo
1506 handleUndoRedo: function(redo)
1508 if (this.readOnly())
1511 this.beginUpdates();
1513 var range = redo ? this._textModel.redo() : this._textModel.undo();
1517 // Restore location post-repaint.
1519 this._restoreSelection(range, true);
1525 * @param {boolean} shiftKey
1527 handleTabKeyPress: function(shiftKey)
1529 if (this.readOnly())
1532 var selection = this._getSelection();
1536 var range = selection.normalize();
1538 this.beginUpdates();
1541 var rangeWasEmpty = range.isEmpty();
1543 newRange = this._textModel.unindentLines(range);
1546 newRange = this._textModel.editRange(range, WebInspector.settings.textEditorIndent.get());
1548 newRange = this._textModel.indentLines(range);
1553 newRange.startColumn = newRange.endColumn;
1554 this._restoreSelection(newRange, true);
1558 handleEnterKey: function()
1560 if (this.readOnly())
1563 var range = this._getSelection();
1567 range = range.normalize();
1569 if (range.endColumn === 0)
1572 var line = this._textModel.line(range.startLine);
1573 var linePrefix = line.substring(0, range.startColumn);
1574 var indentMatch = linePrefix.match(/^\s+/);
1575 var currentIndent = indentMatch ? indentMatch[0] : "";
1577 var textEditorIndent = WebInspector.settings.textEditorIndent.get();
1578 var indent = WebInspector.TextEditorModel.endsWithBracketRegex.test(linePrefix) ? currentIndent + textEditorIndent : currentIndent;
1583 this.beginDomUpdates();
1585 var lineBreak = this._textModel.lineBreak;
1587 if (range.isEmpty() && line.substr(range.endColumn - 1, 2) === '{}') {
1593 newRange = this._textModel.editRange(range, lineBreak + indent + lineBreak + currentIndent);
1595 newRange.endColumn += textEditorIndent.length;
1597 newRange = this._textModel.editRange(range, lineBreak + indent);
1599 this.endDomUpdates();
1600 this._restoreSelection(newRange.collapseToEnd(), true);
1606 * @param {number} lineNumber
1607 * @param {number} chunkNumber
1608 * @param {boolean=} createSuffixChunk
1610 _splitChunkOnALine: function(lineNumber, chunkNumber, createSuffixChunk)
1612 var selection = this._getSelection();
1613 var chunk = WebInspector.TextEditorChunkedPanel.prototype._splitChunkOnALine.call(this, lineNumber, chunkNumber, createSuffixChunk);
1614 this._restoreSelection(selection);
1618 beginDomUpdates: function()
1620 if (!this._domUpdateCoalescingLevel)
1621 this._detachMutationObserver();
1622 WebInspector.TextEditorChunkedPanel.prototype.beginDomUpdates.call(this);
1625 endDomUpdates: function()
1627 WebInspector.TextEditorChunkedPanel.prototype.endDomUpdates.call(this);
1628 if (!this._domUpdateCoalescingLevel)
1629 this._attachMutationObserver();
1632 _buildChunks: function()
1634 for (var i = 0; i < this._textModel.linesCount; ++i)
1635 this._textModel.removeAttribute(i, "highlight");
1637 WebInspector.TextEditorChunkedPanel.prototype._buildChunks.call(this);
1641 * @param {number} startLine
1642 * @param {number} endLine
1644 _createNewChunk: function(startLine, endLine)
1646 return new WebInspector.TextEditorMainChunk(this, startLine, endLine);
1650 * @param {number} fromIndex
1651 * @param {number} toIndex
1653 _expandChunks: function(fromIndex, toIndex)
1655 var lastChunk = this._textChunks[toIndex - 1];
1656 var lastVisibleLine = lastChunk.startLine + lastChunk.linesCount;
1658 var selection = this._getSelection();
1660 this._muteHighlightListener = true;
1661 this._highlighter.highlight(lastVisibleLine);
1662 delete this._muteHighlightListener;
1664 this._restorePaintLinesOperationsCredit();
1665 WebInspector.TextEditorChunkedPanel.prototype._expandChunks.call(this, fromIndex, toIndex);
1666 this._adjustPaintLinesOperationsRefreshValue();
1668 this._restoreSelection(selection);
1672 * @param {number} fromLine
1673 * @param {number} toLine
1675 _highlightDataReady: function(fromLine, toLine)
1677 if (this._muteHighlightListener)
1679 this._restorePaintLinesOperationsCredit();
1680 this._paintLines(fromLine, toLine, true /*restoreSelection*/);
1684 * @param {number} startLine
1685 * @param {number} endLine
1687 _schedulePaintLines: function(startLine, endLine)
1689 if (startLine >= endLine)
1692 if (!this._scheduledPaintLines) {
1693 this._scheduledPaintLines = [ { startLine: startLine, endLine: endLine } ];
1694 this._paintScheduledLinesTimer = setTimeout(this._paintScheduledLines.bind(this), 0);
1696 for (var i = 0; i < this._scheduledPaintLines.length; ++i) {
1697 var chunk = this._scheduledPaintLines[i];
1698 if (chunk.startLine <= endLine && chunk.endLine >= startLine) {
1699 chunk.startLine = Math.min(chunk.startLine, startLine);
1700 chunk.endLine = Math.max(chunk.endLine, endLine);
1703 if (chunk.startLine > endLine) {
1704 this._scheduledPaintLines.splice(i, 0, { startLine: startLine, endLine: endLine });
1708 this._scheduledPaintLines.push({ startLine: startLine, endLine: endLine });
1713 * @param {boolean} skipRestoreSelection
1715 _paintScheduledLines: function(skipRestoreSelection)
1717 if (this._paintScheduledLinesTimer)
1718 clearTimeout(this._paintScheduledLinesTimer);
1719 delete this._paintScheduledLinesTimer;
1721 if (!this._scheduledPaintLines)
1724 // Reschedule the timer if we can not paint the lines yet, or the user is scrolling.
1725 if (this._repaintAllTimer) {
1726 this._paintScheduledLinesTimer = setTimeout(this._paintScheduledLines.bind(this), 50);
1730 var scheduledPaintLines = this._scheduledPaintLines;
1731 delete this._scheduledPaintLines;
1733 this._restorePaintLinesOperationsCredit();
1734 this._paintLineChunks(scheduledPaintLines, !skipRestoreSelection);
1735 this._adjustPaintLinesOperationsRefreshValue();
1738 _restorePaintLinesOperationsCredit: function()
1740 if (!this._paintLinesOperationsRefreshValue)
1741 this._paintLinesOperationsRefreshValue = 250;
1742 this._paintLinesOperationsCredit = this._paintLinesOperationsRefreshValue;
1743 this._paintLinesOperationsLastRefresh = Date.now();
1746 _adjustPaintLinesOperationsRefreshValue: function()
1748 var operationsDone = this._paintLinesOperationsRefreshValue - this._paintLinesOperationsCredit;
1749 if (operationsDone <= 0)
1751 var timePast = Date.now() - this._paintLinesOperationsLastRefresh;
1754 // Make the synchronous CPU chunk for painting the lines 50 msec.
1755 var value = Math.floor(operationsDone / timePast * 50);
1756 this._paintLinesOperationsRefreshValue = Number.constrain(value, 150, 1500);
1760 * @param {number} fromLine
1761 * @param {number} toLine
1762 * @param {boolean=} restoreSelection
1764 _paintLines: function(fromLine, toLine, restoreSelection)
1766 this._paintLineChunks([ { startLine: fromLine, endLine: toLine } ], restoreSelection);
1770 * @param {boolean=} restoreSelection
1772 _paintLineChunks: function(lineChunks, restoreSelection)
1774 // First, paint visible lines, so that in case of long lines we should start highlighting
1775 // the visible area immediately, instead of waiting for the lines above the visible area.
1776 var visibleFrom = this._scrollTop();
1777 var firstVisibleLineNumber = this._findFirstVisibleLineNumber(visibleFrom);
1781 var invisibleLineRows = [];
1782 for (var i = 0; i < lineChunks.length; ++i) {
1783 var lineChunk = lineChunks[i];
1784 if (this._scheduledPaintLines) {
1785 this._schedulePaintLines(lineChunk.startLine, lineChunk.endLine);
1788 for (var lineNumber = lineChunk.startLine; lineNumber < lineChunk.endLine; ++lineNumber) {
1789 if (!chunk || lineNumber < chunk.startLine || lineNumber >= chunk.startLine + chunk.linesCount)
1790 chunk = this.chunkForLine(lineNumber);
1791 var lineRow = chunk.expandedLineRow(lineNumber);
1794 if (lineNumber < firstVisibleLineNumber) {
1795 invisibleLineRows.push(lineRow);
1798 if (restoreSelection && !selection)
1799 selection = this._getSelection();
1800 this._paintLine(lineRow);
1801 if (this._paintLinesOperationsCredit < 0) {
1802 this._schedulePaintLines(lineNumber + 1, lineChunk.endLine);
1808 for (var i = 0; i < invisibleLineRows.length; ++i) {
1809 if (restoreSelection && !selection)
1810 selection = this._getSelection();
1811 this._paintLine(invisibleLineRows[i]);
1814 if (restoreSelection)
1815 this._restoreSelection(selection);
1819 * @param {Element} lineRow
1821 _paintLine: function(lineRow)
1823 var lineNumber = lineRow.lineNumber;
1825 this.beginDomUpdates();
1827 if (this._scheduledPaintLines || this._paintLinesOperationsCredit < 0) {
1828 this._schedulePaintLines(lineNumber, lineNumber + 1);
1832 var highlight = this._textModel.getAttribute(lineNumber, "highlight");
1836 var decorationsElement = lineRow.decorationsElement;
1837 if (!decorationsElement)
1838 lineRow.removeChildren();
1841 var child = lineRow.firstChild;
1842 if (!child || child === decorationsElement)
1844 lineRow.removeChild(child);
1848 var line = this._textModel.line(lineNumber);
1850 lineRow.insertBefore(document.createElement("br"), decorationsElement);
1852 var plainTextStart = -1;
1853 for (var j = 0; j < line.length;) {
1855 // This line is too long - do not waste cycles on minified js highlighting.
1856 if (plainTextStart === -1)
1860 var attribute = highlight[j];
1861 if (!attribute || !attribute.tokenType) {
1862 if (plainTextStart === -1)
1866 if (plainTextStart !== -1) {
1867 this._insertTextNodeBefore(lineRow, decorationsElement, line.substring(plainTextStart, j));
1868 plainTextStart = -1;
1869 --this._paintLinesOperationsCredit;
1871 this._insertSpanBefore(lineRow, decorationsElement, line.substring(j, j + attribute.length), attribute.tokenType);
1872 j += attribute.length;
1873 --this._paintLinesOperationsCredit;
1876 if (plainTextStart !== -1) {
1877 this._insertTextNodeBefore(lineRow, decorationsElement, line.substring(plainTextStart, line.length));
1878 --this._paintLinesOperationsCredit;
1881 if (this._rangeToMark && this._rangeToMark.startLine === lineNumber)
1882 this._markedRangeElement = WebInspector.highlightSearchResult(lineRow, this._rangeToMark.startColumn, this._rangeToMark.endColumn - this._rangeToMark.startColumn);
1883 this.endDomUpdates();
1888 * @param {Element} lineRow
1890 _releaseLinesHighlight: function(lineRow)
1894 if ("spans" in lineRow) {
1895 var spans = lineRow.spans;
1896 for (var j = 0; j < spans.length; ++j)
1897 this._cachedSpans.push(spans[j]);
1898 delete lineRow.spans;
1900 if ("textNodes" in lineRow) {
1901 var textNodes = lineRow.textNodes;
1902 for (var j = 0; j < textNodes.length; ++j)
1903 this._cachedTextNodes.push(textNodes[j]);
1904 delete lineRow.textNodes;
1906 this._cachedRows.push(lineRow);
1910 * @param {?Node=} lastUndamagedLineRow
1911 * @return {WebInspector.TextRange}
1913 _getSelection: function(lastUndamagedLineRow)
1915 var selection = window.getSelection();
1916 if (!selection.rangeCount)
1918 // Selection may be outside of the editor.
1919 if (!this._container.isAncestor(selection.anchorNode) || !this._container.isAncestor(selection.focusNode))
1921 var start = this._selectionToPosition(selection.anchorNode, selection.anchorOffset, lastUndamagedLineRow);
1922 var end = selection.isCollapsed ? start : this._selectionToPosition(selection.focusNode, selection.focusOffset, lastUndamagedLineRow);
1923 return new WebInspector.TextRange(start.line, start.column, end.line, end.column);
1927 * @param {boolean=} scrollIntoView
1929 _restoreSelection: function(range, scrollIntoView)
1934 var start = this._positionToSelection(range.startLine, range.startColumn);
1935 var end = range.isEmpty() ? start : this._positionToSelection(range.endLine, range.endColumn);
1936 window.getSelection().setBaseAndExtent(start.container, start.offset, end.container, end.offset);
1938 if (scrollIntoView) {
1939 for (var node = end.container; node; node = node.parentElement) {
1940 if (node.scrollIntoViewIfNeeded) {
1941 node.scrollIntoViewIfNeeded();
1946 this._lastSelection = range;
1950 * @param {Node} container
1951 * @param {number} offset
1952 * @param {?Node=} lastUndamagedLineRow
1954 _selectionToPosition: function(container, offset, lastUndamagedLineRow)
1956 if (container === this._container && offset === 0)
1957 return { line: 0, column: 0 };
1958 if (container === this._container && offset === 1)
1959 return { line: this._textModel.linesCount - 1, column: this._textModel.lineLength(this._textModel.linesCount - 1) };
1961 // This method can be called on the damaged DOM (when DOM does not match model).
1962 // We need to start counting lines from the first undamaged line if it is given.
1967 if (lastUndamagedLineRow === null) {
1968 // Last undamaged row is given, but is null - force traverse from the beginning
1969 node = this._container.firstChild;
1970 scopeNode = this._container;
1973 var lineRow = this._enclosingLineRowOrSelf(container);
1974 if (!lastUndamagedLineRow || (typeof lineRow.lineNumber === "number" && lineRow.lineNumber <= lastUndamagedLineRow.lineNumber)) {
1975 // DOM is consistent (or we belong to the first damaged row)- lookup the row we belong to and start with it.
1978 lineNumber = node.lineNumber;
1980 // Start with the node following undamaged row. It corresponds to lineNumber + 1.
1981 node = lastUndamagedLineRow.nextSibling;
1982 scopeNode = this._container;
1983 lineNumber = lastUndamagedLineRow.lineNumber + 1;
1987 // Fast return the line start.
1988 if (container === node && offset === 0)
1989 return { line: lineNumber, column: 0 };
1991 // Traverse text and increment lineNumber / column.
1992 for (; node && node !== container; node = node.traverseNextNode(scopeNode)) {
1993 if (node.nodeName.toLowerCase() === "br") {
1996 } else if (node.nodeType === Node.TEXT_NODE) {
1997 var text = node.textContent;
1998 for (var i = 0; i < text.length; ++i) {
1999 if (text.charAt(i) === "\n") {
2008 // We reached our container node, traverse within itself until we reach given offset.
2009 if (node === container && offset) {
2010 var text = node.textContent;
2011 // In case offset == 1 and lineRow is a chunk div, we need to traverse it all.
2012 var textOffset = (node._chunk && offset === 1) ? text.length : offset;
2013 for (var i = 0; i < textOffset; ++i) {
2014 if (text.charAt(i) === "\n") {
2021 return { line: lineNumber, column: column };
2025 * @param {number} line
2026 * @param {number} column
2028 _positionToSelection: function(line, column)
2030 var chunk = this.chunkForLine(line);
2031 // One-lined collapsed chunks may still stay highlighted.
2032 var lineRow = chunk.linesCount === 1 ? chunk.element : chunk.expandedLineRow(line);
2034 var rangeBoundary = lineRow.rangeBoundaryForOffset(column);
2036 var offset = column;
2037 for (var i = chunk.startLine; i < line && i < this._textModel.linesCount; ++i)
2038 offset += this._textModel.lineLength(i) + 1; // \n
2039 lineRow = chunk.element;
2040 if (lineRow.firstChild)
2041 var rangeBoundary = { container: lineRow.firstChild, offset: offset };
2043 var rangeBoundary = { container: lineRow, offset: 0 };
2045 return rangeBoundary;
2049 * @param {Node} element
2051 _enclosingLineRowOrSelf: function(element)
2053 var lineRow = element.enclosingNodeOrSelfWithClass("webkit-line-content");
2057 for (lineRow = element; lineRow; lineRow = lineRow.parentElement) {
2058 if (lineRow.parentElement === this._container)
2065 * @param {Element} element
2066 * @param {Element} oldChild
2067 * @param {string} content
2068 * @param {string} className
2070 _insertSpanBefore: function(element, oldChild, content, className)
2072 if (className === "html-resource-link" || className === "html-external-link") {
2073 element.insertBefore(this._createLink(content, className === "html-external-link"), oldChild);
2077 var span = this._cachedSpans.pop() || document.createElement("span");
2078 span.className = "webkit-" + className;
2079 if (WebInspector.FALSE) // For paint debugging.
2080 span.addStyleClass("debug-fadeout");
2081 span.textContent = content;
2082 element.insertBefore(span, oldChild);
2083 if (!("spans" in element))
2085 element.spans.push(span);
2089 * @param {Element} element
2090 * @param {Element} oldChild
2091 * @param {string} text
2093 _insertTextNodeBefore: function(element, oldChild, text)
2095 var textNode = this._cachedTextNodes.pop();
2097 textNode.nodeValue = text;
2099 textNode = document.createTextNode(text);
2100 element.insertBefore(textNode, oldChild);
2101 if (!("textNodes" in element))
2102 element.textNodes = [];
2103 element.textNodes.push(textNode);
2107 * @param {string} content
2108 * @param {boolean} isExternal
2110 _createLink: function(content, isExternal)
2112 var quote = content.charAt(0);
2113 if (content.length > 1 && (quote === "\"" || quote === "'"))
2114 content = content.substring(1, content.length - 1);
2118 var span = document.createElement("span");
2119 span.className = "webkit-html-attribute-value";
2121 span.appendChild(document.createTextNode(quote));
2122 span.appendChild(this._delegate.createLink(content, isExternal));
2124 span.appendChild(document.createTextNode(quote));
2129 * @param {Array.<WebKitMutation>} mutations
2131 _handleMutations: function(mutations)
2133 if (this._readOnly) {
2134 delete this._keyDownCode;
2138 // Annihilate noop BR addition + removal that takes place upon line removal.
2139 var filteredMutations = mutations.slice();
2140 var addedBRs = new Map();
2141 for (var i = 0; i < mutations.length; ++i) {
2142 var mutation = mutations[i];
2143 if (mutation.type !== "childList")
2145 if (mutation.addedNodes.length === 1 && mutation.addedNodes[0].nodeName === "BR")
2146 addedBRs.put(mutation.addedNodes[0], mutation);
2147 else if (mutation.removedNodes.length === 1 && mutation.removedNodes[0].nodeName === "BR") {
2148 var noopMutation = addedBRs.get(mutation.removedNodes[0]);
2150 filteredMutations.remove(mutation);
2151 filteredMutations.remove(noopMutation);
2157 for (var i = 0; i < filteredMutations.length; ++i) {
2158 var mutation = filteredMutations[i];
2159 var changedNodes = [];
2160 if (mutation.type === "childList" && mutation.addedNodes.length)
2161 changedNodes = Array.prototype.slice.call(mutation.addedNodes);
2162 else if (mutation.type === "childList" && mutation.removedNodes.length)
2163 changedNodes = Array.prototype.slice.call(mutation.removedNodes);
2164 changedNodes.push(mutation.target);
2166 for (var j = 0; j < changedNodes.length; ++j) {
2167 var lines = this._collectDirtyLines(mutation, changedNodes[j]);
2174 dirtyLines.start = Math.min(dirtyLines.start, lines.start);
2175 dirtyLines.end = Math.max(dirtyLines.end, lines.end);
2179 delete this._rangeToMark;
2180 this._applyDomUpdates(dirtyLines);
2183 this._assertDOMMatchesTextModel();
2185 delete this._keyDownCode;
2189 * @param {WebKitMutation} mutation
2190 * @param {Node} target
2193 _collectDirtyLines: function(mutation, target)
2195 var lineRow = this._enclosingLineRowOrSelf(target);
2199 if (lineRow.decorationsElement && lineRow.decorationsElement.isSelfOrAncestor(target)) {
2200 if (this._syncDecorationsForLineListener)
2201 this._syncDecorationsForLineListener(lineRow.lineNumber);
2205 if (typeof lineRow.lineNumber !== "number")
2208 var startLine = lineRow.lineNumber;
2209 var endLine = lineRow._chunk ? lineRow._chunk.endLine - 1 : lineRow.lineNumber;
2210 return { start: startLine, end: endLine };
2214 * @param {Object} dirtyLines
2216 _applyDomUpdates: function(dirtyLines)
2218 var lastUndamagedLineNumber = dirtyLines.start - 1; // Can be -1
2219 var firstUndamagedLineNumber = dirtyLines.end + 1; // Can be this._textModel.linesCount
2221 var lastUndamagedLineChunk = lastUndamagedLineNumber >= 0 ? this._textChunks[this._chunkNumberForLine(lastUndamagedLineNumber)] : null;
2222 var firstUndamagedLineChunk = firstUndamagedLineNumber < this._textModel.linesCount ? this._textChunks[this._chunkNumberForLine(firstUndamagedLineNumber)] : null;
2224 var collectLinesFromNode = lastUndamagedLineChunk ? lastUndamagedLineChunk.lineRowContainingLine(lastUndamagedLineNumber) : null;
2225 var collectLinesToNode = firstUndamagedLineChunk ? firstUndamagedLineChunk.lineRowContainingLine(firstUndamagedLineNumber) : null;
2226 var lines = this._collectLinesFromDOM(collectLinesFromNode, collectLinesToNode);
2228 var startLine = dirtyLines.start;
2229 var endLine = dirtyLines.end;
2231 var editInfo = this._guessEditRangeBasedOnSelection(startLine, endLine, lines);
2233 if (WebInspector.debugDefaultTextEditor)
2234 console.warn("Falling back to expensive edit");
2235 var range = new WebInspector.TextRange(startLine, 0, endLine, this._textModel.lineLength(endLine));
2236 if (!lines.length) {
2237 // Entire damaged area has collapsed. Replace everything between start and end lines with nothing.
2238 editInfo = new WebInspector.DefaultTextEditor.EditInfo(this._textModel.growRangeRight(range), "");
2240 editInfo = new WebInspector.DefaultTextEditor.EditInfo(range, lines.join("\n"));
2243 var selection = this._getSelection(collectLinesFromNode);
2245 // Unindent after block
2246 if (editInfo.text === "}" && editInfo.range.isEmpty() && selection.isEmpty() && !this._textModel.line(editInfo.range.endLine).trim()) {
2247 var offset = this._closingBlockOffset(editInfo.range, selection);
2249 editInfo.range.startColumn = offset;
2250 selection.startColumn = offset + 1;
2251 selection.endColumn = offset + 1;
2255 this._textModel.editRange(editInfo.range, editInfo.text);
2256 this._paintScheduledLines(true);
2257 this._restoreSelection(selection);
2261 * @param {number} startLine
2262 * @param {number} endLine
2263 * @param {Array.<string>} lines
2264 * @return {?WebInspector.DefaultTextEditor.EditInfo}
2266 _guessEditRangeBasedOnSelection: function(startLine, endLine, lines)
2268 // Analyze input data
2269 var textInputData = this._textInputData;
2270 delete this._textInputData;
2271 var isBackspace = this._keyDownCode === WebInspector.KeyboardShortcut.Keys.Backspace.code;
2272 var isDelete = this._keyDownCode === WebInspector.KeyboardShortcut.Keys.Delete.code;
2274 if (!textInputData && (isDelete || isBackspace))
2277 // Return if there is no input data or selection
2278 if (typeof textInputData === "undefined" || !this._lastSelection)
2281 // Adjust selection based on the keyboard actions (grow for backspace, etc.).
2282 textInputData = textInputData || "";
2283 var range = this._lastSelection.normalize();
2284 if (isBackspace && range.isEmpty())
2285 range = this._textModel.growRangeLeft(range);
2286 else if (isDelete && range.isEmpty())
2287 range = this._textModel.growRangeRight(range);
2289 // Test that selection intersects damaged lines
2290 if (startLine > range.endLine || endLine < range.startLine)
2293 var replacementLineCount = textInputData.split("\n").length - 1;
2294 var lineCountDelta = replacementLineCount - range.linesCount;
2295 if (startLine + lines.length - endLine - 1 !== lineCountDelta)
2298 // Clone text model of the size that fits both: selection before edit and the damaged lines after edit.
2299 var cloneFromLine = Math.min(range.startLine, startLine);
2300 var postLastLine = startLine + lines.length + lineCountDelta;
2301 var cloneToLine = Math.min(Math.max(postLastLine, range.endLine) + 1, this._textModel.linesCount);
2302 var domModel = this._textModel.slice(cloneFromLine, cloneToLine);
2303 domModel.editRange(range.shift(-cloneFromLine), textInputData);
2305 // Then we'll test if this new model matches the DOM lines.
2306 for (var i = 0; i < lines.length; ++i) {
2307 if (domModel.line(i + startLine - cloneFromLine) !== lines[i])
2310 return new WebInspector.DefaultTextEditor.EditInfo(range, textInputData);
2313 _assertDOMMatchesTextModel: function()
2315 if (!WebInspector.debugDefaultTextEditor)
2318 console.assert(this.element.innerText === this._textModel.text() + "\n", "DOM does not match model.");
2319 for (var lineRow = this._container.firstChild; lineRow; lineRow = lineRow.nextSibling) {
2320 var lineNumber = lineRow.lineNumber;
2321 if (typeof lineNumber !== "number") {
2322 console.warn("No line number on line row");
2325 if (lineRow._chunk) {
2326 var chunk = lineRow._chunk;
2327 console.assert(lineNumber === chunk.startLine);
2328 var chunkText = this._textModel.copyRange(new WebInspector.TextRange(chunk.startLine, 0, chunk.endLine - 1, this._textModel.lineLength(chunk.endLine - 1)));
2329 if (chunkText !== lineRow.textContent)
2330 console.warn("Chunk is not matching: %d %O", lineNumber, lineRow);
2331 } else if (this._textModel.line(lineNumber) !== lineRow.textContent)
2332 console.warn("Line is not matching: %d %O", lineNumber, lineRow);
2337 * @param {WebInspector.TextRange} oldRange
2338 * @param {WebInspector.TextRange} selection
2341 _closingBlockOffset: function(oldRange, selection)
2343 var nestingLevel = 1;
2344 for (var i = oldRange.endLine; i >= 0; --i) {
2345 var attribute = this._textModel.getAttribute(i, "highlight");
2348 var columnNumbers = Object.keys(attribute).reverse();
2349 for (var j = 0; j < columnNumbers.length; ++j) {
2350 var column = columnNumbers[j];
2351 if (attribute[column].tokenType === "block-start") {
2352 if (!(--nestingLevel)) {
2353 var lineContent = this._textModel.line(i);
2354 return lineContent.length - lineContent.trimLeft().length;
2357 if (attribute[column].tokenType === "block-end")
2365 * @param {WebInspector.TextRange} oldRange
2366 * @param {WebInspector.TextRange} newRange
2368 textChanged: function(oldRange, newRange)
2370 this.beginDomUpdates();
2371 this._removeDecorationsInRange(oldRange);
2372 this._updateChunksForRanges(oldRange, newRange);
2373 this._updateHighlightsForRange(newRange);
2374 this.endDomUpdates();
2378 * @param {WebInspector.TextRange} range
2380 _removeDecorationsInRange: function(range)
2382 for (var i = this._chunkNumberForLine(range.startLine); i < this._textChunks.length; ++i) {
2383 var chunk = this._textChunks[i];
2384 if (chunk.startLine > range.endLine)
2386 chunk.removeAllDecorations();
2391 * @param {WebInspector.TextRange} oldRange
2392 * @param {WebInspector.TextRange} newRange
2394 _updateChunksForRanges: function(oldRange, newRange)
2396 var firstDamagedChunkNumber = this._chunkNumberForLine(oldRange.startLine);
2397 var lastDamagedChunkNumber = firstDamagedChunkNumber;
2398 while (lastDamagedChunkNumber + 1 < this._textChunks.length) {
2399 if (this._textChunks[lastDamagedChunkNumber + 1].startLine > oldRange.endLine)
2401 ++lastDamagedChunkNumber;
2404 var firstDamagedChunk = this._textChunks[firstDamagedChunkNumber];
2405 var lastDamagedChunk = this._textChunks[lastDamagedChunkNumber];
2407 var linesDiff = newRange.linesCount - oldRange.linesCount;
2409 // First, detect chunks that have not been modified and simply shift them.
2411 for (var chunkNumber = lastDamagedChunkNumber + 1; chunkNumber < this._textChunks.length; ++chunkNumber)
2412 this._textChunks[chunkNumber].startLine += linesDiff;
2415 // Remove damaged chunks from DOM and from textChunks model.
2416 var lastUndamagedChunk = firstDamagedChunkNumber > 0 ? this._textChunks[firstDamagedChunkNumber - 1] : null;
2417 var firstUndamagedChunk = lastDamagedChunkNumber + 1 < this._textChunks.length ? this._textChunks[lastDamagedChunkNumber + 1] : null;
2419 var removeDOMFromNode = lastUndamagedChunk ? lastUndamagedChunk.lastElement().nextSibling : this._container.firstChild;
2420 var removeDOMToNode = firstUndamagedChunk ? firstUndamagedChunk.firstElement() : null;
2422 // Fast case - patch single expanded chunk that did not grow / shrink during edit.
2423 if (!linesDiff && firstDamagedChunk === lastDamagedChunk && firstDamagedChunk._expandedLineRows) {
2424 var lastUndamagedLineRow = lastDamagedChunk.expandedLineRow(oldRange.startLine - 1);
2425 var firstUndamagedLineRow = firstDamagedChunk.expandedLineRow(oldRange.endLine + 1);
2426 var localRemoveDOMFromNode = lastUndamagedLineRow ? lastUndamagedLineRow.nextSibling : removeDOMFromNode;
2427 var localRemoveDOMToNode = firstUndamagedLineRow || removeDOMToNode;
2428 removeSubsequentNodes(localRemoveDOMFromNode, localRemoveDOMToNode);
2429 for (var i = newRange.startLine; i < newRange.endLine + 1; ++i) {
2430 var row = firstDamagedChunk._createRow(i);
2431 firstDamagedChunk._expandedLineRows[i - firstDamagedChunk.startLine] = row;
2432 this._container.insertBefore(row, localRemoveDOMToNode);
2434 firstDamagedChunk.updateCollapsedLineRow();
2435 this._assertDOMMatchesTextModel();
2439 removeSubsequentNodes(removeDOMFromNode, removeDOMToNode);
2440 this._textChunks.splice(firstDamagedChunkNumber, lastDamagedChunkNumber - firstDamagedChunkNumber + 1);
2442 // Compute damaged chunks span
2443 var startLine = firstDamagedChunk.startLine;
2444 var endLine = lastDamagedChunk.endLine + linesDiff;
2445 var lineSpan = endLine - startLine;
2447 // Re-create chunks for damaged area.
2448 var insertionIndex = firstDamagedChunkNumber;
2449 var chunkSize = Math.ceil(lineSpan / Math.ceil(lineSpan / this._defaultChunkSize));
2451 for (var i = startLine; i < endLine; i += chunkSize) {
2452 var chunk = this._createNewChunk(i, Math.min(endLine, i + chunkSize));
2453 this._textChunks.splice(insertionIndex++, 0, chunk);
2454 this._container.insertBefore(chunk.element, removeDOMToNode);
2457 this._assertDOMMatchesTextModel();
2461 * @param {WebInspector.TextRange} range
2463 _updateHighlightsForRange: function(range)
2465 var visibleFrom = this._scrollTop();
2466 var visibleTo = visibleFrom + this._clientHeight();
2468 var result = this._findVisibleChunks(visibleFrom, visibleTo);
2469 var chunk = this._textChunks[result.end - 1];
2470 var lastVisibleLine = chunk.startLine + chunk.linesCount;
2472 lastVisibleLine = Math.max(lastVisibleLine, range.endLine + 1);
2473 lastVisibleLine = Math.min(lastVisibleLine, this._textModel.linesCount);
2475 var updated = this._highlighter.updateHighlight(range.startLine, lastVisibleLine);
2477 // Highlights for the chunks below are invalid, so just collapse them.
2478 for (var i = this._chunkNumberForLine(range.startLine); i < this._textChunks.length; ++i)
2479 this._textChunks[i].expanded = false;
2486 * @param {Node} from
2488 * @return {Array.<string>}
2490 _collectLinesFromDOM: function(from, to)
2492 var textContents = [];
2493 var hasContent = false;
2494 for (var node = from ? from.nextSibling : this._container; node && node !== to; node = node.traverseNextNode(this._container)) {
2495 if (node._isDecorationsElement) {
2496 // Skip all children of the decoration container.
2497 node = node.nextSibling;
2498 if (!node || node === to)
2502 if (node.nodeName.toLowerCase() === "br")
2503 textContents.push("\n");
2504 else if (node.nodeType === Node.TEXT_NODE)
2505 textContents.push(node.textContent);
2510 var textContent = textContents.join("");
2511 // The last \n (if any) does not "count" in a DIV.
2512 textContent = textContent.replace(/\n$/, "");
2514 return textContent.split("\n");
2517 _handleSelectionChange: function(event)
2519 var textRange = this._getSelection();
2521 this._lastSelection = textRange;
2522 this._delegate.selectionChanged(textRange);
2525 __proto__: WebInspector.TextEditorChunkedPanel.prototype
2530 * @param {WebInspector.TextEditorChunkedPanel} chunkedPanel
2531 * @param {number} startLine
2532 * @param {number} endLine
2534 WebInspector.TextEditorMainChunk = function(chunkedPanel, startLine, endLine)
2536 this._chunkedPanel = chunkedPanel;
2537 this._textModel = chunkedPanel._textModel;
2539 this.element = document.createElement("div");
2540 this.element.lineNumber = startLine;
2541 this.element.className = "webkit-line-content";
2542 this.element._chunk = this;
2544 this._startLine = startLine;
2545 endLine = Math.min(this._textModel.linesCount, endLine);
2546 this.linesCount = endLine - startLine;
2548 this._expanded = false;
2550 this.updateCollapsedLineRow();
2553 WebInspector.TextEditorMainChunk.prototype = {
2554 addDecoration: function(decoration)
2556 this._chunkedPanel.beginDomUpdates();
2557 if (typeof decoration === "string")
2558 this.element.addStyleClass(decoration);
2560 if (!this.element.decorationsElement) {
2561 this.element.decorationsElement = document.createElement("div");
2562 this.element.decorationsElement.className = "webkit-line-decorations";
2563 this.element.decorationsElement._isDecorationsElement = true;
2564 this.element.appendChild(this.element.decorationsElement);
2566 this.element.decorationsElement.appendChild(decoration);
2568 this._chunkedPanel.endDomUpdates();
2572 * @param {string|Element} decoration
2574 removeDecoration: function(decoration)
2576 this._chunkedPanel.beginDomUpdates();
2577 if (typeof decoration === "string")
2578 this.element.removeStyleClass(decoration);
2579 else if (this.element.decorationsElement)
2580 this.element.decorationsElement.removeChild(decoration);
2581 this._chunkedPanel.endDomUpdates();
2584 removeAllDecorations: function()
2586 this._chunkedPanel.beginDomUpdates();
2587 this.element.className = "webkit-line-content";
2588 if (this.element.decorationsElement) {
2589 this.element.removeChild(this.element.decorationsElement);
2590 delete this.element.decorationsElement;
2592 this._chunkedPanel.endDomUpdates();
2598 isDecorated: function()
2600 return this.element.className !== "webkit-line-content" || !!(this.element.decorationsElement && this.element.decorationsElement.firstChild);
2608 return this._startLine;
2616 return this._startLine + this.linesCount;
2619 set startLine(startLine)
2621 this._startLine = startLine;
2622 this.element.lineNumber = startLine;
2623 if (this._expandedLineRows) {
2624 for (var i = 0; i < this._expandedLineRows.length; ++i)
2625 this._expandedLineRows[i].lineNumber = startLine + i;
2634 return this._expanded;
2637 set expanded(expanded)
2639 if (this._expanded === expanded)
2642 this._expanded = expanded;
2644 if (this.linesCount === 1) {
2646 this._chunkedPanel._paintLine(this.element);
2650 this._chunkedPanel.beginDomUpdates();
2653 this._expandedLineRows = [];
2654 var parentElement = this.element.parentElement;
2655 for (var i = this.startLine; i < this.startLine + this.linesCount; ++i) {
2656 var lineRow = this._createRow(i);
2657 parentElement.insertBefore(lineRow, this.element);
2658 this._expandedLineRows.push(lineRow);
2660 parentElement.removeChild(this.element);
2661 this._chunkedPanel._paintLines(this.startLine, this.startLine + this.linesCount);
2663 var elementInserted = false;
2664 for (var i = 0; i < this._expandedLineRows.length; ++i) {
2665 var lineRow = this._expandedLineRows[i];
2666 var parentElement = lineRow.parentElement;
2667 if (parentElement) {
2668 if (!elementInserted) {
2669 elementInserted = true;
2670 parentElement.insertBefore(this.element, lineRow);
2672 parentElement.removeChild(lineRow);
2674 this._chunkedPanel._releaseLinesHighlight(lineRow);
2676 delete this._expandedLineRows;
2679 this._chunkedPanel.endDomUpdates();
2687 if (!this._expandedLineRows)
2688 return this._chunkedPanel._totalHeight(this.element);
2689 return this._chunkedPanel._totalHeight(this._expandedLineRows[0], this._expandedLineRows[this._expandedLineRows.length - 1]);
2697 return (this._expandedLineRows && this._expandedLineRows.length) ? this._expandedLineRows[0].offsetTop : this.element.offsetTop;
2701 * @param {number} lineNumber
2704 _createRow: function(lineNumber)
2706 var lineRow = this._chunkedPanel._cachedRows.pop() || document.createElement("div");
2707 lineRow.lineNumber = lineNumber;
2708 lineRow.className = "webkit-line-content";
2709 lineRow.textContent = this._textModel.line(lineNumber);
2710 if (!lineRow.textContent)
2711 lineRow.appendChild(document.createElement("br"));
2716 * Called on potentially damaged / inconsistent chunk
2717 * @param {number} lineNumber
2720 lineRowContainingLine: function(lineNumber)
2722 if (!this._expanded)
2723 return this.element;
2724 return this.expandedLineRow(lineNumber);
2728 * @param {number} lineNumber
2731 expandedLineRow: function(lineNumber)
2733 if (!this._expanded || lineNumber < this.startLine || lineNumber >= this.startLine + this.linesCount)
2735 if (!this._expandedLineRows)
2736 return this.element;
2737 return this._expandedLineRows[lineNumber - this.startLine];
2740 updateCollapsedLineRow: function()
2742 if (this.linesCount === 1 && this._expanded)
2746 for (var i = this.startLine; i < this.startLine + this.linesCount; ++i)
2747 lines.push(this._textModel.line(i));
2749 if (WebInspector.FALSE)
2750 console.log("Rebuilding chunk with " + lines.length + " lines");
2752 this.element.removeChildren();
2753 this.element.textContent = lines.join("\n");
2754 // The last empty line will get swallowed otherwise.
2755 if (!lines[lines.length - 1])
2756 this.element.appendChild(document.createElement("br"));
2759 firstElement: function()
2761 return this._expandedLineRows ? this._expandedLineRows[0] : this.element;
2764 lastElement: function()
2766 return this._expandedLineRows ? this._expandedLineRows[this._expandedLineRows.length - 1] : this.element;
2770 WebInspector.debugDefaultTextEditor = false;