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);
68 this._gutterPanel.element.addEventListener("mousedown", this._onMouseDown.bind(this), true);
70 // Explicitly enable middle-click pasting in the editor main panel.
71 this._mainPanel.element.addEventListener("mouseup", consumeMouseUp.bind(this), false);
72 function consumeMouseUp(event)
74 if (event.button === 1)
78 this.element.appendChild(this._mainPanel.element);
79 this.element.appendChild(this._gutterPanel.element);
81 // Forward mouse wheel events from the unscrollable gutter to the main panel.
82 function forwardWheelEvent(event)
84 var clone = document.createEvent("WheelEvent");
85 clone.initWebKitWheelEvent(event.wheelDeltaX, event.wheelDeltaY,
87 event.screenX, event.screenY,
88 event.clientX, event.clientY,
89 event.ctrlKey, event.altKey, event.shiftKey, event.metaKey);
90 this._mainPanel.element.dispatchEvent(clone);
92 this._gutterPanel.element.addEventListener("mousewheel", forwardWheelEvent.bind(this), false);
94 this.element.addEventListener("keydown", this._handleKeyDown.bind(this), false);
95 this.element.addEventListener("contextmenu", this._contextMenu.bind(this), true);
97 this._wordMovementController = new WebInspector.DefaultTextEditor.WordMovementController(this, this._textModel);
98 this._registerShortcuts();
103 * @param {WebInspector.TextRange} range
104 * @param {string} text
106 WebInspector.DefaultTextEditor.EditInfo = function(range, text)
112 WebInspector.DefaultTextEditor.prototype = {
114 * @param {WebInspector.TextRange} range
117 copyRange: function(range)
119 return this._textModel.copyRange(range);
123 * @param {string} regex
124 * @param {string} cssClass
125 * @return {WebInspector.TextEditorMainPanel.HighlightDescriptor}
127 highlightRegex: function(regex, cssClass)
129 return this._mainPanel.highlightRegex(regex, cssClass);
133 * @param {WebInspector.TextEditorMainPanel.HighlightDescriptor} highlightDescriptor
135 removeHighlight: function(highlightDescriptor)
137 this._mainPanel.removeHighlight(highlightDescriptor);
141 * @param {WebInspector.TextRange} range
142 * @param {string} cssClass
144 highlightRange: function(range, cssClass)
146 return this._mainPanel.highlightRange(range, cssClass);
150 * @param {string} mimeType
152 set mimeType(mimeType)
154 this._mainPanel.mimeType = mimeType;
158 * @param {boolean} readOnly
160 setReadOnly: function(readOnly)
162 if (this._mainPanel.readOnly() === readOnly)
164 this._mainPanel.setReadOnly(readOnly, this.isShowing());
165 WebInspector.markBeingEdited(this.element, !readOnly);
173 return this._mainPanel.readOnly();
179 defaultFocusedElement: function()
181 return this._mainPanel.defaultFocusedElement();
185 * @param {number} lineNumber
187 revealLine: function(lineNumber)
189 this._mainPanel.revealLine(lineNumber);
192 _onMouseDown: function(event)
194 var target = event.target.enclosingNodeOrSelfWithClass("webkit-line-number");
197 this.dispatchEventToListeners(WebInspector.TextEditor.Events.GutterClick, { lineNumber: target.lineNumber, event: event });
201 * @param {number} lineNumber
202 * @param {boolean} disabled
203 * @param {boolean} conditional
205 addBreakpoint: function(lineNumber, disabled, conditional)
208 this._gutterPanel.addDecoration(lineNumber, "webkit-breakpoint");
210 this._gutterPanel.addDecoration(lineNumber, "webkit-breakpoint-disabled");
212 this._gutterPanel.removeDecoration(lineNumber, "webkit-breakpoint-disabled");
214 this._gutterPanel.addDecoration(lineNumber, "webkit-breakpoint-conditional");
216 this._gutterPanel.removeDecoration(lineNumber, "webkit-breakpoint-conditional");
221 * @param {number} lineNumber
223 removeBreakpoint: function(lineNumber)
226 this._gutterPanel.removeDecoration(lineNumber, "webkit-breakpoint");
227 this._gutterPanel.removeDecoration(lineNumber, "webkit-breakpoint-disabled");
228 this._gutterPanel.removeDecoration(lineNumber, "webkit-breakpoint-conditional");
233 * @param {number} lineNumber
235 setExecutionLine: function(lineNumber)
237 this._executionLineNumber = lineNumber;
238 this._mainPanel.addDecoration(lineNumber, "webkit-execution-line");
239 this._gutterPanel.addDecoration(lineNumber, "webkit-execution-line");
242 clearExecutionLine: function()
244 if (typeof this._executionLineNumber === "number") {
245 this._mainPanel.removeDecoration(this._executionLineNumber, "webkit-execution-line");
246 this._gutterPanel.removeDecoration(this._executionLineNumber, "webkit-execution-line");
248 delete this._executionLineNumber;
252 * @param {number} lineNumber
253 * @param {Element} element
255 addDecoration: function(lineNumber, element)
257 this._mainPanel.addDecoration(lineNumber, element);
258 this._gutterPanel.addDecoration(lineNumber, element);
259 this._syncDecorationsForLine(lineNumber);
263 * @param {number} lineNumber
264 * @param {Element} element
266 removeDecoration: function(lineNumber, element)
268 this._mainPanel.removeDecoration(lineNumber, element);
269 this._gutterPanel.removeDecoration(lineNumber, element);
270 this._syncDecorationsForLine(lineNumber);
274 * @param {WebInspector.TextRange} range
276 markAndRevealRange: function(range)
279 this.setSelection(range);
280 this._mainPanel.markAndRevealRange(range);
284 * @param {number} lineNumber
286 highlightLine: function(lineNumber)
288 if (typeof lineNumber !== "number" || lineNumber < 0)
291 lineNumber = Math.min(lineNumber, this._textModel.linesCount - 1);
292 this._mainPanel.highlightLine(lineNumber);
295 clearLineHighlight: function()
297 this._mainPanel.clearLineHighlight();
301 * @return {Array.<Element>}
303 elementsToRestoreScrollPositionsFor: function()
305 return [this._mainPanel.element];
309 * @param {WebInspector.TextEditor} textEditor
311 inheritScrollPositions: function(textEditor)
313 this._mainPanel.element._scrollTop = textEditor._mainPanel.element.scrollTop;
314 this._mainPanel.element._scrollLeft = textEditor._mainPanel.element.scrollLeft;
317 beginUpdates: function()
319 this._mainPanel.beginUpdates();
320 this._gutterPanel.beginUpdates();
323 endUpdates: function()
325 this._mainPanel.endUpdates();
326 this._gutterPanel.endUpdates();
327 this._updatePanelOffsets();
332 this._mainPanel.resize();
333 this._gutterPanel.resize();
334 this._updatePanelOffsets();
337 _textChanged: function(event)
339 this._mainPanel.textChanged(event.data.oldRange, event.data.newRange);
340 this._gutterPanel.textChanged(event.data.oldRange, event.data.newRange);
341 this._updatePanelOffsets();
342 if (event.data.editRange)
343 this._delegate.onTextChanged(event.data.oldRange, event.data.newRange);
347 * @param {WebInspector.TextRange} range
348 * @param {string} text
349 * @return {WebInspector.TextRange}
351 editRange: function(range, text)
353 return this._textModel.editRange(range, text);
356 _updatePanelOffsets: function()
358 var lineNumbersWidth = this._gutterPanel.element.offsetWidth;
359 if (lineNumbersWidth)
360 this._mainPanel.element.style.setProperty("left", (lineNumbersWidth + 2) + "px");
362 this._mainPanel.element.style.removeProperty("left"); // Use default value set in CSS.
365 _syncScroll: function()
367 var mainElement = this._mainPanel.element;
368 var gutterElement = this._gutterPanel.element;
369 // Handle horizontal scroll bar at the bottom of the main panel.
370 this._gutterPanel.syncClientHeight(mainElement.clientHeight);
371 gutterElement.scrollTop = mainElement.scrollTop;
375 * @param {number} lineNumber
377 _syncDecorationsForLine: function(lineNumber)
379 if (lineNumber >= this._textModel.linesCount)
382 var mainChunk = this._mainPanel.chunkForLine(lineNumber);
383 if (mainChunk.linesCount === 1 && mainChunk.isDecorated()) {
384 var gutterChunk = this._gutterPanel.makeLineAChunk(lineNumber);
385 var height = mainChunk.height;
387 gutterChunk.element.style.setProperty("height", height + "px");
389 gutterChunk.element.style.removeProperty("height");
391 var gutterChunk = this._gutterPanel.chunkForLine(lineNumber);
392 if (gutterChunk.linesCount === 1)
393 gutterChunk.element.style.removeProperty("height");
398 * @param {Element} gutterRow
400 _syncLineHeight: function(gutterRow)
402 if (this._lineHeightSynced)
404 if (gutterRow && gutterRow.offsetHeight) {
405 // Force equal line heights for the child panels.
406 this.element.style.setProperty("line-height", gutterRow.offsetHeight + "px");
407 this._lineHeightSynced = true;
411 _registerShortcuts: function()
413 var keys = WebInspector.KeyboardShortcut.Keys;
414 var modifiers = WebInspector.KeyboardShortcut.Modifiers;
416 this._shortcuts = {};
418 this._shortcuts[WebInspector.KeyboardShortcut.SelectAll] = this._handleSelectAll.bind(this);
419 this._wordMovementController._registerShortcuts(this._shortcuts);
422 _handleSelectAll: function()
424 this.setSelection(this._textModel.range());
428 _handleKeyDown: function(e)
430 // If the event was not triggered from the entire editor, then
431 // ignore it. https://bugs.webkit.org/show_bug.cgi?id=102906
432 if (e.target.enclosingNodeOrSelfWithClass("webkit-line-decorations"))
435 var shortcutKey = WebInspector.KeyboardShortcut.makeKeyFromEvent(e);
437 var handler = this._shortcuts[shortcutKey];
438 if (handler && handler()) {
442 this._mainPanel.handleKeyDown(shortcutKey, e);
445 _contextMenu: function(event)
447 var anchor = event.target.enclosingNodeOrSelfWithNodeName("a");
450 var contextMenu = new WebInspector.ContextMenu(event);
451 var target = event.target.enclosingNodeOrSelfWithClass("webkit-line-number");
453 this._delegate.populateLineGutterContextMenu(contextMenu, target.lineNumber);
455 this._mainPanel.populateContextMenu(event.target, contextMenu);
460 _handleScrollChanged: function(event)
462 var visibleFrom = this._mainPanel.scrollTop();
463 var firstVisibleLineNumber = this._mainPanel.lineNumberAtOffset(visibleFrom);
464 this._delegate.scrollChanged(firstVisibleLineNumber);
468 * @param {number} lineNumber
470 scrollToLine: function(lineNumber)
472 this._mainPanel.scrollToLine(lineNumber);
476 * @return {WebInspector.TextRange}
478 selection: function()
480 return this._mainPanel.selection();
484 * @return {WebInspector.TextRange?}
486 lastSelection: function()
488 return this._mainPanel.lastSelection();
492 * @param {WebInspector.TextRange} textRange
494 setSelection: function(textRange)
496 this._mainPanel.setSelection(textRange);
500 * @param {string} text
502 setText: function(text)
504 this._textModel.setText(text);
512 return this._textModel.text();
516 * @return {WebInspector.TextRange}
520 return this._textModel.range();
524 * @param {number} lineNumber
527 line: function(lineNumber)
529 return this._textModel.line(lineNumber);
537 return this._textModel.linesCount;
541 * @param {number} line
542 * @param {string} name
543 * @param {?Object} value
545 setAttribute: function(line, name, value)
547 this._textModel.setAttribute(line, name, value);
551 * @param {number} line
552 * @param {string} name
553 * @return {?Object} value
555 getAttribute: function(line, name)
557 return this._textModel.getAttribute(line, name);
561 * @param {number} line
562 * @param {string} name
564 removeAttribute: function(line, name)
566 this._textModel.removeAttribute(line, name);
571 if (!this.readOnly())
572 WebInspector.markBeingEdited(this.element, true);
574 this._mainPanel.wasShown();
579 this._mainPanel.willHide();
580 this._gutterPanel.willHide();
582 if (!this.readOnly())
583 WebInspector.markBeingEdited(this.element, false);
587 * @param {Element} element
588 * @param {Array.<Object>} resultRanges
589 * @param {string} styleClass
590 * @param {Array.<Object>=} changes
592 highlightRangesWithStyleClass: function(element, resultRanges, styleClass, changes)
594 this._mainPanel.beginDomUpdates();
595 WebInspector.highlightRangesWithStyleClass(element, resultRanges, styleClass, changes);
596 this._mainPanel.endDomUpdates();
600 * @param {Element} element
601 * @param {Object} skipClasses
602 * @param {Object} skipTokens
605 highlightExpression: function(element, skipClasses, skipTokens)
607 // Collect tokens belonging to evaluated expression.
608 var tokens = [element];
609 var token = element.previousSibling;
610 while (token && (skipClasses[token.className] || skipTokens[token.textContent.trim()])) {
612 token = token.previousSibling;
616 // Wrap them with highlight element.
617 this._mainPanel.beginDomUpdates();
618 var parentElement = element.parentElement;
619 var nextElement = element.nextSibling;
620 var container = document.createElement("span");
621 for (var i = 0; i < tokens.length; ++i)
622 container.appendChild(tokens[i]);
623 parentElement.insertBefore(container, nextElement);
624 this._mainPanel.endDomUpdates();
629 * @param {Element} highlightElement
631 hideHighlightedExpression: function(highlightElement)
633 this._mainPanel.beginDomUpdates();
634 var parentElement = highlightElement.parentElement;
636 var child = highlightElement.firstChild;
638 var nextSibling = child.nextSibling;
639 parentElement.insertBefore(child, highlightElement);
642 parentElement.removeChild(highlightElement);
644 this._mainPanel.endDomUpdates();
648 * @param {number} scrollTop
649 * @param {number} clientHeight
650 * @param {number} chunkSize
652 overrideViewportForTest: function(scrollTop, clientHeight, chunkSize)
654 this._mainPanel.overrideViewportForTest(scrollTop, clientHeight, chunkSize);
657 __proto__: WebInspector.View.prototype
662 * @param {WebInspector.TextEditorModel} textModel
664 WebInspector.TextEditorChunkedPanel = function(textModel)
666 this._textModel = textModel;
668 this.element = document.createElement("div");
669 this.element.addEventListener("scroll", this._scroll.bind(this), false);
671 this._defaultChunkSize = 50;
672 this._paintCoalescingLevel = 0;
673 this._domUpdateCoalescingLevel = 0;
676 WebInspector.TextEditorChunkedPanel.prototype = {
678 * @param {number} lineNumber
680 scrollToLine: function(lineNumber)
682 if (lineNumber >= this._textModel.linesCount)
685 var chunk = this.makeLineAChunk(lineNumber);
686 this.element.scrollTop = chunk.offsetTop;
690 * @param {number} lineNumber
692 revealLine: function(lineNumber)
694 if (lineNumber >= this._textModel.linesCount)
697 var chunk = this.makeLineAChunk(lineNumber);
698 chunk.element.scrollIntoViewIfNeeded();
702 * @param {number} lineNumber
703 * @param {string|Element} decoration
705 addDecoration: function(lineNumber, decoration)
707 if (lineNumber >= this._textModel.linesCount)
710 var chunk = this.makeLineAChunk(lineNumber);
711 chunk.addDecoration(decoration);
715 * @param {number} lineNumber
716 * @param {string|Element} decoration
718 removeDecoration: function(lineNumber, decoration)
720 if (lineNumber >= this._textModel.linesCount)
723 var chunk = this.chunkForLine(lineNumber);
724 chunk.removeDecoration(decoration);
727 buildChunks: function()
729 this.beginDomUpdates();
731 this._container.removeChildren();
733 this._textChunks = [];
734 for (var i = 0; i < this._textModel.linesCount; i += this._defaultChunkSize) {
735 var chunk = this.createNewChunk(i, i + this._defaultChunkSize);
736 this._textChunks.push(chunk);
737 this._container.appendChild(chunk.element);
742 this.endDomUpdates();
746 * @param {number} lineNumber
749 makeLineAChunk: function(lineNumber)
751 var chunkNumber = this.chunkNumberForLine(lineNumber);
752 var oldChunk = this._textChunks[chunkNumber];
755 console.error("No chunk for line number: " + lineNumber);
759 if (oldChunk.linesCount === 1)
762 return this.splitChunkOnALine(lineNumber, chunkNumber, true);
766 * @param {number} lineNumber
767 * @param {number} chunkNumber
768 * @param {boolean=} createSuffixChunk
771 splitChunkOnALine: function(lineNumber, chunkNumber, createSuffixChunk)
773 this.beginDomUpdates();
775 var oldChunk = this._textChunks[chunkNumber];
776 var wasExpanded = oldChunk.expanded();
779 var insertIndex = chunkNumber + 1;
782 if (lineNumber > oldChunk.startLine) {
783 var prefixChunk = this.createNewChunk(oldChunk.startLine, lineNumber);
784 this._textChunks.splice(insertIndex++, 0, prefixChunk);
785 this._container.insertBefore(prefixChunk.element, oldChunk.element);
789 var endLine = createSuffixChunk ? lineNumber + 1 : oldChunk.startLine + oldChunk.linesCount;
790 var lineChunk = this.createNewChunk(lineNumber, endLine);
791 this._textChunks.splice(insertIndex++, 0, lineChunk);
792 this._container.insertBefore(lineChunk.element, oldChunk.element);
795 if (oldChunk.startLine + oldChunk.linesCount > endLine) {
796 var suffixChunk = this.createNewChunk(endLine, oldChunk.startLine + oldChunk.linesCount);
797 this._textChunks.splice(insertIndex, 0, suffixChunk);
798 this._container.insertBefore(suffixChunk.element, oldChunk.element);
801 // Remove enclosing chunk.
802 this._textChunks.splice(chunkNumber, 1);
803 this._container.removeChild(oldChunk.element);
807 prefixChunk.expand();
810 suffixChunk.expand();
813 this.endDomUpdates();
818 createNewChunk: function(startLine, endLine)
820 throw new Error("createNewChunk() should be implemented by descendants");
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
863 chunkNumberForLine: function(lineNumber)
865 function compareLineNumbers(value, chunk)
867 return value < chunk.startLine ? -1 : 1;
869 var insertBefore = insertionIndexForObjectInListSortedByFunction(lineNumber, this._textChunks, compareLineNumbers);
870 return insertBefore - 1;
874 * @param {number} lineNumber
877 chunkForLine: function(lineNumber)
879 return this._textChunks[this.chunkNumberForLine(lineNumber)];
883 * @param {number} visibleFrom
886 _findFirstVisibleChunkNumber: function(visibleFrom)
888 function compareOffsetTops(value, chunk)
890 return value < chunk.offsetTop ? -1 : 1;
892 var insertBefore = insertionIndexForObjectInListSortedByFunction(visibleFrom, this._textChunks, compareOffsetTops);
893 return insertBefore - 1;
897 * @param {number} visibleFrom
898 * @param {number} visibleTo
899 * @return {{start: number, end: number}}
901 findVisibleChunks: function(visibleFrom, visibleTo)
903 var span = (visibleTo - visibleFrom) * 0.5;
904 visibleFrom = Math.max(visibleFrom - span, 0);
905 visibleTo = visibleTo + span;
907 var from = this._findFirstVisibleChunkNumber(visibleFrom);
908 for (var to = from + 1; to < this._textChunks.length; ++to) {
909 if (this._textChunks[to].offsetTop >= visibleTo)
912 return { start: from, end: to };
916 * @param {number} visibleFrom
919 lineNumberAtOffset: function(visibleFrom)
921 var chunk = this._textChunks[this._findFirstVisibleChunkNumber(visibleFrom)];
922 if (!chunk.expanded())
923 return chunk.startLine;
925 var lineNumbers = [];
926 for (var i = 0; i < chunk.linesCount; ++i) {
927 lineNumbers.push(chunk.startLine + i);
930 function compareLineRowOffsetTops(value, lineNumber)
932 var lineRow = chunk.expandedLineRow(lineNumber);
933 return value < lineRow.offsetTop ? -1 : 1;
935 var insertBefore = insertionIndexForObjectInListSortedByFunction(visibleFrom, lineNumbers, compareLineRowOffsetTops);
936 return lineNumbers[insertBefore - 1];
939 repaintAll: function()
941 delete this._repaintAllTimer;
943 if (this._paintCoalescingLevel)
946 var visibleFrom = this.scrollTop();
947 var visibleTo = visibleFrom + this.clientHeight();
950 var result = this.findVisibleChunks(visibleFrom, visibleTo);
951 this.expandChunks(result.start, result.end);
955 scrollTop: function()
957 return typeof this._scrollTopOverrideForTest === "number" ? this._scrollTopOverrideForTest : this.element.scrollTop;
960 clientHeight: function()
962 return typeof this._clientHeightOverrideForTest === "number" ? this._clientHeightOverrideForTest : this.element.clientHeight;
966 * @param {number} fromIndex
967 * @param {number} toIndex
969 expandChunks: function(fromIndex, toIndex)
971 // First collapse chunks to collect the DOM elements into a cache to reuse them later.
972 for (var i = 0; i < fromIndex; ++i)
973 this._textChunks[i].collapse();
974 for (var i = toIndex; i < this._textChunks.length; ++i)
975 this._textChunks[i].collapse();
976 for (var i = fromIndex; i < toIndex; ++i)
977 this._textChunks[i].expand();
981 * @param {Element} firstElement
982 * @param {Element=} lastElement
985 totalHeight: function(firstElement, lastElement)
987 lastElement = (lastElement || firstElement).nextElementSibling;
989 return lastElement.offsetTop - firstElement.offsetTop;
991 var offsetParent = firstElement.offsetParent;
992 if (offsetParent && offsetParent.scrollHeight > offsetParent.clientHeight)
993 return offsetParent.scrollHeight - firstElement.offsetTop;
996 while (firstElement && firstElement !== lastElement) {
997 total += firstElement.offsetHeight;
998 firstElement = firstElement.nextElementSibling;
1011 * @extends {WebInspector.TextEditorChunkedPanel}
1012 * @param {WebInspector.TextEditorModel} textModel
1013 * @param {function(number)} syncDecorationsForLineListener
1014 * @param {function(Element)} syncLineHeightListener
1016 WebInspector.TextEditorGutterPanel = function(textModel, syncDecorationsForLineListener, syncLineHeightListener)
1018 WebInspector.TextEditorChunkedPanel.call(this, textModel);
1020 this._syncDecorationsForLineListener = syncDecorationsForLineListener;
1021 this._syncLineHeightListener = syncLineHeightListener;
1023 this.element.className = "text-editor-lines";
1025 this._container = document.createElement("div");
1026 this._container.className = "inner-container";
1027 this.element.appendChild(this._container);
1029 this._freeCachedElements();
1031 this._decorations = {};
1034 WebInspector.TextEditorGutterPanel.prototype = {
1035 _freeCachedElements: function()
1037 this._cachedRows = [];
1040 willHide: function()
1042 this._freeCachedElements();
1046 * @param {number} startLine
1047 * @param {number} endLine
1048 * @return {WebInspector.TextEditorGutterChunk}
1050 createNewChunk: function(startLine, endLine)
1052 return new WebInspector.TextEditorGutterChunk(this, startLine, endLine);
1056 * @param {WebInspector.TextRange} oldRange
1057 * @param {WebInspector.TextRange} newRange
1059 textChanged: function(oldRange, newRange)
1061 this.beginDomUpdates();
1063 var linesDiff = newRange.linesCount - oldRange.linesCount;
1065 // Remove old chunks (if needed).
1066 for (var chunkNumber = this._textChunks.length - 1; chunkNumber >= 0; --chunkNumber) {
1067 var chunk = this._textChunks[chunkNumber];
1068 if (chunk.startLine + chunk.linesCount <= this._textModel.linesCount)
1071 this._container.removeChild(chunk.element);
1073 this._textChunks.length = chunkNumber + 1;
1075 // Add new chunks (if needed).
1077 if (this._textChunks.length) {
1078 var lastChunk = this._textChunks[this._textChunks.length - 1];
1079 totalLines = lastChunk.startLine + lastChunk.linesCount;
1082 for (var i = totalLines; i < this._textModel.linesCount; i += this._defaultChunkSize) {
1083 var chunk = this.createNewChunk(i, i + this._defaultChunkSize);
1084 this._textChunks.push(chunk);
1085 this._container.appendChild(chunk.element);
1088 // Shift decorations if necessary
1089 for (var lineNumber in this._decorations) {
1090 lineNumber = parseInt(lineNumber, 10);
1092 // Do not move decorations before the start position.
1093 if (lineNumber < oldRange.startLine)
1095 // Decorations follow the first character of line.
1096 if (lineNumber === oldRange.startLine && oldRange.startColumn)
1099 var lineDecorationsCopy = this._decorations[lineNumber].slice();
1100 for (var i = 0; i < lineDecorationsCopy.length; ++i) {
1101 var decoration = lineDecorationsCopy[i];
1102 this.removeDecoration(lineNumber, decoration);
1104 // Do not restore the decorations before the end position.
1105 if (lineNumber < oldRange.endLine)
1108 this.addDecoration(lineNumber + linesDiff, decoration);
1114 // Decorations may have been removed, so we may have to sync those lines.
1115 var chunkNumber = this.chunkNumberForLine(newRange.startLine);
1116 var chunk = this._textChunks[chunkNumber];
1117 while (chunk && chunk.startLine <= newRange.endLine) {
1118 if (chunk.linesCount === 1)
1119 this._syncDecorationsForLineListener(chunk.startLine);
1120 chunk = this._textChunks[++chunkNumber];
1124 this.endDomUpdates();
1128 * @param {number} clientHeight
1130 syncClientHeight: function(clientHeight)
1132 if (this.element.offsetHeight > clientHeight)
1133 this._container.style.setProperty("padding-bottom", (this.element.offsetHeight - clientHeight) + "px");
1135 this._container.style.removeProperty("padding-bottom");
1139 * @param {number} lineNumber
1140 * @param {string|Element} decoration
1142 addDecoration: function(lineNumber, decoration)
1144 WebInspector.TextEditorChunkedPanel.prototype.addDecoration.call(this, lineNumber, decoration);
1145 var decorations = this._decorations[lineNumber];
1148 this._decorations[lineNumber] = decorations;
1150 decorations.push(decoration);
1154 * @param {number} lineNumber
1155 * @param {string|Element} decoration
1157 removeDecoration: function(lineNumber, decoration)
1159 WebInspector.TextEditorChunkedPanel.prototype.removeDecoration.call(this, lineNumber, decoration);
1160 var decorations = this._decorations[lineNumber];
1162 decorations.remove(decoration);
1163 if (!decorations.length)
1164 delete this._decorations[lineNumber];
1168 __proto__: WebInspector.TextEditorChunkedPanel.prototype
1173 * @param {WebInspector.TextEditorGutterPanel} chunkedPanel
1174 * @param {number} startLine
1175 * @param {number} endLine
1177 WebInspector.TextEditorGutterChunk = function(chunkedPanel, startLine, endLine)
1179 this._chunkedPanel = chunkedPanel;
1180 this._textModel = chunkedPanel._textModel;
1182 this.startLine = startLine;
1183 endLine = Math.min(this._textModel.linesCount, endLine);
1184 this.linesCount = endLine - startLine;
1186 this._expanded = false;
1188 this.element = document.createElement("div");
1189 this.element.lineNumber = startLine;
1190 this.element.className = "webkit-line-number";
1192 if (this.linesCount === 1) {
1193 // Single line chunks are typically created for decorations. Host line number in
1194 // the sub-element in order to allow flexible border / margin management.
1195 var innerSpan = document.createElement("span");
1196 innerSpan.className = "webkit-line-number-inner";
1197 innerSpan.textContent = startLine + 1;
1198 var outerSpan = document.createElement("div");
1199 outerSpan.className = "webkit-line-number-outer";
1200 outerSpan.appendChild(innerSpan);
1201 this.element.appendChild(outerSpan);
1203 var lineNumbers = [];
1204 for (var i = startLine; i < endLine; ++i)
1205 lineNumbers.push(i + 1);
1206 this.element.textContent = lineNumbers.join("\n");
1210 WebInspector.TextEditorGutterChunk.prototype = {
1212 * @param {string} decoration
1214 addDecoration: function(decoration)
1216 this._chunkedPanel.beginDomUpdates();
1217 if (typeof decoration === "string")
1218 this.element.addStyleClass(decoration);
1219 this._chunkedPanel.endDomUpdates();
1223 * @param {string} decoration
1225 removeDecoration: function(decoration)
1227 this._chunkedPanel.beginDomUpdates();
1228 if (typeof decoration === "string")
1229 this.element.removeStyleClass(decoration);
1230 this._chunkedPanel.endDomUpdates();
1236 expanded: function()
1238 return this._expanded;
1243 if (this.linesCount === 1)
1244 this._chunkedPanel._syncDecorationsForLineListener(this.startLine);
1249 this._expanded = true;
1251 if (this.linesCount === 1)
1254 this._chunkedPanel.beginDomUpdates();
1256 this._expandedLineRows = [];
1257 var parentElement = this.element.parentElement;
1258 for (var i = this.startLine; i < this.startLine + this.linesCount; ++i) {
1259 var lineRow = this._createRow(i);
1260 parentElement.insertBefore(lineRow, this.element);
1261 this._expandedLineRows.push(lineRow);
1263 parentElement.removeChild(this.element);
1264 this._chunkedPanel._syncLineHeightListener(this._expandedLineRows[0]);
1266 this._chunkedPanel.endDomUpdates();
1269 collapse: function()
1271 if (this.linesCount === 1)
1272 this._chunkedPanel._syncDecorationsForLineListener(this.startLine);
1274 if (!this._expanded)
1277 this._expanded = false;
1279 if (this.linesCount === 1)
1282 this._chunkedPanel.beginDomUpdates();
1284 var elementInserted = false;
1285 for (var i = 0; i < this._expandedLineRows.length; ++i) {
1286 var lineRow = this._expandedLineRows[i];
1287 var parentElement = lineRow.parentElement;
1288 if (parentElement) {
1289 if (!elementInserted) {
1290 elementInserted = true;
1291 parentElement.insertBefore(this.element, lineRow);
1293 parentElement.removeChild(lineRow);
1295 this._chunkedPanel._cachedRows.push(lineRow);
1297 delete this._expandedLineRows;
1299 this._chunkedPanel.endDomUpdates();
1307 if (!this._expandedLineRows)
1308 return this._chunkedPanel.totalHeight(this.element);
1309 return this._chunkedPanel.totalHeight(this._expandedLineRows[0], this._expandedLineRows[this._expandedLineRows.length - 1]);
1317 return (this._expandedLineRows && this._expandedLineRows.length) ? this._expandedLineRows[0].offsetTop : this.element.offsetTop;
1321 * @param {number} lineNumber
1324 _createRow: function(lineNumber)
1326 var lineRow = this._chunkedPanel._cachedRows.pop() || document.createElement("div");
1327 lineRow.lineNumber = lineNumber;
1328 lineRow.className = "webkit-line-number";
1329 lineRow.textContent = lineNumber + 1;
1336 * @extends {WebInspector.TextEditorChunkedPanel}
1337 * @param {WebInspector.TextEditorDelegate} delegate
1338 * @param {WebInspector.TextEditorModel} textModel
1339 * @param {?string} url
1340 * @param {function()} syncScrollListener
1341 * @param {function(number)} syncDecorationsForLineListener
1343 WebInspector.TextEditorMainPanel = function(delegate, textModel, url, syncScrollListener, syncDecorationsForLineListener)
1345 WebInspector.TextEditorChunkedPanel.call(this, textModel);
1347 this._delegate = delegate;
1348 this._syncScrollListener = syncScrollListener;
1349 this._syncDecorationsForLineListener = syncDecorationsForLineListener;
1352 this._highlighter = new WebInspector.TextEditorHighlighter(textModel, this._highlightDataReady.bind(this));
1353 this._readOnly = true;
1355 this.element.className = "text-editor-contents";
1356 this.element.tabIndex = 0;
1358 this._container = document.createElement("div");
1359 this._container.className = "inner-container";
1360 this._container.tabIndex = 0;
1361 this.element.appendChild(this._container);
1363 this.element.addEventListener("focus", this._handleElementFocus.bind(this), false);
1364 this.element.addEventListener("textInput", this._handleTextInput.bind(this), false);
1365 this.element.addEventListener("cut", this._handleCut.bind(this), false);
1367 this._container.addEventListener("focus", this._handleFocused.bind(this), false);
1369 this._highlightDescriptors = [];
1371 this._tokenHighlighter = new WebInspector.TextEditorMainPanel.TokenHighlighter(this, textModel);
1372 this._braceMatcher = new WebInspector.TextEditorModel.BraceMatcher(textModel);
1373 this._braceHighlighter = new WebInspector.TextEditorMainPanel.BraceHighlightController(this, textModel, this._braceMatcher);
1375 this._freeCachedElements();
1377 this._registerShortcuts();
1380 WebInspector.TextEditorMainPanel.prototype = {
1381 _registerShortcuts: function()
1383 var keys = WebInspector.KeyboardShortcut.Keys;
1384 var modifiers = WebInspector.KeyboardShortcut.Modifiers;
1386 this._shortcuts = {};
1388 this._shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Enter.code, WebInspector.KeyboardShortcut.Modifiers.None)] = this._handleEnterKey.bind(this);
1389 this._shortcuts[WebInspector.KeyboardShortcut.makeKey("z", modifiers.CtrlOrMeta)] = this._handleUndoRedo.bind(this, false);
1391 var handleRedo = this._handleUndoRedo.bind(this, true);
1392 this._shortcuts[WebInspector.KeyboardShortcut.makeKey("z", modifiers.Shift | modifiers.CtrlOrMeta)] = handleRedo;
1393 if (!WebInspector.isMac())
1394 this._shortcuts[WebInspector.KeyboardShortcut.makeKey("y", modifiers.CtrlOrMeta)] = handleRedo;
1396 var handleTabKey = this._handleTabKeyPress.bind(this, false);
1397 var handleShiftTabKey = this._handleTabKeyPress.bind(this, true);
1398 this._shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Tab.code)] = handleTabKey;
1399 this._shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Tab.code, modifiers.Shift)] = handleShiftTabKey;
1403 * @param {string} regex
1404 * @param {string} cssClass
1405 * @return {WebInspector.TextEditorMainPanel.HighlightDescriptor}
1407 highlightRegex: function(regex, cssClass)
1409 var highlightDescriptor = new WebInspector.TextEditorMainPanel.RegexHighlightDescriptor(new RegExp(regex, "g"), cssClass);
1410 this._highlightDescriptors.push(highlightDescriptor);
1411 this._repaintLineRowsAffectedByHighlightDescriptors([highlightDescriptor]);
1412 return highlightDescriptor;
1416 * @param {WebInspector.TextEditorMainPanel.HighlightDescriptor} highlightDescriptor
1418 removeHighlight: function(highlightDescriptor)
1420 this._highlightDescriptors.remove(highlightDescriptor);
1421 this._repaintLineRowsAffectedByHighlightDescriptors([highlightDescriptor]);
1425 * @param {WebInspector.TextRange} range
1426 * @param {string} cssClass
1428 highlightRange: function(range, cssClass)
1430 var highlightDescriptor = new WebInspector.TextEditorMainPanel.RangeHighlightDescriptor(range, cssClass);
1431 this._highlightDescriptors.push(highlightDescriptor);
1432 this._repaintLineRowsAffectedByHighlightDescriptors([highlightDescriptor]);
1433 return highlightDescriptor;
1437 * @param {Array.<WebInspector.TextEditorMainPanel.HighlightDescriptor>} highlightDescriptors
1439 _repaintLineRowsAffectedByHighlightDescriptors: function(highlightDescriptors)
1441 var visibleFrom = this.scrollTop();
1442 var visibleTo = visibleFrom + this.clientHeight();
1444 var visibleChunks = this.findVisibleChunks(visibleFrom, visibleTo);
1446 var affectedLineRows = [];
1447 for (var i = visibleChunks.start; i < visibleChunks.end; ++i) {
1448 var chunk = this._textChunks[i];
1449 if (!chunk.expanded())
1451 for (var lineNumber = chunk.startLine; lineNumber < chunk.startLine + chunk.linesCount; ++lineNumber) {
1452 var lineRow = chunk.expandedLineRow(lineNumber);
1453 var line = this._textModel.line(lineNumber);
1454 for(var j = 0; j < highlightDescriptors.length; ++j) {
1455 if (highlightDescriptors[j].affectsLine(lineNumber, line)) {
1456 affectedLineRows.push(lineRow);
1462 if (affectedLineRows.length === 0)
1464 var selection = this.selection();
1465 this._paintLineRows(affectedLineRows);
1466 this._restoreSelection(selection);
1471 WebInspector.TextEditorChunkedPanel.prototype.resize.call(this);
1472 this._repaintLineRowsAffectedByHighlightDescriptors(this._highlightDescriptors);
1475 wasShown: function()
1477 this._boundSelectionChangeListener = this._handleSelectionChange.bind(this);
1478 document.addEventListener("selectionchange", this._boundSelectionChangeListener, false);
1480 this._isShowing = true;
1481 this._attachMutationObserver();
1484 willHide: function()
1486 document.removeEventListener("selectionchange", this._boundSelectionChangeListener, false);
1487 delete this._boundSelectionChangeListener;
1489 this._detachMutationObserver();
1490 this._isShowing = false;
1491 this._freeCachedElements();
1495 * @param {Element} eventTarget
1496 * @param {WebInspector.ContextMenu} contextMenu
1498 populateContextMenu: function(eventTarget, contextMenu)
1500 var target = this._enclosingLineRowOrSelf(eventTarget);
1501 this._delegate.populateTextAreaContextMenu(contextMenu, target && target.lineNumber);
1505 * @param {WebInspector.TextRange} textRange
1507 setSelection: function(textRange)
1509 this._lastSelection = textRange;
1510 if (this.element.isAncestor(document.activeElement))
1511 this._restoreSelection(textRange);
1514 _handleFocused: function()
1516 if (this._lastSelection)
1517 this.setSelection(this._lastSelection);
1520 _attachMutationObserver: function()
1522 if (!this._isShowing)
1525 if (this._mutationObserver)
1526 this._mutationObserver.disconnect();
1527 this._mutationObserver = new NonLeakingMutationObserver(this._handleMutations.bind(this));
1528 this._mutationObserver.observe(this._container, { subtree: true, childList: true, characterData: true });
1531 _detachMutationObserver: function()
1533 if (!this._isShowing)
1536 if (this._mutationObserver) {
1537 this._mutationObserver.disconnect();
1538 delete this._mutationObserver;
1543 * @param {string} mimeType
1545 set mimeType(mimeType)
1547 this._highlighter.mimeType = mimeType;
1551 * @param {boolean} readOnly
1552 * @param {boolean} requestFocus
1554 setReadOnly: function(readOnly, requestFocus)
1556 if (this._readOnly === readOnly)
1559 this.beginDomUpdates();
1560 this._readOnly = readOnly;
1562 this._container.removeStyleClass("text-editor-editable");
1564 this._container.addStyleClass("text-editor-editable");
1566 this._updateSelectionOnStartEditing();
1568 this.endDomUpdates();
1574 readOnly: function()
1576 return this._readOnly;
1579 _handleElementFocus: function()
1581 if (!this._readOnly)
1582 this._container.focus();
1588 defaultFocusedElement: function()
1591 return this.element;
1592 return this._container;
1595 _updateSelectionOnStartEditing: function()
1597 // focus() needs to go first for the case when the last selection was inside the editor and
1598 // the "Edit" button was clicked. In this case we bail at the check below, but the
1599 // editor does not receive the focus, thus "Esc" does not cancel editing until at least
1600 // one change has been made to the editor contents.
1601 this._container.focus();
1602 var selection = window.getSelection();
1603 if (selection.rangeCount) {
1604 var commonAncestorContainer = selection.getRangeAt(0).commonAncestorContainer;
1605 if (this._container.isSelfOrAncestor(commonAncestorContainer))
1609 selection.removeAllRanges();
1610 var range = document.createRange();
1611 range.setStart(this._container, 0);
1612 range.setEnd(this._container, 0);
1613 selection.addRange(range);
1617 * @param {WebInspector.TextRange} range
1619 markAndRevealRange: function(range)
1621 if (this._rangeToMark) {
1622 var markedLine = this._rangeToMark.startLine;
1623 delete this._rangeToMark;
1624 // Remove the marked region immediately.
1625 this.beginDomUpdates();
1626 var chunk = this.chunkForLine(markedLine);
1627 var wasExpanded = chunk.expanded();
1629 chunk.updateCollapsedLineRow();
1632 this.endDomUpdates();
1636 this._rangeToMark = range;
1637 this.revealLine(range.startLine);
1638 var chunk = this.makeLineAChunk(range.startLine);
1639 this._paintLines(chunk.startLine, chunk.startLine + 1);
1640 if (this._markedRangeElement)
1641 this._markedRangeElement.scrollIntoViewIfNeeded();
1643 delete this._markedRangeElement;
1647 * @param {number} lineNumber
1649 highlightLine: function(lineNumber)
1651 this.clearLineHighlight();
1652 this._highlightedLine = lineNumber;
1653 this.revealLine(lineNumber);
1655 if (!this._readOnly)
1656 this._restoreSelection(WebInspector.TextRange.createFromLocation(lineNumber, 0), false);
1658 this.addDecoration(lineNumber, "webkit-highlighted-line");
1661 clearLineHighlight: function()
1663 if (typeof this._highlightedLine === "number") {
1664 this.removeDecoration(this._highlightedLine, "webkit-highlighted-line");
1665 delete this._highlightedLine;
1669 _freeCachedElements: function()
1671 this._cachedSpans = [];
1672 this._cachedTextNodes = [];
1673 this._cachedRows = [];
1677 * @param {boolean} redo
1680 _handleUndoRedo: function(redo)
1682 if (this.readOnly())
1685 this.beginUpdates();
1687 var range = redo ? this._textModel.redo() : this._textModel.undo();
1691 // Restore location post-repaint.
1693 this._restoreSelection(range, true);
1699 * @param {boolean} shiftKey
1702 _handleTabKeyPress: function(shiftKey)
1704 if (this.readOnly())
1707 var selection = this.selection();
1711 var range = selection.normalize();
1713 this.beginUpdates();
1716 var rangeWasEmpty = range.isEmpty();
1718 newRange = this._textModel.unindentLines(range);
1721 newRange = this._textModel.editRange(range, WebInspector.settings.textEditorIndent.get());
1723 newRange = this._textModel.indentLines(range);
1728 newRange.startColumn = newRange.endColumn;
1729 this._restoreSelection(newRange, true);
1733 _handleEnterKey: function()
1735 if (this.readOnly())
1738 var range = this.selection();
1742 range = range.normalize();
1744 if (range.endColumn === 0)
1747 var line = this._textModel.line(range.startLine);
1748 var linePrefix = line.substring(0, range.startColumn);
1749 var indentMatch = linePrefix.match(/^\s+/);
1750 var currentIndent = indentMatch ? indentMatch[0] : "";
1752 var textEditorIndent = WebInspector.settings.textEditorIndent.get();
1753 var indent = WebInspector.TextEditorModel.endsWithBracketRegex.test(linePrefix) ? currentIndent + textEditorIndent : currentIndent;
1758 this.beginDomUpdates();
1760 var lineBreak = this._textModel.lineBreak;
1762 if (range.isEmpty() && line.substr(range.endColumn - 1, 2) === '{}') {
1768 newRange = this._textModel.editRange(range, lineBreak + indent + lineBreak + currentIndent);
1770 newRange.endColumn += textEditorIndent.length;
1772 newRange = this._textModel.editRange(range, lineBreak + indent);
1774 this.endDomUpdates();
1775 this._restoreSelection(newRange.collapseToEnd(), true);
1781 * @param {number} lineNumber
1782 * @param {number} chunkNumber
1783 * @param {boolean=} createSuffixChunk
1786 splitChunkOnALine: function(lineNumber, chunkNumber, createSuffixChunk)
1788 var selection = this.selection();
1789 var chunk = WebInspector.TextEditorChunkedPanel.prototype.splitChunkOnALine.call(this, lineNumber, chunkNumber, createSuffixChunk);
1790 this._restoreSelection(selection);
1794 beginDomUpdates: function()
1796 if (!this._domUpdateCoalescingLevel)
1797 this._detachMutationObserver();
1798 WebInspector.TextEditorChunkedPanel.prototype.beginDomUpdates.call(this);
1801 endDomUpdates: function()
1803 WebInspector.TextEditorChunkedPanel.prototype.endDomUpdates.call(this);
1804 if (!this._domUpdateCoalescingLevel)
1805 this._attachMutationObserver();
1808 buildChunks: function()
1810 for (var i = 0; i < this._textModel.linesCount; ++i)
1811 this._textModel.removeAttribute(i, "highlight");
1813 WebInspector.TextEditorChunkedPanel.prototype.buildChunks.call(this);
1817 * @param {number} startLine
1818 * @param {number} endLine
1819 * @return {WebInspector.TextEditorMainChunk}
1821 createNewChunk: function(startLine, endLine)
1823 return new WebInspector.TextEditorMainChunk(this, startLine, endLine);
1827 * @param {number} fromIndex
1828 * @param {number} toIndex
1830 expandChunks: function(fromIndex, toIndex)
1832 var lastChunk = this._textChunks[toIndex - 1];
1833 var lastVisibleLine = lastChunk.startLine + lastChunk.linesCount;
1835 var selection = this.selection();
1837 this._muteHighlightListener = true;
1838 this._highlighter.highlight(lastVisibleLine);
1839 delete this._muteHighlightListener;
1841 WebInspector.TextEditorChunkedPanel.prototype.expandChunks.call(this, fromIndex, toIndex);
1843 this._restoreSelection(selection);
1847 * @param {number} fromLine
1848 * @param {number} toLine
1850 _highlightDataReady: function(fromLine, toLine)
1852 if (this._muteHighlightListener)
1854 this._paintLines(fromLine, toLine, true /*restoreSelection*/);
1858 * @param {number} fromLine
1859 * @param {number} toLine
1860 * @param {boolean=} restoreSelection
1862 _paintLines: function(fromLine, toLine, restoreSelection)
1866 for (var lineNumber = fromLine; lineNumber < toLine; ++lineNumber) {
1867 if (!chunk || lineNumber < chunk.startLine || lineNumber >= chunk.startLine + chunk.linesCount)
1868 chunk = this.chunkForLine(lineNumber);
1869 var lineRow = chunk.expandedLineRow(lineNumber);
1872 lineRows.push(lineRow);
1874 if (lineRows.length === 0)
1878 if (restoreSelection)
1879 selection = this.selection();
1881 this._paintLineRows(lineRows);
1883 if (restoreSelection)
1884 this._restoreSelection(selection);
1888 * @param {Array.<Element>} lineRows
1890 _paintLineRows: function(lineRows)
1893 this.beginDomUpdates();
1894 for(var i = 0; i < this._highlightDescriptors.length; ++i) {
1895 var highlightDescriptor = this._highlightDescriptors[i];
1896 this._measureHighlightDescriptor(highlight, lineRows, highlightDescriptor);
1899 for(var i = 0; i < lineRows.length; ++i)
1900 this._paintLine(lineRows[i], highlight[lineRows[i].lineNumber]);
1902 this.endDomUpdates();
1906 * @param {Object.<number, Array.<WebInspector.TextEditorMainPanel.LineOverlayHighlight>>} highlight
1907 * @param {Array.<Element>} lineRows
1908 * @param {WebInspector.TextEditorMainPanel.HighlightDescriptor} highlightDescriptor
1910 _measureHighlightDescriptor: function(highlight, lineRows, highlightDescriptor)
1912 var rowsToMeasure = [];
1913 for(var i = 0; i < lineRows.length; ++i) {
1914 var lineRow = lineRows[i];
1915 var line = this._textModel.line(lineRow.lineNumber);
1916 var ranges = highlightDescriptor.rangesForLine(lineRow.lineNumber, line);
1917 if (ranges.length === 0)
1920 this._renderRanges(lineRow, line, ranges);
1921 rowsToMeasure.push(lineRow);
1924 for(var i = 0; i < rowsToMeasure.length; ++i) {
1925 var lineRow = rowsToMeasure[i];
1926 var lineNumber = lineRow.lineNumber;
1927 var metrics = this._measureSpans(lineRow);
1929 if (!highlight[lineNumber])
1930 highlight[lineNumber] = [];
1932 highlight[lineNumber].push(new WebInspector.TextEditorMainPanel.LineOverlayHighlight(metrics, highlightDescriptor.cssClass()));
1937 * @param {Element} lineRow
1938 * @return {Array.<WebInspector.TextEditorMainPanel.ElementMetrics>}
1940 _measureSpans: function(lineRow)
1942 var spans = lineRow.getElementsByTagName("span");
1944 for(var i = 0; i < spans.length; ++i)
1945 metrics.push(new WebInspector.TextEditorMainPanel.ElementMetrics(spans[i]));
1950 * @param {Element} lineRow
1951 * @param {WebInspector.TextEditorMainPanel.LineOverlayHighlight} highlight
1953 _appendOverlayHighlight: function(lineRow, highlight)
1955 var metrics = highlight.metrics;
1956 var cssClass = highlight.cssClass;
1957 for(var i = 0; i < metrics.length; ++i) {
1958 var highlightSpan = document.createElement("span");
1959 highlightSpan._isOverlayHighlightElement = true;
1960 highlightSpan.addStyleClass(cssClass);
1961 highlightSpan.style.left = metrics[i].left + "px";
1962 highlightSpan.style.width = metrics[i].width + "px";
1963 highlightSpan.style.height = metrics[i].height + "px";
1964 highlightSpan.addStyleClass("text-editor-overlay-highlight");
1965 lineRow.insertBefore(highlightSpan, lineRow.decorationsElement);
1970 * @param {Element} lineRow
1971 * @param {string} line
1972 * @param {Array.<{startColumn: number, endColumn: number, token: ?string}>} ranges
1974 _renderRanges: function(lineRow, line, ranges)
1976 var decorationsElement = lineRow.decorationsElement;
1978 if (!decorationsElement)
1979 lineRow.removeChildren();
1982 var child = lineRow.firstChild;
1983 if (!child || child === decorationsElement)
1985 lineRow.removeChild(child);
1990 lineRow.insertBefore(document.createElement("br"), decorationsElement);
1992 var plainTextStart = 0;
1993 for(var i = 0; i < ranges.length; i++) {
1994 var rangeStart = ranges[i].startColumn;
1995 var rangeEnd = ranges[i].endColumn;
1996 var cssClass = ranges[i].token ? "webkit-" + ranges[i].token : "";
1998 if (plainTextStart < rangeStart) {
1999 this._insertTextNodeBefore(lineRow, decorationsElement, line.substring(plainTextStart, rangeStart));
2001 this._insertSpanBefore(lineRow, decorationsElement, line.substring(rangeStart, rangeEnd + 1), cssClass);
2002 plainTextStart = rangeEnd + 1;
2004 if (plainTextStart < line.length) {
2005 this._insertTextNodeBefore(lineRow, decorationsElement, line.substring(plainTextStart, line.length));
2010 * @param {Element} lineRow
2011 * @param {Array.<WebInspector.TextEditorMainPanel.LineOverlayHighlight>} overlayHighlight
2013 _paintLine: function(lineRow, overlayHighlight)
2015 var lineNumber = lineRow.lineNumber;
2017 this.beginDomUpdates();
2019 var syntaxHighlight = this._textModel.getAttribute(lineNumber, "highlight");
2020 if (!syntaxHighlight)
2023 var line = this._textModel.line(lineNumber);
2024 var ranges = syntaxHighlight.ranges;
2025 this._renderRanges(lineRow, line, ranges);
2027 if (overlayHighlight)
2028 for(var i = 0; i < overlayHighlight.length; ++i)
2029 this._appendOverlayHighlight(lineRow, overlayHighlight[i]);
2031 if (this._rangeToMark && this._rangeToMark.startLine === lineNumber)
2032 this._markedRangeElement = WebInspector.highlightSearchResult(lineRow, this._rangeToMark.startColumn, this._rangeToMark.endColumn - this._rangeToMark.startColumn);
2033 this.endDomUpdates();
2038 * @param {Element} lineRow
2040 _releaseLinesHighlight: function(lineRow)
2044 if ("spans" in lineRow) {
2045 var spans = lineRow.spans;
2046 for (var j = 0; j < spans.length; ++j)
2047 this._cachedSpans.push(spans[j]);
2048 delete lineRow.spans;
2050 if ("textNodes" in lineRow) {
2051 var textNodes = lineRow.textNodes;
2052 for (var j = 0; j < textNodes.length; ++j)
2053 this._cachedTextNodes.push(textNodes[j]);
2054 delete lineRow.textNodes;
2056 this._cachedRows.push(lineRow);
2060 * @param {?Node=} lastUndamagedLineRow
2061 * @return {WebInspector.TextRange}
2063 selection: function(lastUndamagedLineRow)
2065 var selection = window.getSelection();
2066 if (!selection.rangeCount)
2068 // Selection may be outside of the editor.
2069 if (!this._container.isAncestor(selection.anchorNode) || !this._container.isAncestor(selection.focusNode))
2071 var start = this._selectionToPosition(selection.anchorNode, selection.anchorOffset, lastUndamagedLineRow);
2072 var end = selection.isCollapsed ? start : this._selectionToPosition(selection.focusNode, selection.focusOffset, lastUndamagedLineRow);
2073 return new WebInspector.TextRange(start.line, start.column, end.line, end.column);
2076 lastSelection: function()
2078 return this._lastSelection;
2082 * @param {boolean=} scrollIntoView
2084 _restoreSelection: function(range, scrollIntoView)
2089 var start = this._positionToSelection(range.startLine, range.startColumn);
2090 var end = range.isEmpty() ? start : this._positionToSelection(range.endLine, range.endColumn);
2091 window.getSelection().setBaseAndExtent(start.container, start.offset, end.container, end.offset);
2093 if (scrollIntoView) {
2094 for (var node = end.container; node; node = node.parentElement) {
2095 if (node.scrollIntoViewIfNeeded) {
2096 node.scrollIntoViewIfNeeded();
2101 this._lastSelection = range;
2105 * @param {Node} container
2106 * @param {number} offset
2107 * @param {?Node=} lastUndamagedLineRow
2108 * @return {{line: number, column: number}}
2110 _selectionToPosition: function(container, offset, lastUndamagedLineRow)
2112 if (container === this._container && offset === 0)
2113 return { line: 0, column: 0 };
2114 if (container === this._container && offset === 1)
2115 return { line: this._textModel.linesCount - 1, column: this._textModel.lineLength(this._textModel.linesCount - 1) };
2117 // This method can be called on the damaged DOM (when DOM does not match model).
2118 // We need to start counting lines from the first undamaged line if it is given.
2123 if (lastUndamagedLineRow === null) {
2124 // Last undamaged row is given, but is null - force traverse from the beginning
2125 node = this._container.firstChild;
2126 scopeNode = this._container;
2129 var lineRow = this._enclosingLineRowOrSelf(container);
2130 if (!lastUndamagedLineRow || (typeof lineRow.lineNumber === "number" && lineRow.lineNumber <= lastUndamagedLineRow.lineNumber)) {
2131 // DOM is consistent (or we belong to the first damaged row)- lookup the row we belong to and start with it.
2134 lineNumber = node.lineNumber;
2136 // Start with the node following undamaged row. It corresponds to lineNumber + 1.
2137 node = lastUndamagedLineRow.nextSibling;
2138 scopeNode = this._container;
2139 lineNumber = lastUndamagedLineRow.lineNumber + 1;
2143 // Fast return the line start.
2144 if (container === node && offset === 0)
2145 return { line: lineNumber, column: 0 };
2147 // Traverse text and increment lineNumber / column.
2148 for (; node && node !== container; node = node.traverseNextNode(scopeNode)) {
2149 if (node.nodeName.toLowerCase() === "br") {
2152 } else if (node.nodeType === Node.TEXT_NODE) {
2153 var text = node.textContent;
2154 for (var i = 0; i < text.length; ++i) {
2155 if (text.charAt(i) === "\n") {
2164 // We reached our container node, traverse within itself until we reach given offset.
2165 if (node === container && offset) {
2166 var text = node.textContent;
2167 // In case offset == 1 and lineRow is a chunk div, we need to traverse it all.
2168 var textOffset = (node._chunk && offset === 1) ? text.length : offset;
2169 for (var i = 0; i < textOffset; ++i) {
2170 if (text.charAt(i) === "\n") {
2177 return { line: lineNumber, column: column };
2181 * @param {number} line
2182 * @param {number} column
2183 * @return {{container: Element, offset: number}}
2185 _positionToSelection: function(line, column)
2187 var chunk = this.chunkForLine(line);
2188 // One-lined collapsed chunks may still stay highlighted.
2189 var lineRow = chunk.linesCount === 1 ? chunk.element : chunk.expandedLineRow(line);
2191 var rangeBoundary = lineRow.rangeBoundaryForOffset(column);
2193 var offset = column;
2194 for (var i = chunk.startLine; i < line && i < this._textModel.linesCount; ++i)
2195 offset += this._textModel.lineLength(i) + 1; // \n
2196 lineRow = chunk.element;
2197 if (lineRow.firstChild)
2198 var rangeBoundary = { container: lineRow.firstChild, offset: offset };
2200 var rangeBoundary = { container: lineRow, offset: 0 };
2202 return rangeBoundary;
2206 * @param {Node} element
2209 _enclosingLineRowOrSelf: function(element)
2211 var lineRow = element.enclosingNodeOrSelfWithClass("webkit-line-content");
2215 for (lineRow = element; lineRow; lineRow = lineRow.parentElement) {
2216 if (lineRow.parentElement === this._container)
2223 * @param {Element} element
2224 * @param {Element} oldChild
2225 * @param {string} content
2226 * @param {string} className
2228 _insertSpanBefore: function(element, oldChild, content, className)
2230 if (className === "html-resource-link" || className === "html-external-link") {
2231 element.insertBefore(this._createLink(content, className === "html-external-link"), oldChild);
2235 var span = this._cachedSpans.pop() || document.createElement("span");
2236 span.className = className;
2237 if (WebInspector.FALSE) // For paint debugging.
2238 span.addStyleClass("debug-fadeout");
2239 span.textContent = content;
2240 element.insertBefore(span, oldChild);
2241 if (!("spans" in element))
2243 element.spans.push(span);
2247 * @param {Element} element
2248 * @param {Element} oldChild
2249 * @param {string} text
2251 _insertTextNodeBefore: function(element, oldChild, text)
2253 var textNode = this._cachedTextNodes.pop();
2255 textNode.nodeValue = text;
2257 textNode = document.createTextNode(text);
2258 element.insertBefore(textNode, oldChild);
2259 if (!("textNodes" in element))
2260 element.textNodes = [];
2261 element.textNodes.push(textNode);
2265 * @param {string} content
2266 * @param {boolean} isExternal
2269 _createLink: function(content, isExternal)
2271 var quote = content.charAt(0);
2272 if (content.length > 1 && (quote === "\"" || quote === "'"))
2273 content = content.substring(1, content.length - 1);
2277 var span = document.createElement("span");
2278 span.className = "webkit-html-attribute-value";
2280 span.appendChild(document.createTextNode(quote));
2281 span.appendChild(this._delegate.createLink(content, isExternal));
2283 span.appendChild(document.createTextNode(quote));
2288 * @param {Array.<WebKitMutation>} mutations
2290 _handleMutations: function(mutations)
2292 if (this._readOnly) {
2293 delete this._keyDownCode;
2297 // Annihilate noop BR addition + removal that takes place upon line removal.
2298 var filteredMutations = mutations.slice();
2299 var addedBRs = new Map();
2300 for (var i = 0; i < mutations.length; ++i) {
2301 var mutation = mutations[i];
2302 if (mutation.type !== "childList")
2304 if (mutation.addedNodes.length === 1 && mutation.addedNodes[0].nodeName === "BR")
2305 addedBRs.put(mutation.addedNodes[0], mutation);
2306 else if (mutation.removedNodes.length === 1 && mutation.removedNodes[0].nodeName === "BR") {
2307 var noopMutation = addedBRs.get(mutation.removedNodes[0]);
2309 filteredMutations.remove(mutation);
2310 filteredMutations.remove(noopMutation);
2316 for (var i = 0; i < filteredMutations.length; ++i) {
2317 var mutation = filteredMutations[i];
2318 var changedNodes = [];
2319 if (mutation.type === "childList" && mutation.addedNodes.length)
2320 changedNodes = Array.prototype.slice.call(mutation.addedNodes);
2321 else if (mutation.type === "childList" && mutation.removedNodes.length)
2322 changedNodes = Array.prototype.slice.call(mutation.removedNodes);
2323 changedNodes.push(mutation.target);
2325 for (var j = 0; j < changedNodes.length; ++j) {
2326 var lines = this._collectDirtyLines(mutation, changedNodes[j]);
2333 dirtyLines.start = Math.min(dirtyLines.start, lines.start);
2334 dirtyLines.end = Math.max(dirtyLines.end, lines.end);
2338 delete this._rangeToMark;
2339 this._applyDomUpdates(dirtyLines);
2342 this._assertDOMMatchesTextModel();
2344 delete this._keyDownCode;
2348 * @param {WebKitMutation} mutation
2349 * @param {Node} target
2352 _collectDirtyLines: function(mutation, target)
2354 var lineRow = this._enclosingLineRowOrSelf(target);
2358 if (lineRow.decorationsElement && lineRow.decorationsElement.isSelfOrAncestor(target)) {
2359 if (this._syncDecorationsForLineListener)
2360 this._syncDecorationsForLineListener(lineRow.lineNumber);
2364 if (typeof lineRow.lineNumber !== "number")
2367 var startLine = lineRow.lineNumber;
2368 var endLine = lineRow._chunk ? lineRow._chunk.endLine - 1 : lineRow.lineNumber;
2369 return { start: startLine, end: endLine };
2373 * @param {Object} dirtyLines
2375 _applyDomUpdates: function(dirtyLines)
2377 var lastUndamagedLineNumber = dirtyLines.start - 1; // Can be -1
2378 var firstUndamagedLineNumber = dirtyLines.end + 1; // Can be this._textModel.linesCount
2380 var lastUndamagedLineChunk = lastUndamagedLineNumber >= 0 ? this._textChunks[this.chunkNumberForLine(lastUndamagedLineNumber)] : null;
2381 var firstUndamagedLineChunk = firstUndamagedLineNumber < this._textModel.linesCount ? this._textChunks[this.chunkNumberForLine(firstUndamagedLineNumber)] : null;
2383 var collectLinesFromNode = lastUndamagedLineChunk ? lastUndamagedLineChunk.lineRowContainingLine(lastUndamagedLineNumber) : null;
2384 var collectLinesToNode = firstUndamagedLineChunk ? firstUndamagedLineChunk.lineRowContainingLine(firstUndamagedLineNumber) : null;
2385 var lines = this._collectLinesFromDOM(collectLinesFromNode, collectLinesToNode);
2387 var startLine = dirtyLines.start;
2388 var endLine = dirtyLines.end;
2390 var editInfo = this._guessEditRangeBasedOnSelection(startLine, endLine, lines);
2392 if (WebInspector.debugDefaultTextEditor)
2393 console.warn("Falling back to expensive edit");
2394 var range = new WebInspector.TextRange(startLine, 0, endLine, this._textModel.lineLength(endLine));
2395 if (!lines.length) {
2396 // Entire damaged area has collapsed. Replace everything between start and end lines with nothing.
2397 editInfo = new WebInspector.DefaultTextEditor.EditInfo(this._textModel.growRangeRight(range), "");
2399 editInfo = new WebInspector.DefaultTextEditor.EditInfo(range, lines.join("\n"));
2402 var selection = this.selection(collectLinesFromNode);
2404 // Unindent after block
2405 if (editInfo.text === "}" && editInfo.range.isEmpty() && selection.isEmpty() && !this._textModel.line(editInfo.range.endLine).trim()) {
2406 var offset = this._closingBlockOffset(editInfo.range);
2408 editInfo.range.startColumn = offset;
2409 selection.startColumn = offset + 1;
2410 selection.endColumn = offset + 1;
2414 this._textModel.editRange(editInfo.range, editInfo.text);
2415 this._restoreSelection(selection);
2419 * @param {number} startLine
2420 * @param {number} endLine
2421 * @param {Array.<string>} lines
2422 * @return {?WebInspector.DefaultTextEditor.EditInfo}
2424 _guessEditRangeBasedOnSelection: function(startLine, endLine, lines)
2426 // Analyze input data
2427 var textInputData = this._textInputData;
2428 delete this._textInputData;
2429 var isBackspace = this._keyDownCode === WebInspector.KeyboardShortcut.Keys.Backspace.code;
2430 var isDelete = this._keyDownCode === WebInspector.KeyboardShortcut.Keys.Delete.code;
2432 if (!textInputData && (isDelete || isBackspace))
2435 // Return if there is no input data or selection
2436 if (typeof textInputData === "undefined" || !this._lastSelection)
2439 // Adjust selection based on the keyboard actions (grow for backspace, etc.).
2440 textInputData = textInputData || "";
2441 var range = this._lastSelection.normalize();
2442 if (isBackspace && range.isEmpty())
2443 range = this._textModel.growRangeLeft(range);
2444 else if (isDelete && range.isEmpty())
2445 range = this._textModel.growRangeRight(range);
2447 // Test that selection intersects damaged lines
2448 if (startLine > range.endLine || endLine < range.startLine)
2451 var replacementLineCount = textInputData.split("\n").length - 1;
2452 var lineCountDelta = replacementLineCount - range.linesCount;
2453 if (startLine + lines.length - endLine - 1 !== lineCountDelta)
2456 // Clone text model of the size that fits both: selection before edit and the damaged lines after edit.
2457 var cloneFromLine = Math.min(range.startLine, startLine);
2458 var postLastLine = startLine + lines.length + lineCountDelta;
2459 var cloneToLine = Math.min(Math.max(postLastLine, range.endLine) + 1, this._textModel.linesCount);
2460 var domModel = this._textModel.slice(cloneFromLine, cloneToLine);
2461 domModel.editRange(range.shift(-cloneFromLine), textInputData);
2463 // Then we'll test if this new model matches the DOM lines.
2464 for (var i = 0; i < lines.length; ++i) {
2465 if (domModel.line(i + startLine - cloneFromLine) !== lines[i])
2468 return new WebInspector.DefaultTextEditor.EditInfo(range, textInputData);
2471 _assertDOMMatchesTextModel: function()
2473 if (!WebInspector.debugDefaultTextEditor)
2476 console.assert(this.element.innerText === this._textModel.text() + "\n", "DOM does not match model.");
2477 for (var lineRow = this._container.firstChild; lineRow; lineRow = lineRow.nextSibling) {
2478 var lineNumber = lineRow.lineNumber;
2479 if (typeof lineNumber !== "number") {
2480 console.warn("No line number on line row");
2483 if (lineRow._chunk) {
2484 var chunk = lineRow._chunk;
2485 console.assert(lineNumber === chunk.startLine);
2486 var chunkText = this._textModel.copyRange(new WebInspector.TextRange(chunk.startLine, 0, chunk.endLine - 1, this._textModel.lineLength(chunk.endLine - 1)));
2487 if (chunkText !== lineRow.textContent)
2488 console.warn("Chunk is not matching: %d %O", lineNumber, lineRow);
2489 } else if (this._textModel.line(lineNumber) !== lineRow.textContent)
2490 console.warn("Line is not matching: %d %O", lineNumber, lineRow);
2495 * @param {WebInspector.TextRange} oldRange
2498 _closingBlockOffset: function(oldRange)
2500 var leftBrace = this._braceMatcher.findLeftCandidate(oldRange.startLine, oldRange.startColumn);
2501 if (!leftBrace || leftBrace.token !== "block-start")
2503 var lineContent = this._textModel.line(leftBrace.lineNumber);
2504 return lineContent.length - lineContent.trimLeft().length;
2508 * @param {WebInspector.TextRange} oldRange
2509 * @param {WebInspector.TextRange} newRange
2511 textChanged: function(oldRange, newRange)
2513 this.beginDomUpdates();
2514 this._removeDecorationsInRange(oldRange);
2515 this._updateChunksForRanges(oldRange, newRange);
2516 this._updateHighlightsForRange(newRange);
2517 this.endDomUpdates();
2521 * @param {WebInspector.TextRange} range
2523 _removeDecorationsInRange: function(range)
2525 for (var i = this.chunkNumberForLine(range.startLine); i < this._textChunks.length; ++i) {
2526 var chunk = this._textChunks[i];
2527 if (chunk.startLine > range.endLine)
2529 chunk.removeAllDecorations();
2534 * @param {WebInspector.TextRange} oldRange
2535 * @param {WebInspector.TextRange} newRange
2537 _updateChunksForRanges: function(oldRange, newRange)
2539 var firstDamagedChunkNumber = this.chunkNumberForLine(oldRange.startLine);
2540 var lastDamagedChunkNumber = firstDamagedChunkNumber;
2541 while (lastDamagedChunkNumber + 1 < this._textChunks.length) {
2542 if (this._textChunks[lastDamagedChunkNumber + 1].startLine > oldRange.endLine)
2544 ++lastDamagedChunkNumber;
2547 var firstDamagedChunk = this._textChunks[firstDamagedChunkNumber];
2548 var lastDamagedChunk = this._textChunks[lastDamagedChunkNumber];
2550 var linesDiff = newRange.linesCount - oldRange.linesCount;
2552 // First, detect chunks that have not been modified and simply shift them.
2554 for (var chunkNumber = lastDamagedChunkNumber + 1; chunkNumber < this._textChunks.length; ++chunkNumber)
2555 this._textChunks[chunkNumber].startLine += linesDiff;
2558 // Remove damaged chunks from DOM and from textChunks model.
2559 var lastUndamagedChunk = firstDamagedChunkNumber > 0 ? this._textChunks[firstDamagedChunkNumber - 1] : null;
2560 var firstUndamagedChunk = lastDamagedChunkNumber + 1 < this._textChunks.length ? this._textChunks[lastDamagedChunkNumber + 1] : null;
2562 var removeDOMFromNode = lastUndamagedChunk ? lastUndamagedChunk.lastElement().nextSibling : this._container.firstChild;
2563 var removeDOMToNode = firstUndamagedChunk ? firstUndamagedChunk.firstElement() : null;
2565 // Fast case - patch single expanded chunk that did not grow / shrink during edit.
2566 if (!linesDiff && firstDamagedChunk === lastDamagedChunk && firstDamagedChunk._expandedLineRows) {
2567 var lastUndamagedLineRow = lastDamagedChunk.expandedLineRow(oldRange.startLine - 1);
2568 var firstUndamagedLineRow = firstDamagedChunk.expandedLineRow(oldRange.endLine + 1);
2569 var localRemoveDOMFromNode = lastUndamagedLineRow ? lastUndamagedLineRow.nextSibling : removeDOMFromNode;
2570 var localRemoveDOMToNode = firstUndamagedLineRow || removeDOMToNode;
2571 removeSubsequentNodes(localRemoveDOMFromNode, localRemoveDOMToNode);
2572 for (var i = newRange.startLine; i < newRange.endLine + 1; ++i) {
2573 var row = firstDamagedChunk._createRow(i);
2574 firstDamagedChunk._expandedLineRows[i - firstDamagedChunk.startLine] = row;
2575 this._container.insertBefore(row, localRemoveDOMToNode);
2577 firstDamagedChunk.updateCollapsedLineRow();
2578 this._assertDOMMatchesTextModel();
2582 removeSubsequentNodes(removeDOMFromNode, removeDOMToNode);
2583 this._textChunks.splice(firstDamagedChunkNumber, lastDamagedChunkNumber - firstDamagedChunkNumber + 1);
2585 // Compute damaged chunks span
2586 var startLine = firstDamagedChunk.startLine;
2587 var endLine = lastDamagedChunk.endLine + linesDiff;
2588 var lineSpan = endLine - startLine;
2590 // Re-create chunks for damaged area.
2591 var insertionIndex = firstDamagedChunkNumber;
2592 var chunkSize = Math.ceil(lineSpan / Math.ceil(lineSpan / this._defaultChunkSize));
2594 for (var i = startLine; i < endLine; i += chunkSize) {
2595 var chunk = this.createNewChunk(i, Math.min(endLine, i + chunkSize));
2596 this._textChunks.splice(insertionIndex++, 0, chunk);
2597 this._container.insertBefore(chunk.element, removeDOMToNode);
2600 this._assertDOMMatchesTextModel();
2604 * @param {WebInspector.TextRange} range
2606 _updateHighlightsForRange: function(range)
2608 var visibleFrom = this.scrollTop();
2609 var visibleTo = visibleFrom + this.clientHeight();
2611 var result = this.findVisibleChunks(visibleFrom, visibleTo);
2612 var chunk = this._textChunks[result.end - 1];
2613 var lastVisibleLine = chunk.startLine + chunk.linesCount;
2615 lastVisibleLine = Math.max(lastVisibleLine, range.endLine + 1);
2616 lastVisibleLine = Math.min(lastVisibleLine, this._textModel.linesCount);
2618 var updated = this._highlighter.updateHighlight(range.startLine, lastVisibleLine);
2620 // Highlights for the chunks below are invalid, so just collapse them.
2621 for (var i = this.chunkNumberForLine(range.startLine); i < this._textChunks.length; ++i)
2622 this._textChunks[i].collapse();
2629 * @param {Node} from
2631 * @return {Array.<string>}
2633 _collectLinesFromDOM: function(from, to)
2635 var textContents = [];
2636 var hasContent = false;
2637 for (var node = from ? from.nextSibling : this._container; node && node !== to; node = node.traverseNextNode(this._container)) {
2638 // Skip all children of the decoration container and overlay highlight spans.
2639 while (node && node !== to && (node._isDecorationsElement || node._isOverlayHighlightElement))
2640 node = node.nextSibling;
2641 if (!node || node === to)
2645 if (node.nodeName.toLowerCase() === "br")
2646 textContents.push("\n");
2647 else if (node.nodeType === Node.TEXT_NODE)
2648 textContents.push(node.textContent);
2653 var textContent = textContents.join("");
2654 // The last \n (if any) does not "count" in a DIV.
2655 textContent = textContent.replace(/\n$/, "");
2657 return textContent.split("\n");
2661 * @param {Event} event
2663 _handleSelectionChange: function(event)
2665 var textRange = this.selection();
2667 this._lastSelection = textRange;
2669 this._tokenHighlighter.handleSelectionChange(textRange);
2670 this._braceHighlighter.handleSelectionChange(textRange);
2671 this._delegate.selectionChanged(textRange);
2675 * @param {Event} event
2677 _handleTextInput: function(event)
2679 this._textInputData = event.data;
2683 * @param {number} shortcutKey
2684 * @param {Event} event
2686 handleKeyDown: function(shortcutKey, event)
2688 var handler = this._shortcuts[shortcutKey];
2689 if (handler && handler()) {
2690 event.consume(true);
2694 this._keyDownCode = event.keyCode;
2698 * @param {Event} event
2700 _handleCut: function(event)
2702 this._keyDownCode = WebInspector.KeyboardShortcut.Keys.Delete.code;
2706 * @param {number} scrollTop
2707 * @param {number} clientHeight
2708 * @param {number} chunkSize
2710 overrideViewportForTest: function(scrollTop, clientHeight, chunkSize)
2712 this._scrollTopOverrideForTest = scrollTop;
2713 this._clientHeightOverrideForTest = clientHeight;
2714 this._defaultChunkSize = chunkSize;
2717 __proto__: WebInspector.TextEditorChunkedPanel.prototype
2723 WebInspector.TextEditorMainPanel.HighlightDescriptor = function() { }
2725 WebInspector.TextEditorMainPanel.HighlightDescriptor.prototype = {
2727 * @param {number} lineNumber
2728 * @param {string} line
2731 affectsLine: function(lineNumber, line) { return false; },
2734 * @param {number} lineNumber
2735 * @param {string} line
2736 * @return {Array.<{startColumn: number, endColumn: number}>}
2738 rangesForLine: function(lineNumber, line) { return []; },
2743 cssClass: function() { return ""; },
2748 * @implements {WebInspector.TextEditorMainPanel.HighlightDescriptor}
2750 WebInspector.TextEditorMainPanel.RegexHighlightDescriptor = function(regex, cssClass)
2752 this._cssClass = cssClass;
2753 this._regex = regex;
2756 WebInspector.TextEditorMainPanel.RegexHighlightDescriptor.prototype = {
2758 * @param {number} lineNumber
2759 * @param {string} line
2762 affectsLine: function(lineNumber, line)
2764 this._regex.lastIndex = 0;
2765 return this._regex.test(line);
2769 * @param {number} lineNumber
2770 * @param {string} line
2771 * @return {Array.<{startColumn: number, endColumn: number}>}
2773 rangesForLine: function(lineNumber, line)
2777 this._regex.lastIndex = 0;
2778 while (regexResult = this._regex.exec(line)) {
2780 startColumn: regexResult.index,
2781 endColumn: regexResult.index + regexResult[0].length - 1
2790 cssClass: function()
2792 return this._cssClass;
2798 * @implements {WebInspector.TextEditorMainPanel.HighlightDescriptor}
2799 * @param {WebInspector.TextRange} range
2800 * @param {string} cssClass
2802 WebInspector.TextEditorMainPanel.RangeHighlightDescriptor = function(range, cssClass)
2804 this._cssClass = cssClass;
2805 this._range = range;
2808 WebInspector.TextEditorMainPanel.RangeHighlightDescriptor.prototype = {
2810 * @param {number} lineNumber
2811 * @param {string} line
2814 affectsLine: function(lineNumber, line)
2816 return this._range.startLine <= lineNumber && lineNumber <= this._range.endLine && line.length > 0;
2820 * @param {number} lineNumber
2821 * @param {string} line
2822 * @return {Array.<{startColumn: number, endColumn: number}>}
2824 rangesForLine: function(lineNumber, line)
2826 if (!this.affectsLine(lineNumber, line))
2829 var startColumn = lineNumber === this._range.startLine ? this._range.startColumn : 0;
2830 var endColumn = lineNumber === this._range.endLine ? Math.min(this._range.endColumn, line.length) : line.length;
2832 startColumn: startColumn,
2833 endColumn: endColumn
2840 cssClass: function()
2842 return this._cssClass;
2848 * @param {Element} element
2850 WebInspector.TextEditorMainPanel.ElementMetrics = function(element)
2852 this.width = element.offsetWidth;
2853 this.height = element.offsetHeight;
2854 this.left = element.offsetLeft;
2859 * @param {Array.<WebInspector.TextEditorMainPanel.ElementMetrics>} metrics
2860 * @param {string} cssClass
2862 WebInspector.TextEditorMainPanel.LineOverlayHighlight = function(metrics, cssClass)
2864 this.metrics = metrics;
2865 this.cssClass = cssClass;
2870 * @param {WebInspector.TextEditorChunkedPanel} chunkedPanel
2871 * @param {number} startLine
2872 * @param {number} endLine
2874 WebInspector.TextEditorMainChunk = function(chunkedPanel, startLine, endLine)
2876 this._chunkedPanel = chunkedPanel;
2877 this._textModel = chunkedPanel._textModel;
2879 this.element = document.createElement("div");
2880 this.element.lineNumber = startLine;
2881 this.element.className = "webkit-line-content";
2882 this.element._chunk = this;
2884 this._startLine = startLine;
2885 endLine = Math.min(this._textModel.linesCount, endLine);
2886 this.linesCount = endLine - startLine;
2888 this._expanded = false;
2890 this.updateCollapsedLineRow();
2893 WebInspector.TextEditorMainChunk.prototype = {
2895 * @param {Element|string} decoration
2897 addDecoration: function(decoration)
2899 this._chunkedPanel.beginDomUpdates();
2900 if (typeof decoration === "string")
2901 this.element.addStyleClass(decoration);
2903 if (!this.element.decorationsElement) {
2904 this.element.decorationsElement = document.createElement("div");
2905 this.element.decorationsElement.className = "webkit-line-decorations";
2906 this.element.decorationsElement._isDecorationsElement = true;
2907 this.element.appendChild(this.element.decorationsElement);
2909 this.element.decorationsElement.appendChild(decoration);
2911 this._chunkedPanel.endDomUpdates();
2915 * @param {string|Element} decoration
2917 removeDecoration: function(decoration)
2919 this._chunkedPanel.beginDomUpdates();
2920 if (typeof decoration === "string")
2921 this.element.removeStyleClass(decoration);
2922 else if (this.element.decorationsElement)
2923 this.element.decorationsElement.removeChild(decoration);
2924 this._chunkedPanel.endDomUpdates();
2927 removeAllDecorations: function()
2929 this._chunkedPanel.beginDomUpdates();
2930 this.element.className = "webkit-line-content";
2931 if (this.element.decorationsElement) {
2932 if (this.element.decorationsElement.parentElement)
2933 this.element.removeChild(this.element.decorationsElement);
2934 delete this.element.decorationsElement;
2936 this._chunkedPanel.endDomUpdates();
2942 isDecorated: function()
2944 return this.element.className !== "webkit-line-content" || !!(this.element.decorationsElement && this.element.decorationsElement.firstChild);
2952 return this._startLine;
2960 return this._startLine + this.linesCount;
2963 set startLine(startLine)
2965 this._startLine = startLine;
2966 this.element.lineNumber = startLine;
2967 if (this._expandedLineRows) {
2968 for (var i = 0; i < this._expandedLineRows.length; ++i)
2969 this._expandedLineRows[i].lineNumber = startLine + i;
2976 expanded: function()
2978 return this._expanded;
2986 this._expanded = true;
2988 if (this.linesCount === 1) {
2989 this._chunkedPanel._paintLines(this.startLine, this.startLine + 1);
2993 this._chunkedPanel.beginDomUpdates();
2995 this._expandedLineRows = [];
2996 var parentElement = this.element.parentElement;
2997 for (var i = this.startLine; i < this.startLine + this.linesCount; ++i) {
2998 var lineRow = this._createRow(i);
2999 parentElement.insertBefore(lineRow, this.element);
3000 this._expandedLineRows.push(lineRow);
3002 parentElement.removeChild(this.element);
3003 this._chunkedPanel._paintLines(this.startLine, this.startLine + this.linesCount);
3005 this._chunkedPanel.endDomUpdates();
3008 collapse: function()
3010 if (!this._expanded)
3013 this._expanded = false;
3014 if (this.linesCount === 1)
3017 this._chunkedPanel.beginDomUpdates();
3019 var elementInserted = false;
3020 for (var i = 0; i < this._expandedLineRows.length; ++i) {
3021 var lineRow = this._expandedLineRows[i];
3022 var parentElement = lineRow.parentElement;
3023 if (parentElement) {
3024 if (!elementInserted) {
3025 elementInserted = true;
3026 parentElement.insertBefore(this.element, lineRow);
3028 parentElement.removeChild(lineRow);
3030 this._chunkedPanel._releaseLinesHighlight(lineRow);
3032 delete this._expandedLineRows;
3034 this._chunkedPanel.endDomUpdates();
3042 if (!this._expandedLineRows)
3043 return this._chunkedPanel.totalHeight(this.element);
3044 return this._chunkedPanel.totalHeight(this._expandedLineRows[0], this._expandedLineRows[this._expandedLineRows.length - 1]);
3052 return (this._expandedLineRows && this._expandedLineRows.length) ? this._expandedLineRows[0].offsetTop : this.element.offsetTop;
3056 * @param {number} lineNumber
3059 _createRow: function(lineNumber)
3061 var lineRow = this._chunkedPanel._cachedRows.pop() || document.createElement("div");
3062 lineRow.lineNumber = lineNumber;
3063 lineRow.className = "webkit-line-content";
3064 lineRow.textContent = this._textModel.line(lineNumber);
3065 if (!lineRow.textContent)
3066 lineRow.appendChild(document.createElement("br"));
3071 * Called on potentially damaged / inconsistent chunk
3072 * @param {number} lineNumber
3075 lineRowContainingLine: function(lineNumber)
3077 if (!this._expanded)
3078 return this.element;
3079 return this.expandedLineRow(lineNumber);
3083 * @param {number} lineNumber
3086 expandedLineRow: function(lineNumber)
3088 if (!this._expanded || lineNumber < this.startLine || lineNumber >= this.startLine + this.linesCount)
3090 if (!this._expandedLineRows)
3091 return this.element;
3092 return this._expandedLineRows[lineNumber - this.startLine];
3095 updateCollapsedLineRow: function()
3097 if (this.linesCount === 1 && this._expanded)
3101 for (var i = this.startLine; i < this.startLine + this.linesCount; ++i)
3102 lines.push(this._textModel.line(i));
3104 if (WebInspector.FALSE)
3105 console.log("Rebuilding chunk with " + lines.length + " lines");
3107 this.element.removeChildren();
3108 this.element.textContent = lines.join("\n");
3109 // The last empty line will get swallowed otherwise.
3110 if (!lines[lines.length - 1])
3111 this.element.appendChild(document.createElement("br"));
3114 firstElement: function()
3116 return this._expandedLineRows ? this._expandedLineRows[0] : this.element;
3122 lastElement: function()
3124 return this._expandedLineRows ? this._expandedLineRows[this._expandedLineRows.length - 1] : this.element;
3130 * @param {WebInspector.TextEditorMainPanel} mainPanel
3131 * @param {WebInspector.TextEditorModel} textModel
3133 WebInspector.TextEditorMainPanel.TokenHighlighter = function(mainPanel, textModel)
3135 this._mainPanel = mainPanel;
3136 this._textModel = textModel;
3139 WebInspector.TextEditorMainPanel.TokenHighlighter._NonWordCharRegex = /[^a-zA-Z0-9_]/;
3140 WebInspector.TextEditorMainPanel.TokenHighlighter._WordRegex = /^[a-zA-Z0-9_]+$/;
3142 WebInspector.TextEditorMainPanel.TokenHighlighter.prototype = {
3144 * @param {WebInspector.TextRange} range
3146 handleSelectionChange: function(range)
3149 this._removeHighlight();
3153 if (range.startLine !== range.endLine) {
3154 this._removeHighlight();
3158 range = range.normalize();
3159 var selectedText = this._textModel.copyRange(range);
3160 if (selectedText === this._selectedWord)
3163 if (selectedText === "") {
3164 this._removeHighlight();
3168 if (this._isWord(range, selectedText))
3169 this._highlight(selectedText);
3171 this._removeHighlight();
3175 * @param {string} word
3177 _regexString: function(word)
3179 return "\\b" + word + "\\b";
3183 * @param {string} selectedWord
3185 _highlight: function(selectedWord)
3187 this._removeHighlight();
3188 this._selectedWord = selectedWord;
3189 this._highlightDescriptor = this._mainPanel.highlightRegex(this._regexString(selectedWord), "text-editor-token-highlight")
3192 _removeHighlight: function()
3194 if (this._selectedWord) {
3195 this._mainPanel.removeHighlight(this._highlightDescriptor);
3196 delete this._selectedWord;
3197 delete this._highlightDescriptor;
3202 * @param {WebInspector.TextRange} range
3203 * @param {string} selectedText
3206 _isWord: function(range, selectedText)
3208 const NonWordChar = WebInspector.TextEditorMainPanel.TokenHighlighter._NonWordCharRegex;
3209 const WordRegex = WebInspector.TextEditorMainPanel.TokenHighlighter._WordRegex;
3210 var line = this._textModel.line(range.startLine);
3211 var leftBound = range.startColumn === 0 || NonWordChar.test(line.charAt(range.startColumn - 1));
3212 var rightBound = range.endColumn === line.length || NonWordChar.test(line.charAt(range.endColumn));
3213 return leftBound && rightBound && WordRegex.test(selectedText);
3219 * @param {WebInspector.TextEditorModel} textModel
3220 * @param {WebInspector.TextEditor} textEditor
3222 WebInspector.DefaultTextEditor.WordMovementController = function(textEditor, textModel)
3224 this._textModel = textModel;
3225 this._textEditor = textEditor;
3228 WebInspector.DefaultTextEditor.WordMovementController.prototype = {
3231 * @param {Object.<number, function()>} shortcuts
3233 _registerShortcuts: function(shortcuts)
3235 var keys = WebInspector.KeyboardShortcut.Keys;
3236 var modifiers = WebInspector.KeyboardShortcut.Modifiers;
3238 const wordJumpModifier = WebInspector.isMac() ? modifiers.Alt : modifiers.Ctrl;
3239 shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Backspace.code, wordJumpModifier)] = this._handleCtrlBackspace.bind(this);
3240 shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Left.code, wordJumpModifier)] = this._handleCtrlArrow.bind(this, "left");
3241 shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Right.code, wordJumpModifier)] = this._handleCtrlArrow.bind(this, "right");
3242 shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Left.code, modifiers.Shift | wordJumpModifier)] = this._handleCtrlShiftArrow.bind(this, "left");
3243 shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Right.code, modifiers.Shift | wordJumpModifier)] = this._handleCtrlShiftArrow.bind(this, "right");
3247 * @param {WebInspector.TextRange} selection
3248 * @param {string} direction
3249 * @return {WebInspector.TextRange}
3251 _rangeForCtrlArrowMove: function(selection, direction)
3254 * @param {string} char
3256 function isStopChar(char)
3258 return (char > " " && char < "0") ||
3259 (char > "9" && char < "A") ||
3260 (char > "Z" && char < "a") ||
3261 (char > "z" && char <= "~");
3265 * @param {string} char
3267 function isSpaceChar(char)
3269 return char === "\t" || char === "\r" || char === "\n" || char === " ";