| /* |
| * Copyright (C) 2011 Google Inc. All rights reserved. |
| * Copyright (C) 2010 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions are |
| * met: |
| * |
| * * Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * * Redistributions in binary form must reproduce the above |
| * copyright notice, this list of conditions and the following disclaimer |
| * in the documentation and/or other materials provided with the |
| * distribution. |
| * * Neither the name of Google Inc. nor the names of its |
| * contributors may be used to endorse or promote products derived from |
| * this software without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| /** |
| * @constructor |
| * @extends {WebInspector.View} |
| * @implements {WebInspector.TextEditor} |
| * @param {?string} url |
| * @param {WebInspector.TextEditorDelegate} delegate |
| */ |
| WebInspector.DefaultTextEditor = function(url, delegate) |
| { |
| WebInspector.View.call(this); |
| this._delegate = delegate; |
| this._url = url; |
| |
| this.registerRequiredCSS("textEditor.css"); |
| |
| this.element.className = "text-editor monospace"; |
| |
| // Prevent middle-click pasting in the editor unless it is explicitly enabled for certain component. |
| this.element.addEventListener("mouseup", preventDefaultOnMouseUp.bind(this), false); |
| function preventDefaultOnMouseUp(event) |
| { |
| if (event.button === 1) |
| event.consume(true); |
| } |
| |
| this._textModel = new WebInspector.TextEditorModel(); |
| this._textModel.addEventListener(WebInspector.TextEditorModel.Events.TextChanged, this._textChanged, this); |
| |
| var syncScrollListener = this._syncScroll.bind(this); |
| var syncDecorationsForLineListener = this._syncDecorationsForLine.bind(this); |
| var syncLineHeightListener = this._syncLineHeight.bind(this); |
| this._mainPanel = new WebInspector.TextEditorMainPanel(this._delegate, this._textModel, url, syncScrollListener, syncDecorationsForLineListener); |
| this._gutterPanel = new WebInspector.TextEditorGutterPanel(this._textModel, syncDecorationsForLineListener, syncLineHeightListener); |
| |
| this._mainPanel.element.addEventListener("scroll", this._handleScrollChanged.bind(this), false); |
| |
| this._gutterPanel.element.addEventListener("mousedown", this._onMouseDown.bind(this), true); |
| |
| // Explicitly enable middle-click pasting in the editor main panel. |
| this._mainPanel.element.addEventListener("mouseup", consumeMouseUp.bind(this), false); |
| function consumeMouseUp(event) |
| { |
| if (event.button === 1) |
| event.consume(false); |
| } |
| |
| this.element.appendChild(this._mainPanel.element); |
| this.element.appendChild(this._gutterPanel.element); |
| |
| // Forward mouse wheel events from the unscrollable gutter to the main panel. |
| function forwardWheelEvent(event) |
| { |
| var clone = document.createEvent("WheelEvent"); |
| clone.initWebKitWheelEvent(event.wheelDeltaX, event.wheelDeltaY, |
| event.view, |
| event.screenX, event.screenY, |
| event.clientX, event.clientY, |
| event.ctrlKey, event.altKey, event.shiftKey, event.metaKey); |
| this._mainPanel.element.dispatchEvent(clone); |
| } |
| this._gutterPanel.element.addEventListener("mousewheel", forwardWheelEvent.bind(this), false); |
| |
| this.element.addEventListener("keydown", this._handleKeyDown.bind(this), false); |
| this.element.addEventListener("contextmenu", this._contextMenu.bind(this), true); |
| |
| this._wordMovementController = new WebInspector.DefaultTextEditor.WordMovementController(this, this._textModel); |
| this._registerShortcuts(); |
| } |
| |
| /** |
| * @constructor |
| * @param {WebInspector.TextRange} range |
| * @param {string} text |
| */ |
| WebInspector.DefaultTextEditor.EditInfo = function(range, text) |
| { |
| this.range = range; |
| this.text = text; |
| } |
| |
| WebInspector.DefaultTextEditor.prototype = { |
| /** |
| * @param {WebInspector.TextRange} range |
| * @return {string} |
| */ |
| copyRange: function(range) |
| { |
| return this._textModel.copyRange(range); |
| }, |
| |
| /** |
| * @param {string} regex |
| * @param {string} cssClass |
| * @return {Object} |
| */ |
| highlightRegex: function(regex, cssClass) |
| { |
| return this._mainPanel.highlightRegex(regex, cssClass); |
| }, |
| |
| /** |
| * @param {Object} highlightDescriptor |
| */ |
| removeHighlight: function(highlightDescriptor) |
| { |
| this._mainPanel.removeHighlight(highlightDescriptor); |
| }, |
| |
| /** |
| * @param {WebInspector.TextRange} range |
| * @param {string} cssClass |
| * @return {Object} |
| */ |
| highlightRange: function(range, cssClass) |
| { |
| return this._mainPanel.highlightRange(range, cssClass); |
| }, |
| |
| /** |
| * @param {string} mimeType |
| */ |
| set mimeType(mimeType) |
| { |
| this._mainPanel.mimeType = mimeType; |
| }, |
| |
| /** |
| * @param {boolean} readOnly |
| */ |
| setReadOnly: function(readOnly) |
| { |
| if (this._mainPanel.readOnly() === readOnly) |
| return; |
| this._mainPanel.setReadOnly(readOnly, this.isShowing()); |
| WebInspector.markBeingEdited(this.element, !readOnly); |
| }, |
| |
| /** |
| * @return {boolean} |
| */ |
| readOnly: function() |
| { |
| return this._mainPanel.readOnly(); |
| }, |
| |
| /** |
| * @return {Element} |
| */ |
| defaultFocusedElement: function() |
| { |
| return this._mainPanel.defaultFocusedElement(); |
| }, |
| |
| /** |
| * @param {number} lineNumber |
| */ |
| revealLine: function(lineNumber) |
| { |
| this._mainPanel.revealLine(lineNumber); |
| }, |
| |
| _onMouseDown: function(event) |
| { |
| var target = event.target.enclosingNodeOrSelfWithClass("webkit-line-number"); |
| if (!target) |
| return; |
| this.dispatchEventToListeners(WebInspector.TextEditor.Events.GutterClick, { lineNumber: target.lineNumber, event: event }); |
| }, |
| |
| /** |
| * @param {number} lineNumber |
| * @param {boolean} disabled |
| * @param {boolean} conditional |
| */ |
| addBreakpoint: function(lineNumber, disabled, conditional) |
| { |
| this.beginUpdates(); |
| this._gutterPanel.addDecoration(lineNumber, "webkit-breakpoint"); |
| if (disabled) |
| this._gutterPanel.addDecoration(lineNumber, "webkit-breakpoint-disabled"); |
| else |
| this._gutterPanel.removeDecoration(lineNumber, "webkit-breakpoint-disabled"); |
| if (conditional) |
| this._gutterPanel.addDecoration(lineNumber, "webkit-breakpoint-conditional"); |
| else |
| this._gutterPanel.removeDecoration(lineNumber, "webkit-breakpoint-conditional"); |
| this.endUpdates(); |
| }, |
| |
| /** |
| * @param {number} lineNumber |
| */ |
| removeBreakpoint: function(lineNumber) |
| { |
| this.beginUpdates(); |
| this._gutterPanel.removeDecoration(lineNumber, "webkit-breakpoint"); |
| this._gutterPanel.removeDecoration(lineNumber, "webkit-breakpoint-disabled"); |
| this._gutterPanel.removeDecoration(lineNumber, "webkit-breakpoint-conditional"); |
| this.endUpdates(); |
| }, |
| |
| /** |
| * @param {number} lineNumber |
| */ |
| setExecutionLine: function(lineNumber) |
| { |
| this._executionLineNumber = lineNumber; |
| this._mainPanel.addDecoration(lineNumber, "webkit-execution-line"); |
| this._gutterPanel.addDecoration(lineNumber, "webkit-execution-line"); |
| }, |
| |
| clearExecutionLine: function() |
| { |
| if (typeof this._executionLineNumber === "number") { |
| this._mainPanel.removeDecoration(this._executionLineNumber, "webkit-execution-line"); |
| this._gutterPanel.removeDecoration(this._executionLineNumber, "webkit-execution-line"); |
| } |
| delete this._executionLineNumber; |
| }, |
| |
| /** |
| * @param {number} lineNumber |
| * @param {Element} element |
| */ |
| addDecoration: function(lineNumber, element) |
| { |
| this._mainPanel.addDecoration(lineNumber, element); |
| this._gutterPanel.addDecoration(lineNumber, element); |
| this._syncDecorationsForLine(lineNumber); |
| }, |
| |
| /** |
| * @param {number} lineNumber |
| * @param {Element} element |
| */ |
| removeDecoration: function(lineNumber, element) |
| { |
| this._mainPanel.removeDecoration(lineNumber, element); |
| this._gutterPanel.removeDecoration(lineNumber, element); |
| this._syncDecorationsForLine(lineNumber); |
| }, |
| |
| /** |
| * @param {WebInspector.TextRange} range |
| */ |
| markAndRevealRange: function(range) |
| { |
| if (range) |
| this.setSelection(range); |
| this._mainPanel.markAndRevealRange(range); |
| }, |
| |
| /** |
| * @param {number} lineNumber |
| */ |
| highlightLine: function(lineNumber) |
| { |
| if (typeof lineNumber !== "number" || lineNumber < 0) |
| return; |
| |
| lineNumber = Math.min(lineNumber, this._textModel.linesCount - 1); |
| this._mainPanel.highlightLine(lineNumber); |
| }, |
| |
| clearLineHighlight: function() |
| { |
| this._mainPanel.clearLineHighlight(); |
| }, |
| |
| /** |
| * @return {Array.<Element>} |
| */ |
| elementsToRestoreScrollPositionsFor: function() |
| { |
| return [this._mainPanel.element]; |
| }, |
| |
| /** |
| * @param {WebInspector.TextEditor} textEditor |
| */ |
| inheritScrollPositions: function(textEditor) |
| { |
| this._mainPanel.element._scrollTop = textEditor._mainPanel.element.scrollTop; |
| this._mainPanel.element._scrollLeft = textEditor._mainPanel.element.scrollLeft; |
| }, |
| |
| beginUpdates: function() |
| { |
| this._mainPanel.beginUpdates(); |
| this._gutterPanel.beginUpdates(); |
| }, |
| |
| endUpdates: function() |
| { |
| this._mainPanel.endUpdates(); |
| this._gutterPanel.endUpdates(); |
| this._updatePanelOffsets(); |
| }, |
| |
| onResize: function() |
| { |
| this._mainPanel.resize(); |
| this._gutterPanel.resize(); |
| this._updatePanelOffsets(); |
| }, |
| |
| _textChanged: function(event) |
| { |
| this._mainPanel.textChanged(event.data.oldRange, event.data.newRange); |
| this._gutterPanel.textChanged(event.data.oldRange, event.data.newRange); |
| this._updatePanelOffsets(); |
| if (event.data.editRange) |
| this._delegate.onTextChanged(event.data.oldRange, event.data.newRange); |
| }, |
| |
| /** |
| * @param {WebInspector.TextRange} range |
| * @param {string} text |
| * @return {WebInspector.TextRange} |
| */ |
| editRange: function(range, text) |
| { |
| return this._textModel.editRange(range, text, this.lastSelection()); |
| }, |
| |
| _updatePanelOffsets: function() |
| { |
| var lineNumbersWidth = this._gutterPanel.element.offsetWidth; |
| if (lineNumbersWidth) |
| this._mainPanel.element.style.setProperty("left", (lineNumbersWidth + 2) + "px"); |
| else |
| this._mainPanel.element.style.removeProperty("left"); // Use default value set in CSS. |
| }, |
| |
| _syncScroll: function() |
| { |
| var mainElement = this._mainPanel.element; |
| var gutterElement = this._gutterPanel.element; |
| // Handle horizontal scroll bar at the bottom of the main panel. |
| this._gutterPanel.syncClientHeight(mainElement.clientHeight); |
| gutterElement.scrollTop = mainElement.scrollTop; |
| }, |
| |
| /** |
| * @param {number} lineNumber |
| */ |
| _syncDecorationsForLine: function(lineNumber) |
| { |
| if (lineNumber >= this._textModel.linesCount) |
| return; |
| |
| var mainChunk = this._mainPanel.chunkForLine(lineNumber); |
| if (mainChunk.linesCount === 1 && mainChunk.isDecorated()) { |
| var gutterChunk = this._gutterPanel.makeLineAChunk(lineNumber); |
| var height = mainChunk.height; |
| if (height) |
| gutterChunk.element.style.setProperty("height", height + "px"); |
| else |
| gutterChunk.element.style.removeProperty("height"); |
| } else { |
| var gutterChunk = this._gutterPanel.chunkForLine(lineNumber); |
| if (gutterChunk.linesCount === 1) |
| gutterChunk.element.style.removeProperty("height"); |
| } |
| }, |
| |
| /** |
| * @param {Element} gutterRow |
| */ |
| _syncLineHeight: function(gutterRow) |
| { |
| if (this._lineHeightSynced) |
| return; |
| if (gutterRow && gutterRow.offsetHeight) { |
| // Force equal line heights for the child panels. |
| this.element.style.setProperty("line-height", gutterRow.offsetHeight + "px"); |
| this._lineHeightSynced = true; |
| } |
| }, |
| |
| _registerShortcuts: function() |
| { |
| var keys = WebInspector.KeyboardShortcut.Keys; |
| var modifiers = WebInspector.KeyboardShortcut.Modifiers; |
| |
| this._shortcuts = {}; |
| |
| this._shortcuts[WebInspector.KeyboardShortcut.SelectAll] = this._handleSelectAll.bind(this); |
| this._wordMovementController._registerShortcuts(this._shortcuts); |
| }, |
| |
| _handleSelectAll: function() |
| { |
| this.setSelection(this._textModel.range()); |
| return true; |
| }, |
| |
| _handleKeyDown: function(e) |
| { |
| // If the event was not triggered from the entire editor, then |
| // ignore it. https://bugs.webkit.org/show_bug.cgi?id=102906 |
| if (e.target.enclosingNodeOrSelfWithClass("webkit-line-decorations")) |
| return; |
| |
| var shortcutKey = WebInspector.KeyboardShortcut.makeKeyFromEvent(e); |
| |
| var handler = this._shortcuts[shortcutKey]; |
| if (handler && handler()) { |
| e.consume(true); |
| return; |
| } |
| this._mainPanel.handleKeyDown(shortcutKey, e); |
| }, |
| |
| _contextMenu: function(event) |
| { |
| var anchor = event.target.enclosingNodeOrSelfWithNodeName("a"); |
| if (anchor) |
| return; |
| var contextMenu = new WebInspector.ContextMenu(event); |
| var target = event.target.enclosingNodeOrSelfWithClass("webkit-line-number"); |
| if (target) |
| this._delegate.populateLineGutterContextMenu(contextMenu, target.lineNumber); |
| else { |
| this._mainPanel.populateContextMenu(event.target, contextMenu); |
| } |
| contextMenu.show(); |
| }, |
| |
| _handleScrollChanged: function(event) |
| { |
| var visibleFrom = this._mainPanel.scrollTop(); |
| var firstVisibleLineNumber = this._mainPanel.lineNumberAtOffset(visibleFrom); |
| this._delegate.scrollChanged(firstVisibleLineNumber); |
| }, |
| |
| /** |
| * @param {number} lineNumber |
| */ |
| scrollToLine: function(lineNumber) |
| { |
| this._mainPanel.scrollToLine(lineNumber); |
| }, |
| |
| /** |
| * @return {WebInspector.TextRange} |
| */ |
| selection: function() |
| { |
| return this._mainPanel.selection(); |
| }, |
| |
| /** |
| * @return {WebInspector.TextRange?} |
| */ |
| lastSelection: function() |
| { |
| return this._mainPanel.lastSelection(); |
| }, |
| |
| /** |
| * @param {WebInspector.TextRange} textRange |
| */ |
| setSelection: function(textRange) |
| { |
| this._mainPanel.setSelection(textRange); |
| }, |
| |
| /** |
| * @param {string} text |
| */ |
| setText: function(text) |
| { |
| this._textModel.setText(text); |
| }, |
| |
| /** |
| * @return {string} |
| */ |
| text: function() |
| { |
| return this._textModel.text(); |
| }, |
| |
| /** |
| * @return {WebInspector.TextRange} |
| */ |
| range: function() |
| { |
| return this._textModel.range(); |
| }, |
| |
| /** |
| * @param {number} lineNumber |
| * @return {string} |
| */ |
| line: function(lineNumber) |
| { |
| return this._textModel.line(lineNumber); |
| }, |
| |
| /** |
| * @return {number} |
| */ |
| get linesCount() |
| { |
| return this._textModel.linesCount; |
| }, |
| |
| /** |
| * @param {number} line |
| * @param {string} name |
| * @param {?Object} value |
| */ |
| setAttribute: function(line, name, value) |
| { |
| this._textModel.setAttribute(line, name, value); |
| }, |
| |
| /** |
| * @param {number} line |
| * @param {string} name |
| * @return {?Object} value |
| */ |
| getAttribute: function(line, name) |
| { |
| return this._textModel.getAttribute(line, name); |
| }, |
| |
| /** |
| * @param {number} line |
| * @param {string} name |
| */ |
| removeAttribute: function(line, name) |
| { |
| this._textModel.removeAttribute(line, name); |
| }, |
| |
| wasShown: function() |
| { |
| if (!this.readOnly()) |
| WebInspector.markBeingEdited(this.element, true); |
| |
| this._mainPanel.wasShown(); |
| }, |
| |
| willHide: function() |
| { |
| this._mainPanel.willHide(); |
| this._gutterPanel.willHide(); |
| |
| if (!this.readOnly()) |
| WebInspector.markBeingEdited(this.element, false); |
| }, |
| |
| /** |
| * @param {Element} element |
| * @param {Array.<Object>} resultRanges |
| * @param {string} styleClass |
| * @param {Array.<Object>=} changes |
| */ |
| highlightRangesWithStyleClass: function(element, resultRanges, styleClass, changes) |
| { |
| this._mainPanel.beginDomUpdates(); |
| WebInspector.highlightRangesWithStyleClass(element, resultRanges, styleClass, changes); |
| this._mainPanel.endDomUpdates(); |
| }, |
| |
| /** |
| * @param {Element} element |
| * @param {Object} skipClasses |
| * @param {Object} skipTokens |
| * @return {Element} |
| */ |
| highlightExpression: function(element, skipClasses, skipTokens) |
| { |
| // Collect tokens belonging to evaluated expression. |
| var tokens = [element]; |
| var token = element.previousSibling; |
| while (token && (skipClasses[token.className] || skipTokens[token.textContent.trim()])) { |
| tokens.push(token); |
| token = token.previousSibling; |
| } |
| tokens.reverse(); |
| |
| // Wrap them with highlight element. |
| this._mainPanel.beginDomUpdates(); |
| var parentElement = element.parentElement; |
| var nextElement = element.nextSibling; |
| var container = document.createElement("span"); |
| for (var i = 0; i < tokens.length; ++i) |
| container.appendChild(tokens[i]); |
| parentElement.insertBefore(container, nextElement); |
| this._mainPanel.endDomUpdates(); |
| return container; |
| }, |
| |
| /** |
| * @param {Element} highlightElement |
| */ |
| hideHighlightedExpression: function(highlightElement) |
| { |
| this._mainPanel.beginDomUpdates(); |
| var parentElement = highlightElement.parentElement; |
| if (parentElement) { |
| var child = highlightElement.firstChild; |
| while (child) { |
| var nextSibling = child.nextSibling; |
| parentElement.insertBefore(child, highlightElement); |
| child = nextSibling; |
| } |
| parentElement.removeChild(highlightElement); |
| } |
| this._mainPanel.endDomUpdates(); |
| }, |
| |
| /** |
| * @param {number} scrollTop |
| * @param {number} clientHeight |
| * @param {number} chunkSize |
| */ |
| overrideViewportForTest: function(scrollTop, clientHeight, chunkSize) |
| { |
| this._mainPanel.overrideViewportForTest(scrollTop, clientHeight, chunkSize); |
| }, |
| |
| __proto__: WebInspector.View.prototype |
| } |
| |
| /** |
| * @constructor |
| * @param {WebInspector.TextEditorModel} textModel |
| */ |
| WebInspector.TextEditorChunkedPanel = function(textModel) |
| { |
| this._textModel = textModel; |
| |
| this.element = document.createElement("div"); |
| this.element.addEventListener("scroll", this._scroll.bind(this), false); |
| |
| this._defaultChunkSize = 50; |
| this._paintCoalescingLevel = 0; |
| this._domUpdateCoalescingLevel = 0; |
| } |
| |
| WebInspector.TextEditorChunkedPanel.prototype = { |
| /** |
| * @param {number} lineNumber |
| */ |
| scrollToLine: function(lineNumber) |
| { |
| if (lineNumber >= this._textModel.linesCount) |
| return; |
| |
| var chunk = this.makeLineAChunk(lineNumber); |
| this.element.scrollTop = chunk.offsetTop; |
| }, |
| |
| /** |
| * @param {number} lineNumber |
| */ |
| revealLine: function(lineNumber) |
| { |
| if (lineNumber >= this._textModel.linesCount) |
| return; |
| |
| var chunk = this.makeLineAChunk(lineNumber); |
| chunk.element.scrollIntoViewIfNeeded(); |
| }, |
| |
| /** |
| * @param {number} lineNumber |
| * @param {string|Element} decoration |
| */ |
| addDecoration: function(lineNumber, decoration) |
| { |
| if (lineNumber >= this._textModel.linesCount) |
| return; |
| |
| var chunk = this.makeLineAChunk(lineNumber); |
| chunk.addDecoration(decoration); |
| }, |
| |
| /** |
| * @param {number} lineNumber |
| * @param {string|Element} decoration |
| */ |
| removeDecoration: function(lineNumber, decoration) |
| { |
| if (lineNumber >= this._textModel.linesCount) |
| return; |
| |
| var chunk = this.chunkForLine(lineNumber); |
| chunk.removeDecoration(decoration); |
| }, |
| |
| buildChunks: function() |
| { |
| this.beginDomUpdates(); |
| |
| this._container.removeChildren(); |
| |
| this._textChunks = []; |
| for (var i = 0; i < this._textModel.linesCount; i += this._defaultChunkSize) { |
| var chunk = this.createNewChunk(i, i + this._defaultChunkSize); |
| this._textChunks.push(chunk); |
| this._container.appendChild(chunk.element); |
| } |
| |
| this.repaintAll(); |
| |
| this.endDomUpdates(); |
| }, |
| |
| /** |
| * @param {number} lineNumber |
| * @return {Object} |
| */ |
| makeLineAChunk: function(lineNumber) |
| { |
| var chunkNumber = this.chunkNumberForLine(lineNumber); |
| var oldChunk = this._textChunks[chunkNumber]; |
| |
| if (!oldChunk) { |
| console.error("No chunk for line number: " + lineNumber); |
| return null; |
| } |
| |
| if (oldChunk.linesCount === 1) |
| return oldChunk; |
| |
| return this.splitChunkOnALine(lineNumber, chunkNumber, true); |
| }, |
| |
| /** |
| * @param {number} lineNumber |
| * @param {number} chunkNumber |
| * @param {boolean=} createSuffixChunk |
| * @return {Object} |
| */ |
| splitChunkOnALine: function(lineNumber, chunkNumber, createSuffixChunk) |
| { |
| this.beginDomUpdates(); |
| |
| var oldChunk = this._textChunks[chunkNumber]; |
| var wasExpanded = oldChunk.expanded(); |
| oldChunk.collapse(); |
| |
| var insertIndex = chunkNumber + 1; |
| |
| // Prefix chunk. |
| if (lineNumber > oldChunk.startLine) { |
| var prefixChunk = this.createNewChunk(oldChunk.startLine, lineNumber); |
| this._textChunks.splice(insertIndex++, 0, prefixChunk); |
| this._container.insertBefore(prefixChunk.element, oldChunk.element); |
| } |
| |
| // Line chunk. |
| var endLine = createSuffixChunk ? lineNumber + 1 : oldChunk.startLine + oldChunk.linesCount; |
| var lineChunk = this.createNewChunk(lineNumber, endLine); |
| this._textChunks.splice(insertIndex++, 0, lineChunk); |
| this._container.insertBefore(lineChunk.element, oldChunk.element); |
| |
| // Suffix chunk. |
| if (oldChunk.startLine + oldChunk.linesCount > endLine) { |
| var suffixChunk = this.createNewChunk(endLine, oldChunk.startLine + oldChunk.linesCount); |
| this._textChunks.splice(insertIndex, 0, suffixChunk); |
| this._container.insertBefore(suffixChunk.element, oldChunk.element); |
| } |
| |
| // Remove enclosing chunk. |
| this._textChunks.splice(chunkNumber, 1); |
| this._container.removeChild(oldChunk.element); |
| |
| if (wasExpanded) { |
| if (prefixChunk) |
| prefixChunk.expand(); |
| lineChunk.expand(); |
| if (suffixChunk) |
| suffixChunk.expand(); |
| } |
| |
| this.endDomUpdates(); |
| |
| return lineChunk; |
| }, |
| |
| createNewChunk: function(startLine, endLine) |
| { |
| throw new Error("createNewChunk() should be implemented by descendants"); |
| }, |
| |
| _scroll: function() |
| { |
| this._scheduleRepaintAll(); |
| if (this._syncScrollListener) |
| this._syncScrollListener(); |
| }, |
| |
| _scheduleRepaintAll: function() |
| { |
| if (this._repaintAllTimer) |
| clearTimeout(this._repaintAllTimer); |
| this._repaintAllTimer = setTimeout(this.repaintAll.bind(this), 50); |
| }, |
| |
| beginUpdates: function() |
| { |
| this._paintCoalescingLevel++; |
| }, |
| |
| endUpdates: function() |
| { |
| this._paintCoalescingLevel--; |
| if (!this._paintCoalescingLevel) |
| this.repaintAll(); |
| }, |
| |
| beginDomUpdates: function() |
| { |
| this._domUpdateCoalescingLevel++; |
| }, |
| |
| endDomUpdates: function() |
| { |
| this._domUpdateCoalescingLevel--; |
| }, |
| |
| /** |
| * @param {number} lineNumber |
| * @return {number} |
| */ |
| chunkNumberForLine: function(lineNumber) |
| { |
| function compareLineNumbers(value, chunk) |
| { |
| return value < chunk.startLine ? -1 : 1; |
| } |
| var insertBefore = insertionIndexForObjectInListSortedByFunction(lineNumber, this._textChunks, compareLineNumbers); |
| return insertBefore - 1; |
| }, |
| |
| /** |
| * @param {number} lineNumber |
| * @return {Object} |
| */ |
| chunkForLine: function(lineNumber) |
| { |
| return this._textChunks[this.chunkNumberForLine(lineNumber)]; |
| }, |
| |
| /** |
| * @param {number} visibleFrom |
| * @return {number} |
| */ |
| _findFirstVisibleChunkNumber: function(visibleFrom) |
| { |
| function compareOffsetTops(value, chunk) |
| { |
| return value < chunk.offsetTop ? -1 : 1; |
| } |
| var insertBefore = insertionIndexForObjectInListSortedByFunction(visibleFrom, this._textChunks, compareOffsetTops); |
| return insertBefore - 1; |
| }, |
| |
| /** |
| * @param {number} visibleFrom |
| * @param {number} visibleTo |
| * @return {{start: number, end: number}} |
| */ |
| findVisibleChunks: function(visibleFrom, visibleTo) |
| { |
| var span = (visibleTo - visibleFrom) * 0.5; |
| visibleFrom = Math.max(visibleFrom - span, 0); |
| visibleTo = visibleTo + span; |
| |
| var from = this._findFirstVisibleChunkNumber(visibleFrom); |
| for (var to = from + 1; to < this._textChunks.length; ++to) { |
| if (this._textChunks[to].offsetTop >= visibleTo) |
| break; |
| } |
| return { start: from, end: to }; |
| }, |
| |
| /** |
| * @param {number} visibleFrom |
| * @return {number} |
| */ |
| lineNumberAtOffset: function(visibleFrom) |
| { |
| var chunk = this._textChunks[this._findFirstVisibleChunkNumber(visibleFrom)]; |
| if (!chunk.expanded()) |
| return chunk.startLine; |
| |
| var lineNumbers = []; |
| for (var i = 0; i < chunk.linesCount; ++i) { |
| lineNumbers.push(chunk.startLine + i); |
| } |
| |
| function compareLineRowOffsetTops(value, lineNumber) |
| { |
| var lineRow = chunk.expandedLineRow(lineNumber); |
| return value < lineRow.offsetTop ? -1 : 1; |
| } |
| var insertBefore = insertionIndexForObjectInListSortedByFunction(visibleFrom, lineNumbers, compareLineRowOffsetTops); |
| return lineNumbers[insertBefore - 1]; |
| }, |
| |
| repaintAll: function() |
| { |
| delete this._repaintAllTimer; |
| |
| if (this._paintCoalescingLevel) |
| return; |
| |
| var visibleFrom = this.scrollTop(); |
| var visibleTo = visibleFrom + this.clientHeight(); |
| |
| if (visibleTo) { |
| var result = this.findVisibleChunks(visibleFrom, visibleTo); |
| this.expandChunks(result.start, result.end); |
| } |
| }, |
| |
| scrollTop: function() |
| { |
| return typeof this._scrollTopOverrideForTest === "number" ? this._scrollTopOverrideForTest : this.element.scrollTop; |
| }, |
| |
| clientHeight: function() |
| { |
| return typeof this._clientHeightOverrideForTest === "number" ? this._clientHeightOverrideForTest : this.element.clientHeight; |
| }, |
| |
| /** |
| * @param {number} fromIndex |
| * @param {number} toIndex |
| */ |
| expandChunks: function(fromIndex, toIndex) |
| { |
| // First collapse chunks to collect the DOM elements into a cache to reuse them later. |
| for (var i = 0; i < fromIndex; ++i) |
| this._textChunks[i].collapse(); |
| for (var i = toIndex; i < this._textChunks.length; ++i) |
| this._textChunks[i].collapse(); |
| for (var i = fromIndex; i < toIndex; ++i) |
| this._textChunks[i].expand(); |
| }, |
| |
| /** |
| * @param {Element} firstElement |
| * @param {Element=} lastElement |
| * @return {number} |
| */ |
| totalHeight: function(firstElement, lastElement) |
| { |
| lastElement = (lastElement || firstElement).nextElementSibling; |
| if (lastElement) |
| return lastElement.offsetTop - firstElement.offsetTop; |
| |
| var offsetParent = firstElement.offsetParent; |
| if (offsetParent && offsetParent.scrollHeight > offsetParent.clientHeight) |
| return offsetParent.scrollHeight - firstElement.offsetTop; |
| |
| var total = 0; |
| while (firstElement && firstElement !== lastElement) { |
| total += firstElement.offsetHeight; |
| firstElement = firstElement.nextElementSibling; |
| } |
| return total; |
| }, |
| |
| resize: function() |
| { |
| this.repaintAll(); |
| } |
| } |
| |
| /** |
| * @constructor |
| * @extends {WebInspector.TextEditorChunkedPanel} |
| * @param {WebInspector.TextEditorModel} textModel |
| * @param {function(number)} syncDecorationsForLineListener |
| * @param {function(Element)} syncLineHeightListener |
| */ |
| WebInspector.TextEditorGutterPanel = function(textModel, syncDecorationsForLineListener, syncLineHeightListener) |
| { |
| WebInspector.TextEditorChunkedPanel.call(this, textModel); |
| |
| this._syncDecorationsForLineListener = syncDecorationsForLineListener; |
| this._syncLineHeightListener = syncLineHeightListener; |
| |
| this.element.className = "text-editor-lines"; |
| |
| this._container = document.createElement("div"); |
| this._container.className = "inner-container"; |
| this.element.appendChild(this._container); |
| |
| this._freeCachedElements(); |
| this.buildChunks(); |
| this._decorations = {}; |
| } |
| |
| WebInspector.TextEditorGutterPanel.prototype = { |
| _freeCachedElements: function() |
| { |
| this._cachedRows = []; |
| }, |
| |
| willHide: function() |
| { |
| this._freeCachedElements(); |
| }, |
| |
| /** |
| * @param {number} startLine |
| * @param {number} endLine |
| * @return {WebInspector.TextEditorGutterChunk} |
| */ |
| createNewChunk: function(startLine, endLine) |
| { |
| return new WebInspector.TextEditorGutterChunk(this, startLine, endLine); |
| }, |
| |
| /** |
| * @param {WebInspector.TextRange} oldRange |
| * @param {WebInspector.TextRange} newRange |
| */ |
| textChanged: function(oldRange, newRange) |
| { |
| this.beginDomUpdates(); |
| |
| var linesDiff = newRange.linesCount - oldRange.linesCount; |
| if (linesDiff) { |
| // Remove old chunks (if needed). |
| for (var chunkNumber = this._textChunks.length - 1; chunkNumber >= 0; --chunkNumber) { |
| var chunk = this._textChunks[chunkNumber]; |
| if (chunk.startLine + chunk.linesCount <= this._textModel.linesCount) |
| break; |
| chunk.collapse(); |
| this._container.removeChild(chunk.element); |
| } |
| this._textChunks.length = chunkNumber + 1; |
| |
| // Add new chunks (if needed). |
| var totalLines = 0; |
| if (this._textChunks.length) { |
| var lastChunk = this._textChunks[this._textChunks.length - 1]; |
| totalLines = lastChunk.startLine + lastChunk.linesCount; |
| } |
| |
| for (var i = totalLines; i < this._textModel.linesCount; i += this._defaultChunkSize) { |
| var chunk = this.createNewChunk(i, i + this._defaultChunkSize); |
| this._textChunks.push(chunk); |
| this._container.appendChild(chunk.element); |
| } |
| |
| // Shift decorations if necessary |
| for (var lineNumber in this._decorations) { |
| lineNumber = parseInt(lineNumber, 10); |
| |
| // Do not move decorations before the start position. |
| if (lineNumber < oldRange.startLine) |
| continue; |
| // Decorations follow the first character of line. |
| if (lineNumber === oldRange.startLine && oldRange.startColumn) |
| continue; |
| |
| var lineDecorationsCopy = this._decorations[lineNumber].slice(); |
| for (var i = 0; i < lineDecorationsCopy.length; ++i) { |
| var decoration = lineDecorationsCopy[i]; |
| this.removeDecoration(lineNumber, decoration); |
| |
| // Do not restore the decorations before the end position. |
| if (lineNumber < oldRange.endLine) |
| continue; |
| |
| this.addDecoration(lineNumber + linesDiff, decoration); |
| } |
| } |
| |
| this.repaintAll(); |
| } else { |
| // Decorations may have been removed, so we may have to sync those lines. |
| var chunkNumber = this.chunkNumberForLine(newRange.startLine); |
| var chunk = this._textChunks[chunkNumber]; |
| while (chunk && chunk.startLine <= newRange.endLine) { |
| if (chunk.linesCount === 1) |
| this._syncDecorationsForLineListener(chunk.startLine); |
| chunk = this._textChunks[++chunkNumber]; |
| } |
| } |
| |
| this.endDomUpdates(); |
| }, |
| |
| /** |
| * @param {number} clientHeight |
| */ |
| syncClientHeight: function(clientHeight) |
| { |
| if (this.element.offsetHeight > clientHeight) |
| this._container.style.setProperty("padding-bottom", (this.element.offsetHeight - clientHeight) + "px"); |
| else |
| this._container.style.removeProperty("padding-bottom"); |
| }, |
| |
| /** |
| * @param {number} lineNumber |
| * @param {string|Element} decoration |
| */ |
| addDecoration: function(lineNumber, decoration) |
| { |
| WebInspector.TextEditorChunkedPanel.prototype.addDecoration.call(this, lineNumber, decoration); |
| var decorations = this._decorations[lineNumber]; |
| if (!decorations) { |
| decorations = []; |
| this._decorations[lineNumber] = decorations; |
| } |
| decorations.push(decoration); |
| }, |
| |
| /** |
| * @param {number} lineNumber |
| * @param {string|Element} decoration |
| */ |
| removeDecoration: function(lineNumber, decoration) |
| { |
| WebInspector.TextEditorChunkedPanel.prototype.removeDecoration.call(this, lineNumber, decoration); |
| var decorations = this._decorations[lineNumber]; |
| if (decorations) { |
| decorations.remove(decoration); |
| if (!decorations.length) |
| delete this._decorations[lineNumber]; |
| } |
| }, |
| |
| __proto__: WebInspector.TextEditorChunkedPanel.prototype |
| } |
| |
| /** |
| * @constructor |
| * @param {WebInspector.TextEditorGutterPanel} chunkedPanel |
| * @param {number} startLine |
| * @param {number} endLine |
| */ |
| WebInspector.TextEditorGutterChunk = function(chunkedPanel, startLine, endLine) |
| { |
| this._chunkedPanel = chunkedPanel; |
| this._textModel = chunkedPanel._textModel; |
| |
| this.startLine = startLine; |
| endLine = Math.min(this._textModel.linesCount, endLine); |
| this.linesCount = endLine - startLine; |
| |
| this._expanded = false; |
| |
| this.element = document.createElement("div"); |
| this.element.lineNumber = startLine; |
| this.element.className = "webkit-line-number"; |
| |
| if (this.linesCount === 1) { |
| // Single line chunks are typically created for decorations. Host line number in |
| // the sub-element in order to allow flexible border / margin management. |
| var innerSpan = document.createElement("span"); |
| innerSpan.className = "webkit-line-number-inner"; |
| innerSpan.textContent = startLine + 1; |
| var outerSpan = document.createElement("div"); |
| outerSpan.className = "webkit-line-number-outer"; |
| outerSpan.appendChild(innerSpan); |
| this.element.appendChild(outerSpan); |
| } else { |
| var lineNumbers = []; |
| for (var i = startLine; i < endLine; ++i) |
| lineNumbers.push(i + 1); |
| this.element.textContent = lineNumbers.join("\n"); |
| } |
| } |
| |
| WebInspector.TextEditorGutterChunk.prototype = { |
| /** |
| * @param {string} decoration |
| */ |
| addDecoration: function(decoration) |
| { |
| this._chunkedPanel.beginDomUpdates(); |
| if (typeof decoration === "string") |
| this.element.addStyleClass(decoration); |
| this._chunkedPanel.endDomUpdates(); |
| }, |
| |
| /** |
| * @param {string} decoration |
| */ |
| removeDecoration: function(decoration) |
| { |
| this._chunkedPanel.beginDomUpdates(); |
| if (typeof decoration === "string") |
| this.element.removeStyleClass(decoration); |
| this._chunkedPanel.endDomUpdates(); |
| }, |
| |
| /** |
| * @return {boolean} |
| */ |
| expanded: function() |
| { |
| return this._expanded; |
| }, |
| |
| expand: function() |
| { |
| if (this.linesCount === 1) |
| this._chunkedPanel._syncDecorationsForLineListener(this.startLine); |
| |
| if (this._expanded) |
| return; |
| |
| this._expanded = true; |
| |
| if (this.linesCount === 1) |
| return; |
| |
| this._chunkedPanel.beginDomUpdates(); |
| |
| this._expandedLineRows = []; |
| var parentElement = this.element.parentElement; |
| for (var i = this.startLine; i < this.startLine + this.linesCount; ++i) { |
| var lineRow = this._createRow(i); |
| parentElement.insertBefore(lineRow, this.element); |
| this._expandedLineRows.push(lineRow); |
| } |
| parentElement.removeChild(this.element); |
| this._chunkedPanel._syncLineHeightListener(this._expandedLineRows[0]); |
| |
| this._chunkedPanel.endDomUpdates(); |
| }, |
| |
| collapse: function() |
| { |
| if (this.linesCount === 1) |
| this._chunkedPanel._syncDecorationsForLineListener(this.startLine); |
| |
| if (!this._expanded) |
| return; |
| |
| this._expanded = false; |
| |
| if (this.linesCount === 1) |
| return; |
| |
| this._chunkedPanel.beginDomUpdates(); |
| |
| var elementInserted = false; |
| for (var i = 0; i < this._expandedLineRows.length; ++i) { |
| var lineRow = this._expandedLineRows[i]; |
| var parentElement = lineRow.parentElement; |
| if (parentElement) { |
| if (!elementInserted) { |
| elementInserted = true; |
| parentElement.insertBefore(this.element, lineRow); |
| } |
| parentElement.removeChild(lineRow); |
| } |
| this._chunkedPanel._cachedRows.push(lineRow); |
| } |
| delete this._expandedLineRows; |
| |
| this._chunkedPanel.endDomUpdates(); |
| }, |
| |
| /** |
| * @return {number} |
| */ |
| get height() |
| { |
| if (!this._expandedLineRows) |
| return this._chunkedPanel.totalHeight(this.element); |
| return this._chunkedPanel.totalHeight(this._expandedLineRows[0], this._expandedLineRows[this._expandedLineRows.length - 1]); |
| }, |
| |
| /** |
| * @return {number} |
| */ |
| get offsetTop() |
| { |
| return (this._expandedLineRows && this._expandedLineRows.length) ? this._expandedLineRows[0].offsetTop : this.element.offsetTop; |
| }, |
| |
| /** |
| * @param {number} lineNumber |
| * @return {Element} |
| */ |
| _createRow: function(lineNumber) |
| { |
| var lineRow = this._chunkedPanel._cachedRows.pop() || document.createElement("div"); |
| lineRow.lineNumber = lineNumber; |
| lineRow.className = "webkit-line-number"; |
| lineRow.textContent = lineNumber + 1; |
| return lineRow; |
| } |
| } |
| |
| /** |
| * @constructor |
| * @extends {WebInspector.TextEditorChunkedPanel} |
| * @param {WebInspector.TextEditorDelegate} delegate |
| * @param {WebInspector.TextEditorModel} textModel |
| * @param {?string} url |
| * @param {function()} syncScrollListener |
| * @param {function(number)} syncDecorationsForLineListener |
| */ |
| WebInspector.TextEditorMainPanel = function(delegate, textModel, url, syncScrollListener, syncDecorationsForLineListener) |
| { |
| WebInspector.TextEditorChunkedPanel.call(this, textModel); |
| |
| this._delegate = delegate; |
| this._syncScrollListener = syncScrollListener; |
| this._syncDecorationsForLineListener = syncDecorationsForLineListener; |
| |
| this._url = url; |
| this._highlighter = new WebInspector.TextEditorHighlighter(textModel, this._highlightDataReady.bind(this)); |
| this._readOnly = true; |
| |
| this.element.className = "text-editor-contents"; |
| this.element.tabIndex = 0; |
| |
| this._container = document.createElement("div"); |
| this._container.className = "inner-container"; |
| this._container.tabIndex = 0; |
| this.element.appendChild(this._container); |
| |
| this.element.addEventListener("focus", this._handleElementFocus.bind(this), false); |
| this.element.addEventListener("textInput", this._handleTextInput.bind(this), false); |
| this.element.addEventListener("cut", this._handleCut.bind(this), false); |
| this.element.addEventListener("keypress", this._handleKeyPress.bind(this), false); |
| |
| this._showWhitespace = WebInspector.experimentsSettings.showWhitespaceInEditor.isEnabled(); |
| |
| this._container.addEventListener("focus", this._handleFocused.bind(this), false); |
| |
| this._highlightDescriptors = []; |
| |
| this._tokenHighlighter = new WebInspector.TextEditorMainPanel.TokenHighlighter(this, textModel); |
| this._braceMatcher = new WebInspector.TextEditorModel.BraceMatcher(textModel); |
| this._braceHighlighter = new WebInspector.TextEditorMainPanel.BraceHighlightController(this, textModel, this._braceMatcher); |
| this._smartBraceController = new WebInspector.TextEditorMainPanel.SmartBraceController(this, textModel, this._braceMatcher); |
| |
| this._freeCachedElements(); |
| this.buildChunks(); |
| this._registerShortcuts(); |
| } |
| |
| WebInspector.TextEditorMainPanel._ConsecutiveWhitespaceChars = { |
| 1: " ", |
| 2: " ", |
| 4: " ", |
| 8: " ", |
| 16: " " |
| }; |
| |
| WebInspector.TextEditorMainPanel.prototype = { |
| _registerShortcuts: function() |
| { |
| var keys = WebInspector.KeyboardShortcut.Keys; |
| var modifiers = WebInspector.KeyboardShortcut.Modifiers; |
| |
| this._shortcuts = {}; |
| |
| this._shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Enter.code, WebInspector.KeyboardShortcut.Modifiers.None)] = this._handleEnterKey.bind(this); |
| this._shortcuts[WebInspector.KeyboardShortcut.makeKey("z", modifiers.CtrlOrMeta)] = this._handleUndoRedo.bind(this, false); |
| |
| var handleRedo = this._handleUndoRedo.bind(this, true); |
| this._shortcuts[WebInspector.KeyboardShortcut.makeKey("z", modifiers.Shift | modifiers.CtrlOrMeta)] = handleRedo; |
| if (!WebInspector.isMac()) |
| this._shortcuts[WebInspector.KeyboardShortcut.makeKey("y", modifiers.CtrlOrMeta)] = handleRedo; |
| |
| var handleTabKey = this._handleTabKeyPress.bind(this, false); |
| var handleShiftTabKey = this._handleTabKeyPress.bind(this, true); |
| this._shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Tab.code)] = handleTabKey; |
| this._shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Tab.code, modifiers.Shift)] = handleShiftTabKey; |
| |
| var homeKey = WebInspector.isMac() ? keys.Right : keys.Home; |
| var homeModifier = WebInspector.isMac() ? modifiers.Meta : modifiers.None; |
| this._shortcuts[WebInspector.KeyboardShortcut.makeKey(homeKey.code, homeModifier)] = this._handleHomeKey.bind(this, false); |
| this._shortcuts[WebInspector.KeyboardShortcut.makeKey(homeKey.code, homeModifier | modifiers.Shift)] = this._handleHomeKey.bind(this, true); |
| |
| this._charOverrides = {}; |
| |
| this._smartBraceController.registerShortcuts(this._shortcuts); |
| this._smartBraceController.registerCharOverrides(this._charOverrides); |
| }, |
| |
| _handleKeyPress: function(event) |
| { |
| var char = String.fromCharCode(event.which); |
| var handler = this._charOverrides[char]; |
| if (handler && handler()) { |
| event.consume(true); |
| return; |
| } |
| this._keyDownCode = event.keyCode; |
| }, |
| |
| /** |
| * @param {boolean} shift |
| */ |
| _handleHomeKey: function(shift) |
| { |
| var selection = this.selection(); |
| |
| var line = this._textModel.line(selection.endLine); |
| var firstNonBlankCharacter = 0; |
| while (firstNonBlankCharacter < line.length) { |
| var char = line.charAt(firstNonBlankCharacter); |
| if (char === " " || char === "\t") |
| ++firstNonBlankCharacter; |
| else |
| break; |
| } |
| if (firstNonBlankCharacter >= line.length || selection.endColumn === firstNonBlankCharacter) |
| return false; |
| |
| selection.endColumn = firstNonBlankCharacter; |
| if (!shift) |
| selection = selection.collapseToEnd(); |
| this._restoreSelection(selection); |
| return true; |
| }, |
| |
| /** |
| * @param {string} regex |
| * @param {string} cssClass |
| * @return {Object} |
| */ |
| highlightRegex: function(regex, cssClass) |
| { |
| var highlightDescriptor = new WebInspector.TextEditorMainPanel.RegexHighlightDescriptor(new RegExp(regex, "g"), cssClass); |
| this._highlightDescriptors.push(highlightDescriptor); |
| this._repaintLineRowsAffectedByHighlightDescriptors([highlightDescriptor]); |
| return highlightDescriptor; |
| }, |
| |
| /** |
| * @param {Object} highlightDescriptor |
| */ |
| removeHighlight: function(highlightDescriptor) |
| { |
| this._highlightDescriptors.remove(highlightDescriptor); |
| this._repaintLineRowsAffectedByHighlightDescriptors([highlightDescriptor]); |
| }, |
| |
| /** |
| * @param {WebInspector.TextRange} range |
| * @param {string} cssClass |
| * @return {Object} |
| */ |
| highlightRange: function(range, cssClass) |
| { |
| var highlightDescriptor = new WebInspector.TextEditorMainPanel.RangeHighlightDescriptor(range, cssClass); |
| this._highlightDescriptors.push(highlightDescriptor); |
| this._repaintLineRowsAffectedByHighlightDescriptors([highlightDescriptor]); |
| return highlightDescriptor; |
| }, |
| |
| /** |
| * @param {Array.<WebInspector.TextEditorMainPanel.HighlightDescriptor>} highlightDescriptors |
| */ |
| _repaintLineRowsAffectedByHighlightDescriptors: function(highlightDescriptors) |
| { |
| var visibleFrom = this.scrollTop(); |
| var visibleTo = visibleFrom + this.clientHeight(); |
| |
| var visibleChunks = this.findVisibleChunks(visibleFrom, visibleTo); |
| |
| var affectedLineRows = []; |
| for (var i = visibleChunks.start; i < visibleChunks.end; ++i) { |
| var chunk = this._textChunks[i]; |
| if (!chunk.expanded()) |
| continue; |
| for (var lineNumber = chunk.startLine; lineNumber < chunk.startLine + chunk.linesCount; ++lineNumber) { |
| var lineRow = chunk.expandedLineRow(lineNumber); |
| var line = this._textModel.line(lineNumber); |
| for(var j = 0; j < highlightDescriptors.length; ++j) { |
| if (highlightDescriptors[j].affectsLine(lineNumber, line)) { |
| affectedLineRows.push(lineRow); |
| break; |
| } |
| } |
| } |
| } |
| if (affectedLineRows.length === 0) |
| return; |
| var selection = this.selection(); |
| this._paintLineRows(affectedLineRows); |
| this._restoreSelection(selection); |
| }, |
| |
| resize: function() |
| { |
| WebInspector.TextEditorChunkedPanel.prototype.resize.call(this); |
| this._repaintLineRowsAffectedByHighlightDescriptors(this._highlightDescriptors); |
| }, |
| |
| wasShown: function() |
| { |
| this._boundSelectionChangeListener = this._handleSelectionChange.bind(this); |
| document.addEventListener("selectionchange", this._boundSelectionChangeListener, false); |
| |
| this._isShowing = true; |
| this._attachMutationObserver(); |
| }, |
| |
| willHide: function() |
| { |
| document.removeEventListener("selectionchange", this._boundSelectionChangeListener, false); |
| delete this._boundSelectionChangeListener; |
| |
| this._detachMutationObserver(); |
| this._isShowing = false; |
| this._freeCachedElements(); |
| }, |
| |
| /** |
| * @param {Element} eventTarget |
| * @param {WebInspector.ContextMenu} contextMenu |
| */ |
| populateContextMenu: function(eventTarget, contextMenu) |
| { |
| var target = this._enclosingLineRowOrSelf(eventTarget); |
| this._delegate.populateTextAreaContextMenu(contextMenu, target && target.lineNumber); |
| }, |
| |
| /** |
| * @param {WebInspector.TextRange} textRange |
| */ |
| setSelection: function(textRange) |
| { |
| this._lastSelection = textRange; |
| if (this.element.isAncestor(document.activeElement)) |
| this._restoreSelection(textRange); |
| }, |
| |
| _handleFocused: function() |
| { |
| if (this._lastSelection) |
| this.setSelection(this._lastSelection); |
| }, |
| |
| _attachMutationObserver: function() |
| { |
| if (!this._isShowing) |
| return; |
| |
| if (this._mutationObserver) |
| this._mutationObserver.disconnect(); |
| this._mutationObserver = new NonLeakingMutationObserver(this._handleMutations.bind(this)); |
| this._mutationObserver.observe(this._container, { subtree: true, childList: true, characterData: true }); |
| }, |
| |
| _detachMutationObserver: function() |
| { |
| if (!this._isShowing) |
| return; |
| |
| if (this._mutationObserver) { |
| this._mutationObserver.disconnect(); |
| delete this._mutationObserver; |
| } |
| }, |
| |
| /** |
| * @param {string} mimeType |
| */ |
| set mimeType(mimeType) |
| { |
| this._highlighter.mimeType = mimeType; |
| }, |
| |
| /** |
| * @param {boolean} readOnly |
| * @param {boolean} requestFocus |
| */ |
| setReadOnly: function(readOnly, requestFocus) |
| { |
| if (this._readOnly === readOnly) |
| return; |
| |
| this.beginDomUpdates(); |
| this._readOnly = readOnly; |
| if (this._readOnly) |
| this._container.removeStyleClass("text-editor-editable"); |
| else { |
| this._container.addStyleClass("text-editor-editable"); |
| if (requestFocus) |
| this._updateSelectionOnStartEditing(); |
| } |
| this.endDomUpdates(); |
| }, |
| |
| /** |
| * @return {boolean} |
| */ |
| readOnly: function() |
| { |
| return this._readOnly; |
| }, |
| |
| _handleElementFocus: function() |
| { |
| if (!this._readOnly) |
| this._container.focus(); |
| }, |
| |
| /** |
| * @return {Element} |
| */ |
| defaultFocusedElement: function() |
| { |
| if (this._readOnly) |
| return this.element; |
| return this._container; |
| }, |
| |
| _updateSelectionOnStartEditing: function() |
| { |
| // focus() needs to go first for the case when the last selection was inside the editor and |
| // the "Edit" button was clicked. In this case we bail at the check below, but the |
| // editor does not receive the focus, thus "Esc" does not cancel editing until at least |
| // one change has been made to the editor contents. |
| this._container.focus(); |
| var selection = window.getSelection(); |
| if (selection.rangeCount) { |
| var commonAncestorContainer = selection.getRangeAt(0).commonAncestorContainer; |
| if (this._container.isSelfOrAncestor(commonAncestorContainer)) |
| return; |
| } |
| |
| selection.removeAllRanges(); |
| var range = document.createRange(); |
| range.setStart(this._container, 0); |
| range.setEnd(this._container, 0); |
| selection.addRange(range); |
| }, |
| |
| /** |
| * @param {WebInspector.TextRange} range |
| */ |
| markAndRevealRange: function(range) |
| { |
| if (this._rangeToMark) { |
| var markedLine = this._rangeToMark.startLine; |
| delete this._rangeToMark; |
| // Remove the marked region immediately. |
| this.beginDomUpdates(); |
| var chunk = this.chunkForLine(markedLine); |
| var wasExpanded = chunk.expanded(); |
| chunk.collapse(); |
| chunk.updateCollapsedLineRow(); |
| if (wasExpanded) |
| chunk.expand(); |
| this.endDomUpdates(); |
| } |
| |
| if (range) { |
| this._rangeToMark = range; |
| this.revealLine(range.startLine); |
| var chunk = this.makeLineAChunk(range.startLine); |
| this._paintLines(chunk.startLine, chunk.startLine + 1); |
| if (this._markedRangeElement) |
| this._markedRangeElement.scrollIntoViewIfNeeded(); |
| } |
| delete this._markedRangeElement; |
| }, |
| |
| /** |
| * @param {number} lineNumber |
| */ |
| highlightLine: function(lineNumber) |
| { |
| this.clearLineHighlight(); |
| this._highlightedLine = lineNumber; |
| this.revealLine(lineNumber); |
| |
| if (!this._readOnly) |
| this._restoreSelection(WebInspector.TextRange.createFromLocation(lineNumber, 0), false); |
| |
| this.addDecoration(lineNumber, "webkit-highlighted-line"); |
| }, |
| |
| clearLineHighlight: function() |
| { |
| if (typeof this._highlightedLine === "number") { |
| this.removeDecoration(this._highlightedLine, "webkit-highlighted-line"); |
| delete this._highlightedLine; |
| } |
| }, |
| |
| _freeCachedElements: function() |
| { |
| this._cachedSpans = []; |
| this._cachedTextNodes = []; |
| this._cachedRows = []; |
| }, |
| |
| /** |
| * @param {boolean} redo |
| * @return {boolean} |
| */ |
| _handleUndoRedo: function(redo) |
| { |
| if (this.readOnly()) |
| return false; |
| |
| this.beginUpdates(); |
| |
| var range = redo ? this._textModel.redo() : this._textModel.undo(); |
| |
| this.endUpdates(); |
| |
| // Restore location post-repaint. |
| if (range) |
| this._restoreSelection(range, true); |
| |
| return true; |
| }, |
| |
| /** |
| * @param {boolean} shiftKey |
| * @return {boolean} |
| */ |
| _handleTabKeyPress: function(shiftKey) |
| { |
| if (this.readOnly()) |
| return false; |
| |
| var selection = this.selection(); |
| if (!selection) |
| return false; |
| |
| var range = selection.normalize(); |
| |
| this.beginUpdates(); |
| |
| var newRange; |
| var rangeWasEmpty = range.isEmpty(); |
| if (shiftKey) |
| newRange = this._textModel.unindentLines(range); |
| else { |
| if (rangeWasEmpty) |
| newRange = this._textModel.editRange(range, WebInspector.settings.textEditorIndent.get()); |
| else |
| newRange = this._textModel.indentLines(range); |
| } |
| |
| this.endUpdates(); |
| if (rangeWasEmpty) |
| newRange.startColumn = newRange.endColumn; |
| this._restoreSelection(newRange, true); |
| return true; |
| }, |
| |
| _handleEnterKey: function() |
| { |
| if (this.readOnly()) |
| return false; |
| |
| var range = this.selection(); |
| if (!range) |
| return false; |
| |
| range = range.normalize(); |
| |
| if (range.endColumn === 0) |
| return false; |
| |
| var line = this._textModel.line(range.startLine); |
| var linePrefix = line.substring(0, range.startColumn); |
| var indentMatch = linePrefix.match(/^\s+/); |
| var currentIndent = indentMatch ? indentMatch[0] : ""; |
| |
| var textEditorIndent = WebInspector.settings.textEditorIndent.get(); |
| var indent = WebInspector.TextEditorModel.endsWithBracketRegex.test(linePrefix) ? currentIndent + textEditorIndent : currentIndent; |
| |
| if (!indent) |
| return false; |
| |
| this.beginDomUpdates(); |
| |
| var lineBreak = this._textModel.lineBreak; |
| var newRange; |
| if (range.isEmpty() && line.substr(range.endColumn - 1, 2) === '{}') { |
| // {|} |
| // becomes |
| // { |
| // | |
| // } |
| newRange = this._textModel.editRange(range, lineBreak + indent + lineBreak + currentIndent); |
| newRange.endLine--; |
| newRange.endColumn += textEditorIndent.length; |
| } else |
| newRange = this._textModel.editRange(range, lineBreak + indent); |
| |
| this.endDomUpdates(); |
| this._restoreSelection(newRange.collapseToEnd(), true); |
| |
| return true; |
| }, |
| |
| /** |
| * @param {number} lineNumber |
| * @param {number} chunkNumber |
| * @param {boolean=} createSuffixChunk |
| * @return {Object} |
| */ |
| splitChunkOnALine: function(lineNumber, chunkNumber, createSuffixChunk) |
| { |
| var selection = this.selection(); |
| var chunk = WebInspector.TextEditorChunkedPanel.prototype.splitChunkOnALine.call(this, lineNumber, chunkNumber, createSuffixChunk); |
| this._restoreSelection(selection); |
| return chunk; |
| }, |
| |
| beginDomUpdates: function() |
| { |
| if (!this._domUpdateCoalescingLevel) |
| this._detachMutationObserver(); |
| WebInspector.TextEditorChunkedPanel.prototype.beginDomUpdates.call(this); |
| }, |
| |
| endDomUpdates: function() |
| { |
| WebInspector.TextEditorChunkedPanel.prototype.endDomUpdates.call(this); |
| if (!this._domUpdateCoalescingLevel) |
| this._attachMutationObserver(); |
| }, |
| |
| buildChunks: function() |
| { |
| for (var i = 0; i < this._textModel.linesCount; ++i) |
| this._textModel.removeAttribute(i, "highlight"); |
| |
| WebInspector.TextEditorChunkedPanel.prototype.buildChunks.call(this); |
| }, |
| |
| /** |
| * @param {number} startLine |
| * @param {number} endLine |
| * @return {WebInspector.TextEditorMainChunk} |
| */ |
| createNewChunk: function(startLine, endLine) |
| { |
| return new WebInspector.TextEditorMainChunk(this, startLine, endLine); |
| }, |
| |
| /** |
| * @param {number} fromIndex |
| * @param {number} toIndex |
| */ |
| expandChunks: function(fromIndex, toIndex) |
| { |
| var lastChunk = this._textChunks[toIndex - 1]; |
| var lastVisibleLine = lastChunk.startLine + lastChunk.linesCount; |
| |
| var selection = this.selection(); |
| |
| this._muteHighlightListener = true; |
| this._highlighter.highlight(lastVisibleLine); |
| delete this._muteHighlightListener; |
| |
| WebInspector.TextEditorChunkedPanel.prototype.expandChunks.call(this, fromIndex, toIndex); |
| |
| this._restoreSelection(selection); |
| }, |
| |
| /** |
| * @param {number} fromLine |
| * @param {number} toLine |
| */ |
| _highlightDataReady: function(fromLine, toLine) |
| { |
| if (this._muteHighlightListener) |
| return; |
| this._paintLines(fromLine, toLine, true /*restoreSelection*/); |
| }, |
| |
| /** |
| * @param {number} fromLine |
| * @param {number} toLine |
| * @param {boolean=} restoreSelection |
| */ |
| _paintLines: function(fromLine, toLine, restoreSelection) |
| { |
| var lineRows = []; |
| var chunk; |
| for (var lineNumber = fromLine; lineNumber < toLine; ++lineNumber) { |
| if (!chunk || lineNumber < chunk.startLine || lineNumber >= chunk.startLine + chunk.linesCount) |
| chunk = this.chunkForLine(lineNumber); |
| var lineRow = chunk.expandedLineRow(lineNumber); |
| if (!lineRow) |
| continue; |
| lineRows.push(lineRow); |
| } |
| if (lineRows.length === 0) |
| return; |
| |
| var selection; |
| if (restoreSelection) |
| selection = this.selection(); |
| |
| this._paintLineRows(lineRows); |
| |
| if (restoreSelection) |
| this._restoreSelection(selection); |
| }, |
| |
| /** |
| * @param {Array.<Element>} lineRows |
| */ |
| _paintLineRows: function(lineRows) |
| { |
| var highlight = {}; |
| this.beginDomUpdates(); |
| for(var i = 0; i < this._highlightDescriptors.length; ++i) { |
| var highlightDescriptor = this._highlightDescriptors[i]; |
| this._measureHighlightDescriptor(highlight, lineRows, highlightDescriptor); |
| } |
| |
| for(var i = 0; i < lineRows.length; ++i) |
| this._paintLine(lineRows[i], highlight[lineRows[i].lineNumber]); |
| |
| this.endDomUpdates(); |
| }, |
| |
| /** |
| * @param {Object.<number, Array.<WebInspector.TextEditorMainPanel.LineOverlayHighlight>>} highlight |
| * @param {Array.<Element>} lineRows |
| * @param {WebInspector.TextEditorMainPanel.HighlightDescriptor} highlightDescriptor |
| */ |
| _measureHighlightDescriptor: function(highlight, lineRows, highlightDescriptor) |
| { |
| var rowsToMeasure = []; |
| for(var i = 0; i < lineRows.length; ++i) { |
| var lineRow = lineRows[i]; |
| var line = this._textModel.line(lineRow.lineNumber); |
| var ranges = highlightDescriptor.rangesForLine(lineRow.lineNumber, line); |
| if (ranges.length === 0) |
| continue; |
| |
| this._renderRanges(lineRow, line, ranges); |
| rowsToMeasure.push(lineRow); |
| } |
| |
| for(var i = 0; i < rowsToMeasure.length; ++i) { |
| var lineRow = rowsToMeasure[i]; |
| var lineNumber = lineRow.lineNumber; |
| var metrics = this._measureSpans(lineRow); |
| |
| if (!highlight[lineNumber]) |
| highlight[lineNumber] = []; |
| |
| highlight[lineNumber].push(new WebInspector.TextEditorMainPanel.LineOverlayHighlight(metrics, highlightDescriptor.cssClass())); |
| } |
| }, |
| |
| /** |
| * @param {Element} lineRow |
| * @return {Array.<WebInspector.TextEditorMainPanel.ElementMetrics>} |
| */ |
| _measureSpans: function(lineRow) |
| { |
| var spans = lineRow.getElementsByTagName("span"); |
| var metrics = []; |
| for(var i = 0; i < spans.length; ++i) |
| metrics.push(new WebInspector.TextEditorMainPanel.ElementMetrics(spans[i])); |
| return metrics; |
| }, |
| |
| /** |
| * @param {Element} lineRow |
| * @param {WebInspector.TextEditorMainPanel.LineOverlayHighlight} highlight |
| */ |
| _appendOverlayHighlight: function(lineRow, highlight) |
| { |
| var metrics = highlight.metrics; |
| var cssClass = highlight.cssClass; |
| for(var i = 0; i < metrics.length; ++i) { |
| var highlightSpan = document.createElement("span"); |
| highlightSpan._isOverlayHighlightElement = true; |
| highlightSpan.addStyleClass(cssClass); |
| highlightSpan.style.left = metrics[i].left + "px"; |
| highlightSpan.style.width = metrics[i].width + "px"; |
| highlightSpan.style.height = metrics[i].height + "px"; |
| highlightSpan.addStyleClass("text-editor-overlay-highlight"); |
| lineRow.insertBefore(highlightSpan, lineRow.decorationsElement); |
| } |
| }, |
| |
| /** |
| * @param {Element} lineRow |
| * @param {string} line |
| * @param {Array.<{startColumn: number, endColumn: number, token: ?string}>} ranges |
| * @param {boolean=} splitWhitespaceSequences |
| */ |
| _renderRanges: function(lineRow, line, ranges, splitWhitespaceSequences) |
| { |
| var decorationsElement = lineRow.decorationsElement; |
| |
| if (!decorationsElement) |
| lineRow.removeChildren(); |
| else { |
| while (true) { |
| var child = lineRow.firstChild; |
| if (!child || child === decorationsElement) |
| break; |
| lineRow.removeChild(child); |
| } |
| } |
| |
| if (!line) |
| lineRow.insertBefore(document.createElement("br"), decorationsElement); |
| |
| var plainTextStart = 0; |
| for(var i = 0; i < ranges.length; i++) { |
| var rangeStart = ranges[i].startColumn; |
| var rangeEnd = ranges[i].endColumn; |
| |
| if (plainTextStart < rangeStart) { |
| this._insertTextNodeBefore(lineRow, decorationsElement, line.substring(plainTextStart, rangeStart)); |
| } |
| |
| if (splitWhitespaceSequences && ranges[i].token === "whitespace") |
| this._renderWhitespaceCharsWithFixedSizeSpans(lineRow, decorationsElement, rangeEnd - rangeStart + 1); |
| else |
| this._insertSpanBefore(lineRow, decorationsElement, line.substring(rangeStart, rangeEnd + 1), ranges[i].token ? "webkit-" + ranges[i].token : ""); |
| plainTextStart = rangeEnd + 1; |
| } |
| if (plainTextStart < line.length) { |
| this._insertTextNodeBefore(lineRow, decorationsElement, line.substring(plainTextStart, line.length)); |
| } |
| }, |
| |
| /** |
| * @param {Element} lineRow |
| * @param {Element} decorationsElement |
| * @param {number} length |
| */ |
| _renderWhitespaceCharsWithFixedSizeSpans: function(lineRow, decorationsElement, length) |
| { |
| for (var whitespaceLength = 16; whitespaceLength > 0; whitespaceLength >>= 1) { |
| var cssClass = "webkit-whitespace webkit-whitespace-" + whitespaceLength; |
| for (; length >= whitespaceLength; length -= whitespaceLength) |
| this._insertSpanBefore(lineRow, decorationsElement, WebInspector.TextEditorMainPanel._ConsecutiveWhitespaceChars[whitespaceLength], cssClass); |
| } |
| }, |
| |
| /** |
| * @param {Element} lineRow |
| * @param {Array.<WebInspector.TextEditorMainPanel.LineOverlayHighlight>} overlayHighlight |
| */ |
| _paintLine: function(lineRow, overlayHighlight) |
| { |
| var lineNumber = lineRow.lineNumber; |
| |
| this.beginDomUpdates(); |
| try { |
| var syntaxHighlight = this._textModel.getAttribute(lineNumber, "highlight"); |
| if (!syntaxHighlight) |
| return; |
| |
| var line = this._textModel.line(lineNumber); |
| var ranges = syntaxHighlight.ranges; |
| this._renderRanges(lineRow, line, ranges, this._showWhitespace); |
| |
| if (overlayHighlight) |
| for(var i = 0; i < overlayHighlight.length; ++i) |
| this._appendOverlayHighlight(lineRow, overlayHighlight[i]); |
| } finally { |
| if (this._rangeToMark && this._rangeToMark.startLine === lineNumber) |
| this._markedRangeElement = WebInspector.highlightSearchResult(lineRow, this._rangeToMark.startColumn, this._rangeToMark.endColumn - this._rangeToMark.startColumn); |
| this.endDomUpdates(); |
| } |
| }, |
| |
| /** |
| * @param {Element} lineRow |
| */ |
| _releaseLinesHighlight: function(lineRow) |
| { |
| if (!lineRow) |
| return; |
| if ("spans" in lineRow) { |
| var spans = lineRow.spans; |
| for (var j = 0; j < spans.length; ++j) |
| this._cachedSpans.push(spans[j]); |
| delete lineRow.spans; |
| } |
| if ("textNodes" in lineRow) { |
| var textNodes = lineRow.textNodes; |
| for (var j = 0; j < textNodes.length; ++j) |
| this._cachedTextNodes.push(textNodes[j]); |
| delete lineRow.textNodes; |
| } |
| this._cachedRows.push(lineRow); |
| }, |
| |
| /** |
| * @param {?Node=} lastUndamagedLineRow |
| * @return {WebInspector.TextRange} |
| */ |
| selection: function(lastUndamagedLineRow) |
| { |
| var selection = window.getSelection(); |
| if (!selection.rangeCount) |
| return null; |
| // Selection may be outside of the editor. |
| if (!this._container.isAncestor(selection.anchorNode) || !this._container.isAncestor(selection.focusNode)) |
| return null; |
| // Selection may be inside one of decorations. |
| if (selection.focusNode.enclosingNodeOrSelfWithClass("webkit-line-decorations", this._container)) |
| return null; |
| var start = this._selectionToPosition(selection.anchorNode, selection.anchorOffset, lastUndamagedLineRow); |
| var end = selection.isCollapsed ? start : this._selectionToPosition(selection.focusNode, selection.focusOffset, lastUndamagedLineRow); |
| return new WebInspector.TextRange(start.line, start.column, end.line, end.column); |
| }, |
| |
| lastSelection: function() |
| { |
| return this._lastSelection; |
| }, |
| |
| /** |
| * @param {boolean=} scrollIntoView |
| */ |
| _restoreSelection: function(range, scrollIntoView) |
| { |
| if (!range) |
| return; |
| |
| var start = this._positionToSelection(range.startLine, range.startColumn); |
| var end = range.isEmpty() ? start : this._positionToSelection(range.endLine, range.endColumn); |
| window.getSelection().setBaseAndExtent(start.container, start.offset, end.container, end.offset); |
| |
| if (scrollIntoView) { |
| for (var node = end.container; node; node = node.parentElement) { |
| if (node.scrollIntoViewIfNeeded) { |
| node.scrollIntoViewIfNeeded(); |
| break; |
| } |
| } |
| } |
| this._lastSelection = range; |
| }, |
| |
| /** |
| * @param {Node} container |
| * @param {number} offset |
| * @param {?Node=} lastUndamagedLineRow |
| * @return {{line: number, column: number}} |
| */ |
| _selectionToPosition: function(container, offset, lastUndamagedLineRow) |
| { |
| if (container === this._container && offset === 0) |
| return { line: 0, column: 0 }; |
| if (container === this._container && offset === 1) |
| return { line: this._textModel.linesCount - 1, column: this._textModel.lineLength(this._textModel.linesCount - 1) }; |
| |
| // This method can be called on the damaged DOM (when DOM does not match model). |
| // We need to start counting lines from the first undamaged line if it is given. |
| var lineNumber; |
| var column = 0; |
| var node; |
| var scopeNode; |
| if (lastUndamagedLineRow === null) { |
| // Last undamaged row is given, but is null - force traverse from the beginning |
| node = this._container.firstChild; |
| scopeNode = this._container; |
| lineNumber = 0; |
| } else { |
| var lineRow = this._enclosingLineRowOrSelf(container); |
| if (!lastUndamagedLineRow || (typeof lineRow.lineNumber === "number" && lineRow.lineNumber <= lastUndamagedLineRow.lineNumber)) { |
| // DOM is consistent (or we belong to the first damaged row)- lookup the row we belong to and start with it. |
| node = lineRow; |
| scopeNode = node; |
| lineNumber = node.lineNumber; |
| } else { |
| // Start with the node following undamaged row. It corresponds to lineNumber + 1. |
| node = lastUndamagedLineRow.nextSibling; |
| scopeNode = this._container; |
| lineNumber = lastUndamagedLineRow.lineNumber + 1; |
| } |
| } |
| |
| // Fast return the line start. |
| if (container === node && offset === 0) |
| return { line: lineNumber, column: 0 }; |
| |
| // Traverse text and increment lineNumber / column. |
| for (; node && node !== container; node = node.traverseNextNode(scopeNode)) { |
| if (node.nodeName.toLowerCase() === "br") { |
| lineNumber++; |
| column = 0; |
| } else if (node.nodeType === Node.TEXT_NODE) { |
| var text = node.textContent; |
| for (var i = 0; i < text.length; ++i) { |
| if (text.charAt(i) === "\n") { |
| lineNumber++; |
| column = 0; |
| } else |
| column++; |
| } |
| } |
| } |
| |
| // We reached our container node, traverse within itself until we reach given offset. |
| if (node === container && offset) { |
| var text = node.textContent; |
| // In case offset == 1 and lineRow is a chunk div, we need to traverse it all. |
| var textOffset = (node._chunk && offset === 1) ? text.length : offset; |
| for (var i = 0; i < textOffset; ++i) { |
| if (text.charAt(i) === "\n") { |
| lineNumber++; |
| column = 0; |
| } else |
| column++; |
| } |
| } |
| return { line: lineNumber, column: column }; |
| }, |
| |
| /** |
| * @param {number} line |
| * @param {number} column |
| * @return {{container: Element, offset: number}} |
| */ |
| _positionToSelection: function(line, column) |
| { |
| var chunk = this.chunkForLine(line); |
| // One-lined collapsed chunks may still stay highlighted. |
| var lineRow = chunk.linesCount === 1 ? chunk.element : chunk.expandedLineRow(line); |
| if (lineRow) |
| var rangeBoundary = lineRow.rangeBoundaryForOffset(column); |
| else { |
| var offset = column; |
| for (var i = chunk.startLine; i < line && i < this._textModel.linesCount; ++i) |
| offset += this._textModel.lineLength(i) + 1; // \n |
| lineRow = chunk.element; |
| if (lineRow.firstChild) |
| var rangeBoundary = { container: lineRow.firstChild, offset: offset }; |
| else |
| var rangeBoundary = { container: lineRow, offset: 0 }; |
| } |
| return rangeBoundary; |
| }, |
| |
| /** |
| * @param {Node} element |
| * @return {?Node} |
| */ |
| _enclosingLineRowOrSelf: function(element) |
| { |
| var lineRow = element.enclosingNodeOrSelfWithClass("webkit-line-content"); |
| if (lineRow) |
| return lineRow; |
| |
| for (lineRow = element; lineRow; lineRow = lineRow.parentElement) { |
| if (lineRow.parentElement === this._container) |
| return lineRow; |
| } |
| return null; |
| }, |
| |
| /** |
| * @param {Element} element |
| * @param {Element} oldChild |
| * @param {string} content |
| * @param {string} className |
| */ |
| _insertSpanBefore: function(element, oldChild, content, className) |
| { |
| if (className === "html-resource-link" || className === "html-external-link") { |
| element.insertBefore(this._createLink(content, className === "html-external-link"), oldChild); |
| return; |
| } |
| |
| var span = this._cachedSpans.pop() || document.createElement("span"); |
| span.className = className; |
| if (WebInspector.FALSE) // For paint debugging. |
| span.addStyleClass("debug-fadeout"); |
| span.textContent = content; |
| element.insertBefore(span, oldChild); |
| if (!("spans" in element)) |
| element.spans = []; |
| element.spans.push(span); |
| }, |
| |
| /** |
| * @param {Element} element |
| * @param {Element} oldChild |
| * @param {string} text |
| */ |
| _insertTextNodeBefore: function(element, oldChild, text) |
| { |
| var textNode = this._cachedTextNodes.pop(); |
| if (textNode) |
| textNode.nodeValue = text; |
| else |
| textNode = document.createTextNode(text); |
| element.insertBefore(textNode, oldChild); |
| if (!("textNodes" in element)) |
| element.textNodes = []; |
| element.textNodes.push(textNode); |
| }, |
| |
| /** |
| * @param {string} content |
| * @param {boolean} isExternal |
| * @return {Element} |
| */ |
| _createLink: function(content, isExternal) |
| { |
| var quote = content.charAt(0); |
| if (content.length > 1 && (quote === "\"" || quote === "'")) |
| content = content.substring(1, content.length - 1); |
| else |
| quote = null; |
| |
| var span = document.createElement("span"); |
| span.className = "webkit-html-attribute-value"; |
| if (quote) |
| span.appendChild(document.createTextNode(quote)); |
| span.appendChild(this._delegate.createLink(content, isExternal)); |
| if (quote) |
| span.appendChild(document.createTextNode(quote)); |
| return span; |
| }, |
| |
| /** |
| * @param {Array.<WebKitMutation>} mutations |
| */ |
| _handleMutations: function(mutations) |
| { |
| if (this._readOnly) { |
| delete this._keyDownCode; |
| return; |
| } |
| |
| // Annihilate noop BR addition + removal that takes place upon line removal. |
| var filteredMutations = mutations.slice(); |
| var addedBRs = new Map(); |
| for (var i = 0; i < mutations.length; ++i) { |
| var mutation = mutations[i]; |
| if (mutation.type !== "childList") |
| continue; |
| if (mutation.addedNodes.length === 1 && mutation.addedNodes[0].nodeName === "BR") |
| addedBRs.put(mutation.addedNodes[0], mutation); |
| else if (mutation.removedNodes.length === 1 && mutation.removedNodes[0].nodeName === "BR") { |
| var noopMutation = addedBRs.get(mutation.removedNodes[0]); |
| if (noopMutation) { |
| filteredMutations.remove(mutation); |
| filteredMutations.remove(noopMutation); |
| } |
| } |
| } |
| |
| var dirtyLines; |
| for (var i = 0; i < filteredMutations.length; ++i) { |
| var mutation = filteredMutations[i]; |
| var changedNodes = []; |
| if (mutation.type === "childList" && mutation.addedNodes.length) |
| changedNodes = Array.prototype.slice.call(mutation.addedNodes); |
| else if (mutation.type === "childList" && mutation.removedNodes.length) |
| changedNodes = Array.prototype.slice.call(mutation.removedNodes); |
| changedNodes.push(mutation.target); |
| |
| for (var j = 0; j < changedNodes.length; ++j) { |
| var lines = this._collectDirtyLines(mutation, changedNodes[j]); |
| if (!lines) |
| continue; |
| if (!dirtyLines) { |
| dirtyLines = lines; |
| continue; |
| } |
| dirtyLines.start = Math.min(dirtyLines.start, lines.start); |
| dirtyLines.end = Math.max(dirtyLines.end, lines.end); |
| } |
| } |
| if (dirtyLines) { |
| delete this._rangeToMark; |
| this._applyDomUpdates(dirtyLines); |
| } |
| |
| this._assertDOMMatchesTextModel(); |
| |
| delete this._keyDownCode; |
| }, |
| |
| /** |
| * @param {WebKitMutation} mutation |
| * @param {Node} target |
| * @return {?Object} |
| */ |
| _collectDirtyLines: function(mutation, target) |
| { |
| var lineRow = this._enclosingLineRowOrSelf(target); |
| if (!lineRow) |
| return null; |
| |
| if (lineRow.decorationsElement && lineRow.decorationsElement.isSelfOrAncestor(target)) { |
| if (this._syncDecorationsForLineListener) |
| this._syncDecorationsForLineListener(lineRow.lineNumber); |
| return null; |
| } |
| |
| if (typeof lineRow.lineNumber !== "number") |
| return null; |
| |
| var startLine = lineRow.lineNumber; |
| var endLine = lineRow._chunk ? lineRow._chunk.endLine - 1 : lineRow.lineNumber; |
| return { start: startLine, end: endLine }; |
| }, |
| |
| /** |
| * @param {Object} dirtyLines |
| */ |
| _applyDomUpdates: function(dirtyLines) |
| { |
| var lastUndamagedLineNumber = dirtyLines.start - 1; // Can be -1 |
| var firstUndamagedLineNumber = dirtyLines.end + 1; // Can be this._textModel.linesCount |
| |
| var lastUndamagedLineChunk = lastUndamagedLineNumber >= 0 ? this._textChunks[this.chunkNumberForLine(lastUndamagedLineNumber)] : null; |
| var firstUndamagedLineChunk = firstUndamagedLineNumber < this._textModel.linesCount ? this._textChunks[this.chunkNumberForLine(firstUndamagedLineNumber)] : null; |
| |
| var collectLinesFromNode = lastUndamagedLineChunk ? lastUndamagedLineChunk.lineRowContainingLine(lastUndamagedLineNumber) : null; |
| var collectLinesToNode = firstUndamagedLineChunk ? firstUndamagedLineChunk.lineRowContainingLine(firstUndamagedLineNumber) : null; |
| var lines = this._collectLinesFromDOM(collectLinesFromNode, collectLinesToNode); |
| |
| var startLine = dirtyLines.start; |
| var endLine = dirtyLines.end; |
| |
| var originalSelection = this._lastSelection; |
| var editInfo = this._guessEditRangeBasedOnSelection(startLine, endLine, lines); |
| if (!editInfo) { |
| if (WebInspector.debugDefaultTextEditor) |
| console.warn("Falling back to expensive edit"); |
| var range = new WebInspector.TextRange(startLine, 0, endLine, this._textModel.lineLength(endLine)); |
| if (!lines.length) { |
| // Entire damaged area has collapsed. Replace everything between start and end lines with nothing. |
| editInfo = new WebInspector.DefaultTextEditor.EditInfo(this._textModel.growRangeRight(range), ""); |
| } else |
| editInfo = new WebInspector.DefaultTextEditor.EditInfo(range, lines.join("\n")); |
| } |
| |
| var selection = this.selection(collectLinesFromNode); |
| |
| // Unindent after block |
| if (editInfo.text === "}" && editInfo.range.isEmpty() && selection.isEmpty() && !this._textModel.line(editInfo.range.endLine).trim()) { |
| var offset = this._closingBlockOffset(editInfo.range); |
| if (offset >= 0) { |
| editInfo.range.startColumn = offset; |
| selection.startColumn = offset + 1; |
| selection.endColumn = offset + 1; |
| } |
| } |
| |
| this._textModel.editRange(editInfo.range, editInfo.text, originalSelection); |
| this._restoreSelection(selection); |
| }, |
| |
| /** |
| * @param {number} startLine |
| * @param {number} endLine |
| * @param {Array.<string>} lines |
| * @return {?WebInspector.DefaultTextEditor.EditInfo} |
| */ |
| _guessEditRangeBasedOnSelection: function(startLine, endLine, lines) |
| { |
| // Analyze input data |
| var textInputData = this._textInputData; |
| delete this._textInputData; |
| var isBackspace = this._keyDownCode === WebInspector.KeyboardShortcut.Keys.Backspace.code; |
| var isDelete = this._keyDownCode === WebInspector.KeyboardShortcut.Keys.Delete.code; |
| |
| if (!textInputData && (isDelete || isBackspace)) |
| textInputData = ""; |
| |
| // Return if there is no input data or selection |
| if (typeof textInputData === "undefined" || !this._lastSelection) |
| return null; |
| |
| // Adjust selection based on the keyboard actions (grow for backspace, etc.). |
| textInputData = textInputData || ""; |
| var range = this._lastSelection.normalize(); |
| if (isBackspace && range.isEmpty()) |
| range = this._textModel.growRangeLeft(range); |
| else if (isDelete && range.isEmpty()) |
| range = this._textModel.growRangeRight(range); |
| |
| // Test that selection intersects damaged lines |
| if (startLine > range.endLine || endLine < range.startLine) |
| return null; |
| |
| var replacementLineCount = textInputData.split("\n").length - 1; |
| var lineCountDelta = replacementLineCount - range.linesCount; |
| if (startLine + lines.length - endLine - 1 !== lineCountDelta) |
| return null; |
| |
| // Clone text model of the size that fits both: selection before edit and the damaged lines after edit. |
| var cloneFromLine = Math.min(range.startLine, startLine); |
| var postLastLine = startLine + lines.length + lineCountDelta; |
| var cloneToLine = Math.min(Math.max(postLastLine, range.endLine) + 1, this._textModel.linesCount); |
| var domModel = this._textModel.slice(cloneFromLine, cloneToLine); |
| domModel.editRange(range.shift(-cloneFromLine), textInputData); |
| |
| // Then we'll test if this new model matches the DOM lines. |
| for (var i = 0; i < lines.length; ++i) { |
| if (domModel.line(i + startLine - cloneFromLine) !== lines[i]) |
| return null; |
| } |
| return new WebInspector.DefaultTextEditor.EditInfo(range, textInputData); |
| }, |
| |
| _assertDOMMatchesTextModel: function() |
| { |
| if (!WebInspector.debugDefaultTextEditor) |
| return; |
| |
| console.assert(this.element.innerText === this._textModel.text() + "\n", "DOM does not match model."); |
| for (var lineRow = this._container.firstChild; lineRow; lineRow = lineRow.nextSibling) { |
| var lineNumber = lineRow.lineNumber; |
| if (typeof lineNumber !== "number") { |
| console.warn("No line number on line row"); |
| continue; |
| } |
| if (lineRow._chunk) { |
| var chunk = lineRow._chunk; |
| console.assert(lineNumber === chunk.startLine); |
| var chunkText = this._textModel.copyRange(new WebInspector.TextRange(chunk.startLine, 0, chunk.endLine - 1, this._textModel.lineLength(chunk.endLine - 1))); |
| if (chunkText !== lineRow.textContent) |
| console.warn("Chunk is not matching: %d %O", lineNumber, lineRow); |
| } else if (this._textModel.line(lineNumber) !== lineRow.textContent) |
| console.warn("Line is not matching: %d %O", lineNumber, lineRow); |
| } |
| }, |
| |
| /** |
| * @param {WebInspector.TextRange} oldRange |
| * @return {number} |
| */ |
| _closingBlockOffset: function(oldRange) |
| { |
| var leftBrace = this._braceMatcher.findLeftCandidate(oldRange.startLine, oldRange.startColumn); |
| if (!leftBrace || leftBrace.token !== "block-start") |
| return -1; |
| var lineContent = this._textModel.line(leftBrace.lineNumber); |
| return lineContent.length - lineContent.trimLeft().length; |
| }, |
| |
| /** |
| * @param {WebInspector.TextRange} oldRange |
| * @param {WebInspector.TextRange} newRange |
| */ |
| textChanged: function(oldRange, newRange) |
| { |
| this.beginDomUpdates(); |
| this._removeDecorationsInRange(oldRange); |
| this._updateChunksForRanges(oldRange, newRange); |
| this._updateHighlightsForRange(newRange); |
| this.endDomUpdates(); |
| }, |
| |
| /** |
| * @param {WebInspector.TextRange} range |
| */ |
| _removeDecorationsInRange: function(range) |
| { |
| for (var i = this.chunkNumberForLine(range.startLine); i < this._textChunks.length; ++i) { |
| var chunk = this._textChunks[i]; |
| if (chunk.startLine > range.endLine) |
| break; |
| chunk.removeAllDecorations(); |
| } |
| }, |
| |
| /** |
| * @param {WebInspector.TextRange} oldRange |
| * @param {WebInspector.TextRange} newRange |
| */ |
| _updateChunksForRanges: function(oldRange, newRange) |
| { |
| var firstDamagedChunkNumber = this.chunkNumberForLine(oldRange.startLine); |
| var lastDamagedChunkNumber = firstDamagedChunkNumber; |
| while (lastDamagedChunkNumber + 1 < this._textChunks.length) { |
| if (this._textChunks[lastDamagedChunkNumber + 1].startLine > oldRange.endLine) |
| break; |
| ++lastDamagedChunkNumber; |
| } |
| |
| var firstDamagedChunk = this._textChunks[firstDamagedChunkNumber]; |
| var lastDamagedChunk = this._textChunks[lastDamagedChunkNumber]; |
| |
| var linesDiff = newRange.linesCount - oldRange.linesCount; |
| |
| // First, detect chunks that have not been modified and simply shift them. |
| if (linesDiff) { |
| for (var chunkNumber = lastDamagedChunkNumber + 1; chunkNumber < this._textChunks.length; ++chunkNumber) |
| this._textChunks[chunkNumber].startLine += linesDiff; |
| } |
| |
| // Remove damaged chunks from DOM and from textChunks model. |
| var lastUndamagedChunk = firstDamagedChunkNumber > 0 ? this._textChunks[firstDamagedChunkNumber - 1] : null; |
| var firstUndamagedChunk = lastDamagedChunkNumber + 1 < this._textChunks.length ? this._textChunks[lastDamagedChunkNumber + 1] : null; |
| |
| var removeDOMFromNode = lastUndamagedChunk ? lastUndamagedChunk.lastElement().nextSibling : this._container.firstChild; |
| var removeDOMToNode = firstUndamagedChunk ? firstUndamagedChunk.firstElement() : null; |
| |
| // Fast case - patch single expanded chunk that did not grow / shrink during edit. |
| if (!linesDiff && firstDamagedChunk === lastDamagedChunk && firstDamagedChunk._expandedLineRows) { |
| var lastUndamagedLineRow = lastDamagedChunk.expandedLineRow(oldRange.startLine - 1); |
| var firstUndamagedLineRow = firstDamagedChunk.expandedLineRow(oldRange.endLine + 1); |
| var localRemoveDOMFromNode = lastUndamagedLineRow ? lastUndamagedLineRow.nextSibling : removeDOMFromNode; |
| var localRemoveDOMToNode = firstUndamagedLineRow || removeDOMToNode; |
| removeSubsequentNodes(localRemoveDOMFromNode, localRemoveDOMToNode); |
| for (var i = newRange.startLine; i < newRange.endLine + 1; ++i) { |
| var row = firstDamagedChunk._createRow(i); |
| firstDamagedChunk._expandedLineRows[i - firstDamagedChunk.startLine] = row; |
| this._container.insertBefore(row, localRemoveDOMToNode); |
| } |
| firstDamagedChunk.updateCollapsedLineRow(); |
| this._assertDOMMatchesTextModel(); |
| return; |
| } |
| |
| removeSubsequentNodes(removeDOMFromNode, removeDOMToNode); |
| this._textChunks.splice(firstDamagedChunkNumber, lastDamagedChunkNumber - firstDamagedChunkNumber + 1); |
| |
| // Compute damaged chunks span |
| var startLine = firstDamagedChunk.startLine; |
| var endLine = lastDamagedChunk.endLine + linesDiff; |
| var lineSpan = endLine - startLine; |
| |
| // Re-create chunks for damaged area. |
| var insertionIndex = firstDamagedChunkNumber; |
| var chunkSize = Math.ceil(lineSpan / Math.ceil(lineSpan / this._defaultChunkSize)); |
| |
| for (var i = startLine; i < endLine; i += chunkSize) { |
| var chunk = this.createNewChunk(i, Math.min(endLine, i + chunkSize)); |
| this._textChunks.splice(insertionIndex++, 0, chunk); |
| this._container.insertBefore(chunk.element, removeDOMToNode); |
| } |
| |
| this._assertDOMMatchesTextModel(); |
| }, |
| |
| /** |
| * @param {WebInspector.TextRange} range |
| */ |
| _updateHighlightsForRange: function(range) |
| { |
| var visibleFrom = this.scrollTop(); |
| var visibleTo = visibleFrom + this.clientHeight(); |
| |
| var result = this.findVisibleChunks(visibleFrom, visibleTo); |
| var chunk = this._textChunks[result.end - 1]; |
| var lastVisibleLine = chunk.startLine + chunk.linesCount; |
| |
| lastVisibleLine = Math.max(lastVisibleLine, range.endLine + 1); |
| lastVisibleLine = Math.min(lastVisibleLine, this._textModel.linesCount); |
| |
| var updated = this._highlighter.updateHighlight(range.startLine, lastVisibleLine); |
| if (!updated) { |
| // Highlights for the chunks below are invalid, so just collapse them. |
| for (var i = this.chunkNumberForLine(range.startLine); i < this._textChunks.length; ++i) |
| this._textChunks[i].collapse(); |
| } |
| |
| this.repaintAll(); |
| }, |
| |
| /** |
| * @param {Node} from |
| * @param {Node} to |
| * @return {Array.<string>} |
| */ |
| _collectLinesFromDOM: function(from, to) |
| { |
| var textContents = []; |
| var hasContent = false; |
| for (var node = from ? from.nextSibling : this._container; node && node !== to; node = node.traverseNextNode(this._container)) { |
| // Skip all children of the decoration container and overlay highlight spans. |
| while (node && node !== to && (node._isDecorationsElement || node._isOverlayHighlightElement)) |
| node = node.nextSibling; |
| if (!node || node === to) |
| break; |
| |
| hasContent = true; |
| if (node.nodeName.toLowerCase() === "br") |
| textContents.push("\n"); |
| else if (node.nodeType === Node.TEXT_NODE) |
| textContents.push(node.textContent); |
| } |
| if (!hasContent) |
| return []; |
| |
| var textContent = textContents.join(""); |
| // The last \n (if any) does not "count" in a DIV. |
| textContent = textContent.replace(/\n$/, ""); |
| |
| return textContent.split("\n"); |
| }, |
| |
| /** |
| * @param {Event} event |
| */ |
| _handleSelectionChange: function(event) |
| { |
| var textRange = this.selection(); |
| if (textRange) |
| this._lastSelection = textRange; |
| |
| this._tokenHighlighter.handleSelectionChange(textRange); |
| this._braceHighlighter.handleSelectionChange(textRange); |
| this._delegate.selectionChanged(textRange); |
| }, |
| |
| /** |
| * @param {Event} event |
| */ |
| _handleTextInput: function(event) |
| { |
| this._textInputData = event.data; |
| }, |
| |
| /** |
| * @param {number} shortcutKey |
| * @param {Event} event |
| */ |
| handleKeyDown: function(shortcutKey, event) |
| { |
| var handler = this._shortcuts[shortcutKey]; |
| if (handler && handler()) { |
| event.consume(true); |
| return; |
| } |
| |
| this._keyDownCode = event.keyCode; |
| }, |
| |
| /** |
| * @param {Event} event |
| */ |
| _handleCut: function(event) |
| { |
| this._keyDownCode = WebInspector.KeyboardShortcut.Keys.Delete.code; |
| }, |
| |
| /** |
| * @param {number} scrollTop |
| * @param {number} clientHeight |
| * @param {number} chunkSize |
| */ |
| overrideViewportForTest: function(scrollTop, clientHeight, chunkSize) |
| { |
| this._scrollTopOverrideForTest = scrollTop; |
| this._clientHeightOverrideForTest = clientHeight; |
| this._defaultChunkSize = chunkSize; |
| }, |
| |
| __proto__: WebInspector.TextEditorChunkedPanel.prototype |
| } |
| |
| /** |
| * @interface |
| */ |
| WebInspector.TextEditorMainPanel.HighlightDescriptor = function() { } |
| |
| WebInspector.TextEditorMainPanel.HighlightDescriptor.prototype = { |
| /** |
| * @param {number} lineNumber |
| * @param {string} line |
| * @return {boolean} |
| */ |
| affectsLine: function(lineNumber, line) { return false; }, |
| |
| /** |
| * @param {number} lineNumber |
| * @param {string} line |
| * @return {Array.<{startColumn: number, endColumn: number}>} |
| */ |
| rangesForLine: function(lineNumber, line) { return []; }, |
| |
| /** |
| * @return {string} |
| */ |
| cssClass: function() { return ""; }, |
| } |
| |
| /** |
| * @constructor |
| * @implements {WebInspector.TextEditorMainPanel.HighlightDescriptor} |
| */ |
| WebInspector.TextEditorMainPanel.RegexHighlightDescriptor = function(regex, cssClass) |
| { |
| this._cssClass = cssClass; |
| this._regex = regex; |
| } |
| |
| WebInspector.TextEditorMainPanel.RegexHighlightDescriptor.prototype = { |
| /** |
| * @param {number} lineNumber |
| * @param {string} line |
| * @return {boolean} |
| */ |
| affectsLine: function(lineNumber, line) |
| { |
| this._regex.lastIndex = 0; |
| return this._regex.test(line); |
| }, |
| |
| /** |
| * @param {number} lineNumber |
| * @param {string} line |
| * @return {Array.<{startColumn: number, endColumn: number}>} |
| */ |
| rangesForLine: function(lineNumber, line) |
| { |
| var ranges = []; |
| var regexResult; |
| this._regex.lastIndex = 0; |
| while (regexResult = this._regex.exec(line)) { |
| ranges.push({ |
| startColumn: regexResult.index, |
| endColumn: regexResult.index + regexResult[0].length - 1 |
| }); |
| } |
| return ranges; |
| }, |
| |
| /** |
| * @return {string} |
| */ |
| cssClass: function() |
| { |
| return this._cssClass; |
| } |
| } |
| |
| /** |
| * @constructor |
| * @implements {WebInspector.TextEditorMainPanel.HighlightDescriptor} |
| * @param {WebInspector.TextRange} range |
| * @param {string} cssClass |
| */ |
| WebInspector.TextEditorMainPanel.RangeHighlightDescriptor = function(range, cssClass) |
| { |
| this._cssClass = cssClass; |
| this._range = range; |
| } |
| |
| WebInspector.TextEditorMainPanel.RangeHighlightDescriptor.prototype = { |
| /** |
| * @param {number} lineNumber |
| * @param {string} line |
| * @return {boolean} |
| */ |
| affectsLine: function(lineNumber, line) |
| { |
| return this._range.startLine <= lineNumber && lineNumber <= this._range.endLine && line.length > 0; |
| }, |
| |
| /** |
| * @param {number} lineNumber |
| * @param {string} line |
| * @return {Array.<{startColumn: number, endColumn: number}>} |
| */ |
| rangesForLine: function(lineNumber, line) |
| { |
| if (!this.affectsLine(lineNumber, line)) |
| return []; |
| |
| var startColumn = lineNumber === this._range.startLine ? this._range.startColumn : 0; |
| var endColumn = lineNumber === this._range.endLine ? Math.min(this._range.endColumn, line.length) : line.length; |
| return [{ |
| startColumn: startColumn, |
| endColumn: endColumn |
| }]; |
| }, |
| |
| /** |
| * @return {string} |
| */ |
| cssClass: function() |
| { |
| return this._cssClass; |
| } |
| } |
| |
| /** |
| * @constructor |
| * @param {Element} element |
| */ |
| WebInspector.TextEditorMainPanel.ElementMetrics = function(element) |
| { |
| this.width = element.offsetWidth; |
| this.height = element.offsetHeight; |
| this.left = element.offsetLeft; |
| } |
| |
| /** |
| * @constructor |
| * @param {Array.<WebInspector.TextEditorMainPanel.ElementMetrics>} metrics |
| * @param {string} cssClass |
| */ |
| WebInspector.TextEditorMainPanel.LineOverlayHighlight = function(metrics, cssClass) |
| { |
| this.metrics = metrics; |
| this.cssClass = cssClass; |
| } |
| |
| /** |
| * @constructor |
| * @param {WebInspector.TextEditorChunkedPanel} chunkedPanel |
| * @param {number} startLine |
| * @param {number} endLine |
| */ |
| WebInspector.TextEditorMainChunk = function(chunkedPanel, startLine, endLine) |
| { |
| this._chunkedPanel = chunkedPanel; |
| this._textModel = chunkedPanel._textModel; |
| |
| this.element = document.createElement("div"); |
| this.element.lineNumber = startLine; |
| this.element.className = "webkit-line-content"; |
| this.element._chunk = this; |
| |
| this._startLine = startLine; |
| endLine = Math.min(this._textModel.linesCount, endLine); |
| this.linesCount = endLine - startLine; |
| |
| this._expanded = false; |
| |
| this.updateCollapsedLineRow(); |
| } |
| |
| WebInspector.TextEditorMainChunk.prototype = { |
| /** |
| * @param {Element|string} decoration |
| */ |
| addDecoration: function(decoration) |
| { |
| this._chunkedPanel.beginDomUpdates(); |
| if (typeof decoration === "string") |
| this.element.addStyleClass(decoration); |
| else { |
| if (!this.element.decorationsElement) { |
| this.element.decorationsElement = document.createElement("div"); |
| this.element.decorationsElement.className = "webkit-line-decorations"; |
| this.element.decorationsElement._isDecorationsElement = true; |
| this.element.appendChild(this.element.decorationsElement); |
| } |
| this.element.decorationsElement.appendChild(decoration); |
| } |
| this._chunkedPanel.endDomUpdates(); |
| }, |
| |
| /** |
| * @param {string|Element} decoration |
| */ |
| removeDecoration: function(decoration) |
| { |
| this._chunkedPanel.beginDomUpdates(); |
| if (typeof decoration === "string") |
| this.element.removeStyleClass(decoration); |
| else if (this.element.decorationsElement) |
| this.element.decorationsElement.removeChild(decoration); |
| this._chunkedPanel.endDomUpdates(); |
| }, |
| |
| removeAllDecorations: function() |
| { |
| this._chunkedPanel.beginDomUpdates(); |
| this.element.className = "webkit-line-content"; |
| if (this.element.decorationsElement) { |
| if (this.element.decorationsElement.parentElement) |
| this.element.removeChild(this.element.decorationsElement); |
| delete this.element.decorationsElement; |
| } |
| this._chunkedPanel.endDomUpdates(); |
| }, |
| |
| /** |
| * @return {boolean} |
| */ |
| isDecorated: function() |
| { |
| return this.element.className !== "webkit-line-content" || !!(this.element.decorationsElement && this.element.decorationsElement.firstChild); |
| }, |
| |
| /** |
| * @return {number} |
| */ |
| get startLine() |
| { |
| return this._startLine; |
| }, |
| |
| /** |
| * @return {number} |
| */ |
| get endLine() |
| { |
| return this._startLine + this.linesCount; |
| }, |
| |
| set startLine(startLine) |
| { |
| this._startLine = startLine; |
| this.element.lineNumber = startLine; |
| if (this._expandedLineRows) { |
| for (var i = 0; i < this._expandedLineRows.length; ++i) |
| this._expandedLineRows[i].lineNumber = startLine + i; |
| } |
| }, |
| |
| /** |
| * @return {boolean} |
| */ |
| expanded: function() |
| { |
| return this._expanded; |
| }, |
| |
| expand: function() |
| { |
| if (this._expanded) |
| return; |
| |
| this._expanded = true; |
| |
| if (this.linesCount === 1) { |
| this._chunkedPanel._paintLines(this.startLine, this.startLine + 1); |
| return; |
| } |
| |
| this._chunkedPanel.beginDomUpdates(); |
| |
| this._expandedLineRows = []; |
| var parentElement = this.element.parentElement; |
| for (var i = this.startLine; i < this.startLine + this.linesCount; ++i) { |
| var lineRow = this._createRow(i); |
| parentElement.insertBefore(lineRow, this.element); |
| this._expandedLineRows.push(lineRow); |
| } |
| parentElement.removeChild(this.element); |
| this._chunkedPanel._paintLines(this.startLine, this.startLine + this.linesCount); |
| |
| this._chunkedPanel.endDomUpdates(); |
| }, |
| |
| collapse: function() |
| { |
| if (!this._expanded) |
| return; |
| |
| this._expanded = false; |
| if (this.linesCount === 1) |
| return; |
| |
| this._chunkedPanel.beginDomUpdates(); |
| |
| var elementInserted = false; |
| for (var i = 0; i < this._expandedLineRows.length; ++i) { |
| var lineRow = this._expandedLineRows[i]; |
| var parentElement = lineRow.parentElement; |
| if (parentElement) { |
| if (!elementInserted) { |
| elementInserted = true; |
| parentElement.insertBefore(this.element, lineRow); |
| } |
| parentElement.removeChild(lineRow); |
| } |
| this._chunkedPanel._releaseLinesHighlight(lineRow); |
| } |
| delete this._expandedLineRows; |
| |
| this._chunkedPanel.endDomUpdates(); |
| }, |
| |
| /** |
| * @return {number} |
| */ |
| get height() |
| { |
| if (!this._expandedLineRows) |
| return this._chunkedPanel.totalHeight(this.element); |
| return this._chunkedPanel.totalHeight(this._expandedLineRows[0], this._expandedLineRows[this._expandedLineRows.length - 1]); |
| }, |
| |
| /** |
| * @return {number} |
| */ |
| get offsetTop() |
| { |
| return (this._expandedLineRows && this._expandedLineRows.length) ? this._expandedLineRows[0].offsetTop : this.element.offsetTop; |
| }, |
| |
| /** |
| * @param {number} lineNumber |
| * @return {Element} |
| */ |
| _createRow: function(lineNumber) |
| { |
| var lineRow = this._chunkedPanel._cachedRows.pop() || document.createElement("div"); |
| lineRow.lineNumber = lineNumber; |
| lineRow.className = "webkit-line-content"; |
| lineRow.textContent = this._textModel.line(lineNumber); |
| if (!lineRow.textContent) |
| lineRow.appendChild(document.createElement("br")); |
| return lineRow; |
| }, |
| |
| /** |
| * Called on potentially damaged / inconsistent chunk |
| * @param {number} lineNumber |
| * @return {?Node} |
| */ |
| lineRowContainingLine: function(lineNumber) |
| { |
| if (!this._expanded) |
| return this.element; |
| return this.expandedLineRow(lineNumber); |
| }, |
| |
| /** |
| * @param {number} lineNumber |
| * @return {Element} |
| */ |
| expandedLineRow: function(lineNumber) |
| { |
| if (!this._expanded || lineNumber < this.startLine || lineNumber >= this.startLine + this.linesCount) |
| return null; |
| if (!this._expandedLineRows) |
| return this.element; |
| return this._expandedLineRows[lineNumber - this.startLine]; |
| }, |
| |
| updateCollapsedLineRow: function() |
| { |
| if (this.linesCount === 1 && this._expanded) |
| return; |
| |
| var lines = []; |
| for (var i = this.startLine; i < this.startLine + this.linesCount; ++i) |
| lines.push(this._textModel.line(i)); |
| |
| if (WebInspector.FALSE) |
| console.log("Rebuilding chunk with " + lines.length + " lines"); |
| |
| this.element.removeChildren(); |
| this.element.textContent = lines.join("\n"); |
| // The last empty line will get swallowed otherwise. |
| if (!lines[lines.length - 1]) |
| this.element.appendChild(document.createElement("br")); |
| }, |
| |
| firstElement: function() |
| { |
| return this._expandedLineRows ? this._expandedLineRows[0] : this.element; |
| }, |
| |
| /** |
| * @return {Element} |
| */ |
| lastElement: function() |
| { |
| return this._expandedLineRows ? this._expandedLineRows[this._expandedLineRows.length - 1] : this.element; |
| } |
| } |
| |
| /** |
| * @constructor |
| * @param {WebInspector.TextEditorMainPanel} mainPanel |
| * @param {WebInspector.TextEditorModel} textModel |
| */ |
| WebInspector.TextEditorMainPanel.TokenHighlighter = function(mainPanel, textModel) |
| { |
| this._mainPanel = mainPanel; |
| this._textModel = textModel; |
| } |
| |
| WebInspector.TextEditorMainPanel.TokenHighlighter.prototype = { |
| /** |
| * @param {WebInspector.TextRange} range |
| */ |
| handleSelectionChange: function(range) |
| { |
| if (!range) { |
| this._removeHighlight(); |
| return; |
| } |
| |
| if (range.startLine !== range.endLine) { |
| this._removeHighlight(); |
| return; |
| } |
| |
| range = range.normalize(); |
| var selectedText = this._textModel.copyRange(range); |
| if (selectedText === this._selectedWord) |
| return; |
| |
| if (selectedText === "") { |
| this._removeHighlight(); |
| return; |
| } |
| |
| if (this._isWord(range, selectedText)) |
| this._highlight(selectedText); |
| else |
| this._removeHighlight(); |
| }, |
| |
| /** |
| * @param {string} word |
| */ |
| _regexString: function(word) |
| { |
| return "\\b" + word + "\\b"; |
| }, |
| |
| /** |
| * @param {string} selectedWord |
| */ |
| _highlight: function(selectedWord) |
| { |
| this._removeHighlight(); |
| this._selectedWord = selectedWord; |
| this._highlightDescriptor = this._mainPanel.highlightRegex(this._regexString(selectedWord), "text-editor-token-highlight") |
| }, |
| |
| _removeHighlight: function() |
| { |
| if (this._selectedWord) { |
| this._mainPanel.removeHighlight(this._highlightDescriptor); |
| delete this._selectedWord; |
| delete this._highlightDescriptor; |
| } |
| }, |
| |
| /** |
| * @param {WebInspector.TextRange} range |
| * @param {string} selectedText |
| * @return {boolean} |
| */ |
| _isWord: function(range, selectedText) |
| { |
| var line = this._textModel.line(range.startLine); |
| var leftBound = range.startColumn === 0 || !WebInspector.TextUtils.isWordChar(line.charAt(range.startColumn - 1)); |
| var rightBound = range.endColumn === line.length || !WebInspector.TextUtils.isWordChar(line.charAt(range.endColumn)); |
| return leftBound && rightBound && WebInspector.TextUtils.isWord(selectedText); |
| } |
| } |
| |
| /** |
| * @constructor |
| * @param {WebInspector.TextEditorModel} textModel |
| * @param {WebInspector.TextEditor} textEditor |
| */ |
| WebInspector.DefaultTextEditor.WordMovementController = function(textEditor, textModel) |
| { |
| this._textModel = textModel; |
| this._textEditor = textEditor; |
| } |
| |
| WebInspector.DefaultTextEditor.WordMovementController.prototype = { |
| |
| /** |
| * @param {Object.<number, function()>} shortcuts |
| */ |
| _registerShortcuts: function(shortcuts) |
| { |
| var keys = WebInspector.KeyboardShortcut.Keys; |
| var modifiers = WebInspector.KeyboardShortcut.Modifiers; |
| |
| const wordJumpModifier = WebInspector.isMac() ? modifiers.Alt : modifiers.Ctrl; |
| shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Backspace.code, wordJumpModifier)] = this._handleCtrlBackspace.bind(this); |
| shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Left.code, wordJumpModifier)] = this._handleCtrlArrow.bind(this, "left"); |
| shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Right.code, wordJumpModifier)] = this._handleCtrlArrow.bind(this, "right"); |
| shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Left.code, modifiers.Shift | wordJumpModifier)] = this._handleCtrlShiftArrow.bind(this, "left"); |
| shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Right.code, modifiers.Shift | wordJumpModifier)] = this._handleCtrlShiftArrow.bind(this, "right"); |
| }, |
| |
| /** |
| * @param {WebInspector.TextRange} selection |
| * @param {string} direction |
| * @return {WebInspector.TextRange} |
| */ |
| _rangeForCtrlArrowMove: function(selection, direction) |
| { |
| const isStopChar = WebInspector.TextUtils.isStopChar; |
| const isSpaceChar = WebInspector.TextUtils.isSpaceChar; |
| |
| var lineNumber = selection.endLine; |
| var column = selection.endColumn; |
| if (direction === "left") |
| --column; |
| |
| if (column === -1 && direction === "left") { |
| if (lineNumber > 0) |
| return new WebInspector.TextRange(selection.startLine, selection.startColumn, lineNumber - 1, this._textModel.line(lineNumber - 1).length); |
| else |
| return selection.clone(); |
| } |
| |
| var line = this._textModel.line(lineNumber); |
| if (column === line.length && direction === "right") { |
| if (lineNumber + 1 < this._textModel.linesCount) |
| return new WebInspector.TextRange(selection.startLine, selection.startColumn, selection.endLine + 1, 0); |
| else |
| return selection.clone(); |
| } |
| |
| var delta = direction === "left" ? -1 : +1; |
| var directionDependentEndColumnOffset = (delta + 1) / 2; |
| |
| if (isSpaceChar(line.charAt(column))) { |
| while(column + delta >= 0 && column + delta < line.length && isSpaceChar(line.charAt(column + delta))) |
| column += delta; |
| if (column + delta < 0 || column + delta === line.length) |
| return new WebInspector.TextRange(selection.startLine, selection.startColumn, lineNumber, column + directionDependentEndColumnOffset); |
| else |
| column += delta; |
| } |
| |
| var group = isStopChar(line.charAt(column)); |
| |
| while(column + delta >= 0 && column + delta < line.length && isStopChar(line.charAt(column + delta)) === group && !isSpaceChar(line.charAt(column + delta))) |
| column += delta; |
| |
| return new WebInspector.TextRange(selection.startLine, selection.startColumn, lineNumber, column + directionDependentEndColumnOffset); |
| }, |
| |
| /** |
| * @param {string} direction |
| * @return {boolean} |
| */ |
| _handleCtrlArrow: function(direction) |
| { |
| var newSelection = this._rangeForCtrlArrowMove(this._textEditor.selection(), direction); |
| this._textEditor.setSelection(newSelection.collapseToEnd()); |
| return true; |
| }, |
| |
| /** |
| * @param {string} direction |
| * @return {boolean} |
| */ |
| _handleCtrlShiftArrow: function(direction) |
| { |
| this._textEditor.setSelection(this._rangeForCtrlArrowMove(this._textEditor.selection(), direction)); |
| return true; |
| }, |
| |
| /** |
| * @return {boolean} |
| */ |
| _handleCtrlBackspace: function() |
| { |
| var selection = this._textEditor.selection(); |
| if (!selection.isEmpty()) |
| return false; |
| |
| var newSelection = this._rangeForCtrlArrowMove(selection, "left"); |
| this._textModel.editRange(newSelection.normalize(), "", selection); |
| |
| this._textEditor.setSelection(newSelection.collapseToEnd()); |
| return true; |
| } |
| } |
| |
| /** |
| * @constructor |
| * @param {WebInspector.TextEditorMainPanel} textEditor |
| * @param {WebInspector.TextEditorModel} textModel |
| * @param {WebInspector.TextEditorModel.BraceMatcher} braceMatcher |
| */ |
| WebInspector.TextEditorMainPanel.BraceHighlightController = function(textEditor, textModel, braceMatcher) |
| { |
| this._textEditor = textEditor; |
| this._textModel = textModel; |
| this._braceMatcher = braceMatcher; |
| this._highlightDescriptors = []; |
| } |
| |
| WebInspector.TextEditorMainPanel.BraceHighlightController.prototype = { |
| /** |
| * @param {string} line |
| * @param {number} column |
| * @return {number} |
| */ |
| activeBraceColumnForCursorPosition: function(line, column) |
| { |
| var char = line.charAt(column); |
| if (WebInspector.TextUtils.isOpeningBraceChar(char)) |
| return column; |
| |
| var previousChar = line.charAt(column - 1); |
| if (WebInspector.TextUtils.isBraceChar(previousChar)) |
| return column - 1; |
| |
| if (WebInspector.TextUtils.isBraceChar(char)) |
| return column; |
| else |
| return -1; |
| }, |
| |
| /** |
| * @param {WebInspector.TextRange} selectionRange |
| */ |
| handleSelectionChange: function(selectionRange) |
| { |
| if (!selectionRange || !selectionRange.isEmpty()) { |
| this._removeHighlight(); |
| return; |
| } |
| |
| if (this._highlightedRange && this._highlightedRange.compareTo(selectionRange) === 0) |
| return; |
| |
| this._removeHighlight(); |
| var lineNumber = selectionRange.startLine; |
| var column = selectionRange.startColumn; |
| var line = this._textModel.line(lineNumber); |
| column = this.activeBraceColumnForCursorPosition(line, column); |
| if (column < 0) |
| return; |
| |
| var enclosingBraces = this._braceMatcher.enclosingBraces(lineNumber, column); |
| if (!enclosingBraces) |
| return; |
| |
| this._highlightedRange = selectionRange; |
| this._highlightDescriptors.push(this._textEditor.highlightRange(WebInspector.TextRange.createFromLocation(enclosingBraces.leftBrace.lineNumber, enclosingBraces.leftBrace.column), "text-editor-brace-match")); |
| this._highlightDescriptors.push(this._textEditor.highlightRange(WebInspector.TextRange.createFromLocation(enclosingBraces.rightBrace.lineNumber, enclosingBraces.rightBrace.column), "text-editor-brace-match")); |
| }, |
| |
| _removeHighlight: function() |
| { |
| if (!this._highlightDescriptors.length) |
| return; |
| |
| for(var i = 0; i < this._highlightDescriptors.length; ++i) |
| this._textEditor.removeHighlight(this._highlightDescriptors[i]); |
| |
| this._highlightDescriptors = []; |
| delete this._highlightedRange; |
| } |
| } |
| |
| /** |
| * @constructor |
| * @param {WebInspector.TextEditorMainPanel} mainPanel |
| * @param {WebInspector.TextEditorModel} textModel |
| * @param {WebInspector.TextEditorModel.BraceMatcher} braceMatcher |
| */ |
| WebInspector.TextEditorMainPanel.SmartBraceController = function(mainPanel, textModel, braceMatcher) |
| { |
| this._mainPanel = mainPanel; |
| this._textModel = textModel; |
| this._braceMatcher = braceMatcher |
| } |
| |
| WebInspector.TextEditorMainPanel.SmartBraceController.prototype = { |
| /** |
| * @param {Object.<number, function()>} shortcuts |
| */ |
| registerShortcuts: function(shortcuts) |
| { |
| if (!WebInspector.experimentsSettings.textEditorSmartBraces.isEnabled()) |
| return; |
| |
| var keys = WebInspector.KeyboardShortcut.Keys; |
| var modifiers = WebInspector.KeyboardShortcut.Modifiers; |
| |
| shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Backspace.code, modifiers.None)] = this._handleBackspace.bind(this); |
| }, |
| |
| /** |
| * @param {Object.<string, function()>} charOverrides |
| */ |
| registerCharOverrides: function(charOverrides) |
| { |
| if (!WebInspector.experimentsSettings.textEditorSmartBraces.isEnabled()) |
| return; |
| charOverrides["("] = this._handleBracePairInsertion.bind(this, "()"); |
| charOverrides[")"] = this._handleClosingBraceOverride.bind(this, ")"); |
| charOverrides["{"] = this._handleBracePairInsertion.bind(this, "{}"); |
| charOverrides["}"] = this._handleClosingBraceOverride.bind(this, "}"); |
| }, |
| |
| _handleBackspace: function() |
| { |
| var selection = this._mainPanel.lastSelection(); |
| if (!selection || !selection.isEmpty()) |
| return false; |
| |
| var column = selection.startColumn; |
| if (column == 0) |
| return false; |
| |
| var lineNumber = selection.startLine; |
| var line = this._textModel.line(lineNumber); |
| if (column === line.length) |
| return false; |
| |
| var pair = line.substr(column - 1, 2); |
| if (pair === "()" || pair === "{}") { |
| this._textModel.editRange(new WebInspector.TextRange(lineNumber, column - 1, lineNumber, column + 1), ""); |
| this._mainPanel.setSelection(WebInspector.TextRange.createFromLocation(lineNumber, column - 1)); |
| return true; |
| } else |
| return false; |
| }, |
| |
| /** |
| * @param {string} bracePair |
| * @return {boolean} |
| */ |
| _handleBracePairInsertion: function(bracePair) |
| { |
| var selection = this._mainPanel.lastSelection().normalize(); |
| if (selection.isEmpty()) { |
| var lineNumber = selection.startLine; |
| var column = selection.startColumn; |
| var line = this._textModel.line(lineNumber); |
| if (column < line.length) { |
| var char = line.charAt(column); |
| if (WebInspector.TextUtils.isWordChar(char) || (!WebInspector.TextUtils.isBraceChar(char) && WebInspector.TextUtils.isStopChar(char))) |
| return false; |
| } |
| } |
| this._textModel.editRange(selection, bracePair); |
| this._mainPanel.setSelection(WebInspector.TextRange.createFromLocation(selection.startLine, selection.startColumn + 1)); |
| return true; |
| }, |
| |
| /** |
| * @param {string} brace |
| * @return {boolean} |
| */ |
| _handleClosingBraceOverride: function(brace) |
| { |
| var selection = this._mainPanel.lastSelection().normalize(); |
| if (!selection || !selection.isEmpty()) |
| return false; |
| |
| var lineNumber = selection.startLine; |
| var column = selection.startColumn; |
| var line = this._textModel.line(lineNumber); |
| if (line.charAt(column) !== brace) |
| return false; |
| |
| var braces = this._braceMatcher.enclosingBraces(lineNumber, column); |
| if (braces && braces.rightBrace.lineNumber === lineNumber && braces.rightBrace.column === column) { |
| this._mainPanel.setSelection(WebInspector.TextRange.createFromLocation(lineNumber, column + 1)); |
| return true; |
| } else |
| return false; |
| }, |
| } |
| |
| WebInspector.debugDefaultTextEditor = false; |