| /* |
| * 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. |
| */ |
| |
| WebInspector.TextViewer = function(textModel, platform, url, delegate) |
| { |
| WebInspector.View.call(this); |
| |
| this._textModel = textModel; |
| this._textModel.changeListener = this._textChanged.bind(this); |
| this._textModel.resetUndoStack(); |
| this._delegate = delegate; |
| |
| this.element.className = "text-editor monospace"; |
| |
| var enterTextChangeMode = this._enterInternalTextChangeMode.bind(this); |
| var exitTextChangeMode = this._exitInternalTextChangeMode.bind(this); |
| var syncScrollListener = this._syncScroll.bind(this); |
| var syncDecorationsForLineListener = this._syncDecorationsForLine.bind(this); |
| this._mainPanel = new WebInspector.TextEditorMainPanel(this._textModel, url, syncScrollListener, syncDecorationsForLineListener, enterTextChangeMode, exitTextChangeMode); |
| this._gutterPanel = new WebInspector.TextEditorGutterPanel(this._textModel, syncDecorationsForLineListener); |
| this.element.appendChild(this._mainPanel.element); |
| this.element.appendChild(this._gutterPanel.element); |
| |
| // Forward mouse wheel events from the unscrollable gutter to the main panel. |
| this._gutterPanel.element.addEventListener("mousewheel", function(e) { |
| this._mainPanel.element.dispatchEvent(e); |
| }.bind(this), false); |
| |
| this.element.addEventListener("dblclick", this._doubleClick.bind(this), true); |
| this.element.addEventListener("keydown", this._handleKeyDown.bind(this), false); |
| |
| this._registerShortcuts(); |
| } |
| |
| WebInspector.TextViewer.prototype = { |
| set mimeType(mimeType) |
| { |
| this._mainPanel.mimeType = mimeType; |
| }, |
| |
| set readOnly(readOnly) |
| { |
| if (this._mainPanel.readOnly === readOnly) |
| return; |
| this._mainPanel.readOnly = readOnly; |
| this._delegate.readOnlyStateChanged(readOnly); |
| }, |
| |
| get readOnly() |
| { |
| return this._mainPanel.readOnly; |
| }, |
| |
| get textModel() |
| { |
| return this._textModel; |
| }, |
| |
| revealLine: function(lineNumber) |
| { |
| this._mainPanel.revealLine(lineNumber); |
| }, |
| |
| addDecoration: function(lineNumber, decoration) |
| { |
| this._mainPanel.addDecoration(lineNumber, decoration); |
| this._gutterPanel.addDecoration(lineNumber, decoration); |
| }, |
| |
| removeDecoration: function(lineNumber, decoration) |
| { |
| this._mainPanel.removeDecoration(lineNumber, decoration); |
| this._gutterPanel.removeDecoration(lineNumber, decoration); |
| }, |
| |
| markAndRevealRange: function(range) |
| { |
| this._mainPanel.markAndRevealRange(range); |
| }, |
| |
| highlightLine: function(lineNumber) |
| { |
| if (typeof lineNumber !== "number" || lineNumber < 0) |
| return; |
| |
| this._mainPanel.highlightLine(lineNumber); |
| }, |
| |
| clearLineHighlight: function() |
| { |
| this._mainPanel.clearLineHighlight(); |
| }, |
| |
| freeCachedElements: function() |
| { |
| this._mainPanel.freeCachedElements(); |
| this._gutterPanel.freeCachedElements(); |
| }, |
| |
| get scrollTop() |
| { |
| return this._mainPanel.element.scrollTop; |
| }, |
| |
| set scrollTop(scrollTop) |
| { |
| this._mainPanel.element.scrollTop = scrollTop; |
| }, |
| |
| get scrollLeft() |
| { |
| return this._mainPanel.element.scrollLeft; |
| }, |
| |
| set scrollLeft(scrollLeft) |
| { |
| this._mainPanel.element.scrollLeft = scrollLeft; |
| }, |
| |
| beginUpdates: function() |
| { |
| this._mainPanel.beginUpdates(); |
| this._gutterPanel.beginUpdates(); |
| }, |
| |
| endUpdates: function() |
| { |
| this._mainPanel.endUpdates(); |
| this._gutterPanel.endUpdates(); |
| this._updatePanelOffsets(); |
| }, |
| |
| resize: function() |
| { |
| this._mainPanel.resize(); |
| this._gutterPanel.resize(); |
| this._updatePanelOffsets(); |
| }, |
| |
| // WebInspector.TextModel listener |
| _textChanged: function(oldRange, newRange, oldText, newText) |
| { |
| if (!this._internalTextChangeMode) |
| this._textModel.resetUndoStack(); |
| this._mainPanel.textChanged(oldRange, newRange); |
| this._gutterPanel.textChanged(oldRange, newRange); |
| this._updatePanelOffsets(); |
| }, |
| |
| _enterInternalTextChangeMode: function() |
| { |
| this._internalTextChangeMode = true; |
| this._delegate.startEditing(); |
| }, |
| |
| _exitInternalTextChangeMode: function(oldRange, newRange) |
| { |
| this._internalTextChangeMode = false; |
| this._delegate.endEditing(oldRange, newRange); |
| }, |
| |
| _updatePanelOffsets: function() |
| { |
| var lineNumbersWidth = this._gutterPanel.element.offsetWidth; |
| if (lineNumbersWidth) |
| this._mainPanel.element.style.setProperty("left", lineNumbersWidth + "px"); |
| else |
| this._mainPanel.element.style.removeProperty("left"); // Use default value set in CSS. |
| }, |
| |
| _syncScroll: function() |
| { |
| // Async call due to performance reasons. |
| setTimeout(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; |
| }.bind(this), 0); |
| }, |
| |
| _syncDecorationsForLine: function(lineNumber) |
| { |
| if (lineNumber >= this._textModel.linesCount) |
| return; |
| |
| var mainChunk = this._mainPanel.chunkForLine(lineNumber); |
| if (mainChunk.linesCount === 1 && mainChunk.decorated) { |
| 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"); |
| } |
| }, |
| |
| _doubleClick: function(event) |
| { |
| if (!this.readOnly || this._commitEditingInProgress) |
| return; |
| |
| var lineRow = event.target.enclosingNodeOrSelfWithClass("webkit-line-content"); |
| if (!lineRow) |
| return; // Do not trigger editing from line numbers. |
| |
| if (!this._delegate.isContentEditable()) |
| return; |
| |
| this.readOnly = false; |
| window.getSelection().collapseToStart(); |
| }, |
| |
| _registerShortcuts: function() |
| { |
| var keys = WebInspector.KeyboardShortcut.Keys; |
| var modifiers = WebInspector.KeyboardShortcut.Modifiers; |
| |
| this._shortcuts = {}; |
| var commitEditing = this._commitEditing.bind(this); |
| var cancelEditing = this._cancelEditing.bind(this); |
| this._shortcuts[WebInspector.KeyboardShortcut.makeKey("s", modifiers.CtrlOrMeta)] = commitEditing; |
| this._shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Enter.code, modifiers.CtrlOrMeta)] = commitEditing; |
| this._shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Esc.code)] = cancelEditing; |
| |
| var handleUndo = this._mainPanel.handleUndoRedo.bind(this._mainPanel, false); |
| var handleRedo = this._mainPanel.handleUndoRedo.bind(this._mainPanel, true); |
| this._shortcuts[WebInspector.KeyboardShortcut.makeKey("z", modifiers.CtrlOrMeta)] = handleUndo; |
| this._shortcuts[WebInspector.KeyboardShortcut.makeKey("z", modifiers.Shift | modifiers.CtrlOrMeta)] = handleRedo; |
| |
| var handleTabKey = this._mainPanel.handleTabKeyPress.bind(this._mainPanel, false); |
| var handleShiftTabKey = this._mainPanel.handleTabKeyPress.bind(this._mainPanel, true); |
| this._shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Tab.code)] = handleTabKey; |
| this._shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Tab.code, modifiers.Shift)] = handleShiftTabKey; |
| }, |
| |
| _handleKeyDown: function(e) |
| { |
| var shortcutKey = WebInspector.KeyboardShortcut.makeKeyFromEvent(e); |
| var handler = this._shortcuts[shortcutKey]; |
| if (handler && handler.call(this)) { |
| e.preventDefault(); |
| e.stopPropagation(); |
| } |
| }, |
| |
| _commitEditing: function() |
| { |
| if (this.readOnly) |
| return false; |
| |
| this.readOnly = true; |
| function didCommitEditing(error) |
| { |
| this._commitEditingInProgress = false; |
| if (error) |
| this.readOnly = false; |
| } |
| this._commitEditingInProgress = true; |
| this._delegate.commitEditing(didCommitEditing.bind(this)); |
| return true; |
| }, |
| |
| _cancelEditing: function() |
| { |
| if (this.readOnly) |
| return false; |
| |
| this.readOnly = true; |
| this._delegate.cancelEditing(); |
| return true; |
| } |
| } |
| |
| WebInspector.TextViewer.prototype.__proto__ = WebInspector.View.prototype; |
| |
| WebInspector.TextViewerDelegate = function() |
| { |
| } |
| |
| WebInspector.TextViewerDelegate.prototype = { |
| isContentEditable: function() |
| { |
| // Should be implemented by subclasses. |
| }, |
| |
| readOnlyStateChanged: function(readOnly) |
| { |
| // Should be implemented by subclasses. |
| }, |
| |
| startEditing: function() |
| { |
| // Should be implemented by subclasses. |
| }, |
| |
| endEditing: function(oldRange, newRange) |
| { |
| // Should be implemented by subclasses. |
| }, |
| |
| commitEditing: function() |
| { |
| // Should be implemented by subclasses. |
| }, |
| |
| cancelEditing: function() |
| { |
| // Should be implemented by subclasses. |
| } |
| } |
| |
| WebInspector.TextViewerDelegate.prototype.__proto__ = WebInspector.Object.prototype; |
| |
| WebInspector.TextEditorChunkedPanel = function(textModel) |
| { |
| this._textModel = textModel; |
| |
| this._defaultChunkSize = 50; |
| this._paintCoalescingLevel = 0; |
| this._domUpdateCoalescingLevel = 0; |
| } |
| |
| WebInspector.TextEditorChunkedPanel.prototype = { |
| get textModel() |
| { |
| return this._textModel; |
| }, |
| |
| revealLine: function(lineNumber) |
| { |
| if (lineNumber >= this._textModel.linesCount) |
| return; |
| |
| var chunk = this.makeLineAChunk(lineNumber); |
| chunk.element.scrollIntoViewIfNeeded(); |
| }, |
| |
| addDecoration: function(lineNumber, decoration) |
| { |
| if (lineNumber >= this._textModel.linesCount) |
| return; |
| |
| var chunk = this.makeLineAChunk(lineNumber); |
| chunk.addDecoration(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(); |
| }, |
| |
| makeLineAChunk: function(lineNumber) |
| { |
| var chunkNumber = this._chunkNumberForLine(lineNumber); |
| var oldChunk = this._textChunks[chunkNumber]; |
| |
| if (!oldChunk) { |
| console.error("No chunk for line number: " + lineNumber); |
| return; |
| } |
| |
| if (oldChunk.linesCount === 1) |
| return oldChunk; |
| |
| return this._splitChunkOnALine(lineNumber, chunkNumber); |
| }, |
| |
| _splitChunkOnALine: function(lineNumber, chunkNumber) |
| { |
| this.beginDomUpdates(); |
| |
| var oldChunk = this._textChunks[chunkNumber]; |
| var wasExpanded = oldChunk.expanded; |
| oldChunk.expanded = false; |
| |
| 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 lineChunk = this._createNewChunk(lineNumber, lineNumber + 1); |
| this._textChunks.splice(insertIndex++, 0, lineChunk); |
| this._container.insertBefore(lineChunk.element, oldChunk.element); |
| |
| // Suffix chunk. |
| if (oldChunk.startLine + oldChunk.linesCount > lineNumber + 1) { |
| var suffixChunk = this._createNewChunk(lineNumber + 1, 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.expanded = true; |
| lineChunk.expanded = true; |
| if (suffixChunk) |
| suffixChunk.expanded = true; |
| } |
| |
| this.endDomUpdates(); |
| |
| return lineChunk; |
| }, |
| |
| _scroll: function() |
| { |
| // FIXME: Replace the "2" with the padding-left value from CSS. |
| if (this.element.scrollLeft <= 2) |
| this.element.scrollLeft = 0; |
| |
| 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--; |
| }, |
| |
| _chunkNumberForLine: function(lineNumber) |
| { |
| function compareLineNumbers(value, chunk) |
| { |
| return value < chunk.startLine ? -1 : 1; |
| } |
| var insertBefore = insertionIndexForObjectInListSortedByFunction(lineNumber, this._textChunks, compareLineNumbers); |
| return insertBefore - 1; |
| }, |
| |
| chunkForLine: function(lineNumber) |
| { |
| return this._textChunks[this._chunkNumberForLine(lineNumber)]; |
| }, |
| |
| _findFirstVisibleChunkNumber: function(visibleFrom) |
| { |
| function compareOffsetTops(value, chunk) |
| { |
| return value < chunk.offsetTop ? -1 : 1; |
| } |
| var insertBefore = insertionIndexForObjectInListSortedByFunction(visibleFrom, this._textChunks, compareOffsetTops); |
| return insertBefore - 1; |
| }, |
| |
| _findVisibleChunks: function(visibleFrom, visibleTo) |
| { |
| 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 }; |
| }, |
| |
| _findFirstVisibleLineNumber: 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.getExpandedLineRow(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 || this._dirtyLines) |
| return; |
| |
| var visibleFrom = this.element.scrollTop; |
| var visibleTo = this.element.scrollTop + this.element.clientHeight; |
| |
| if (visibleTo) { |
| var result = this._findVisibleChunks(visibleFrom, visibleTo); |
| this._expandChunks(result.start, result.end); |
| } |
| }, |
| |
| _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].expanded = false; |
| for (var i = toIndex; i < this._textChunks.length; ++i) |
| this._textChunks[i].expanded = false; |
| for (var i = fromIndex; i < toIndex; ++i) |
| this._textChunks[i].expanded = true; |
| }, |
| |
| _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(); |
| } |
| } |
| |
| WebInspector.TextEditorGutterPanel = function(textModel, syncDecorationsForLineListener) |
| { |
| WebInspector.TextEditorChunkedPanel.call(this, textModel); |
| |
| this._syncDecorationsForLineListener = syncDecorationsForLineListener; |
| |
| this.element = document.createElement("div"); |
| this.element.className = "text-editor-lines"; |
| |
| this._container = document.createElement("div"); |
| this._container.className = "inner-container"; |
| this.element.appendChild(this._container); |
| |
| this.element.addEventListener("scroll", this._scroll.bind(this), false); |
| |
| this.freeCachedElements(); |
| this._buildChunks(); |
| } |
| |
| WebInspector.TextEditorGutterPanel.prototype = { |
| freeCachedElements: function() |
| { |
| this._cachedRows = []; |
| }, |
| |
| _createNewChunk: function(startLine, endLine) |
| { |
| return new WebInspector.TextEditorGutterChunk(this, startLine, endLine); |
| }, |
| |
| 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.expanded = false; |
| 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); |
| } |
| 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(); |
| }, |
| |
| 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"); |
| } |
| } |
| |
| WebInspector.TextEditorGutterPanel.prototype.__proto__ = WebInspector.TextEditorChunkedPanel.prototype; |
| |
| WebInspector.TextEditorGutterChunk = function(textViewer, startLine, endLine) |
| { |
| this._textViewer = textViewer; |
| this._textModel = textViewer._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 = { |
| addDecoration: function(decoration) |
| { |
| this._textViewer.beginDomUpdates(); |
| if (typeof decoration === "string") |
| this.element.addStyleClass(decoration); |
| this._textViewer.endDomUpdates(); |
| }, |
| |
| removeDecoration: function(decoration) |
| { |
| this._textViewer.beginDomUpdates(); |
| if (typeof decoration === "string") |
| this.element.removeStyleClass(decoration); |
| this._textViewer.endDomUpdates(); |
| }, |
| |
| get expanded() |
| { |
| return this._expanded; |
| }, |
| |
| set expanded(expanded) |
| { |
| if (this.linesCount === 1) |
| this._textViewer._syncDecorationsForLineListener(this.startLine); |
| |
| if (this._expanded === expanded) |
| return; |
| |
| this._expanded = expanded; |
| |
| if (this.linesCount === 1) |
| return; |
| |
| this._textViewer.beginDomUpdates(); |
| |
| if (expanded) { |
| 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); |
| } else { |
| 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._textViewer._cachedRows.push(lineRow); |
| } |
| delete this._expandedLineRows; |
| } |
| |
| this._textViewer.endDomUpdates(); |
| }, |
| |
| get height() |
| { |
| if (!this._expandedLineRows) |
| return this._textViewer._totalHeight(this.element); |
| return this._textViewer._totalHeight(this._expandedLineRows[0], this._expandedLineRows[this._expandedLineRows.length - 1]); |
| }, |
| |
| get offsetTop() |
| { |
| return (this._expandedLineRows && this._expandedLineRows.length) ? this._expandedLineRows[0].offsetTop : this.element.offsetTop; |
| }, |
| |
| _createRow: function(lineNumber) |
| { |
| var lineRow = this._textViewer._cachedRows.pop() || document.createElement("div"); |
| lineRow.lineNumber = lineNumber; |
| lineRow.className = "webkit-line-number"; |
| lineRow.textContent = lineNumber + 1; |
| return lineRow; |
| } |
| } |
| |
| WebInspector.TextEditorMainPanel = function(textModel, url, syncScrollListener, syncDecorationsForLineListener, enterTextChangeMode, exitTextChangeMode) |
| { |
| WebInspector.TextEditorChunkedPanel.call(this, textModel); |
| |
| this._syncScrollListener = syncScrollListener; |
| this._syncDecorationsForLineListener = syncDecorationsForLineListener; |
| this._enterTextChangeMode = enterTextChangeMode; |
| this._exitTextChangeMode = exitTextChangeMode; |
| |
| this._url = url; |
| this._highlighter = new WebInspector.TextEditorHighlighter(textModel, this._highlightDataReady.bind(this)); |
| this._readOnly = true; |
| |
| this.element = document.createElement("div"); |
| 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("scroll", this._scroll.bind(this), false); |
| |
| // In WebKit the DOMNodeRemoved event is fired AFTER the node is removed, thus it should be |
| // attached to all DOM nodes that we want to track. Instead, we attach the DOMNodeRemoved |
| // listeners only on the line rows, and use DOMSubtreeModified to track node removals inside |
| // the line rows. For more info see: https://bugs.webkit.org/show_bug.cgi?id=55666 |
| this._handleDOMUpdatesCallback = this._handleDOMUpdates.bind(this); |
| this._container.addEventListener("DOMCharacterDataModified", this._handleDOMUpdatesCallback, false); |
| this._container.addEventListener("DOMNodeInserted", this._handleDOMUpdatesCallback, false); |
| this._container.addEventListener("DOMSubtreeModified", this._handleDOMUpdatesCallback, false); |
| |
| this.freeCachedElements(); |
| this._buildChunks(); |
| } |
| |
| WebInspector.TextEditorMainPanel.prototype = { |
| set mimeType(mimeType) |
| { |
| this._highlighter.mimeType = mimeType; |
| }, |
| |
| set readOnly(readOnly) |
| { |
| 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"); |
| this.endDomUpdates(); |
| }, |
| |
| get readOnly() |
| { |
| return this._readOnly; |
| }, |
| |
| markAndRevealRange: function(range) |
| { |
| if (this._rangeToMark) { |
| var markedLine = this._rangeToMark.startLine; |
| delete this._rangeToMark; |
| // Remove the marked region immediately. |
| if (!this._dirtyLines) { |
| this.beginDomUpdates(); |
| var chunk = this.chunkForLine(markedLine); |
| var wasExpanded = chunk.expanded; |
| chunk.expanded = false; |
| chunk.updateCollapsedLineRow(); |
| chunk.expanded = wasExpanded; |
| this.endDomUpdates(); |
| } else |
| this._paintLines(markedLine, markedLine + 1); |
| } |
| |
| if (range) { |
| this._rangeToMark = range; |
| this.revealLine(range.startLine); |
| var chunk = this.makeLineAChunk(range.startLine); |
| this._paintLine(chunk.element); |
| if (this._markedRangeElement) |
| this._markedRangeElement.scrollIntoViewIfNeeded(); |
| } |
| delete this._markedRangeElement; |
| }, |
| |
| highlightLine: function(lineNumber) |
| { |
| this.clearLineHighlight(); |
| this._highlightedLine = lineNumber; |
| this.revealLine(lineNumber); |
| 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 = []; |
| }, |
| |
| handleUndoRedo: function(redo) |
| { |
| if (this._readOnly || this._dirtyLines) |
| return false; |
| |
| this.beginUpdates(); |
| this._enterTextChangeMode(); |
| |
| var callback = function(oldRange, newRange) { |
| this._exitTextChangeMode(oldRange, newRange); |
| this._enterTextChangeMode(); |
| }.bind(this); |
| |
| var range = redo ? this._textModel.redo(callback) : this._textModel.undo(callback); |
| if (range) |
| this._setCaretLocation(range.endLine, range.endColumn, true); |
| |
| this._exitTextChangeMode(null, null); |
| this.endUpdates(); |
| |
| return true; |
| }, |
| |
| handleTabKeyPress: function(shiftKey) |
| { |
| if (this._readOnly || this._dirtyLines) |
| return false; |
| |
| var selection = this._getSelection(); |
| if (!selection) |
| return false; |
| |
| if (shiftKey) |
| return true; |
| |
| this.beginUpdates(); |
| this._enterTextChangeMode(); |
| |
| var range = selection; |
| if (range.startLine > range.endLine || (range.startLine === range.endLine && range.startColumn > range.endColumn)) |
| range = new WebInspector.TextRange(range.endLine, range.endColumn, range.startLine, range.startColumn); |
| |
| var newRange = this._setText(range, "\t"); |
| |
| this._exitTextChangeMode(range, newRange); |
| this.endUpdates(); |
| |
| this._setCaretLocation(newRange.endLine, newRange.endColumn, true); |
| return true; |
| }, |
| |
| _splitChunkOnALine: function(lineNumber, chunkNumber) |
| { |
| var selection = this._getSelection(); |
| var chunk = WebInspector.TextEditorChunkedPanel.prototype._splitChunkOnALine.call(this, lineNumber, chunkNumber); |
| this._restoreSelection(selection); |
| return chunk; |
| }, |
| |
| _buildChunks: function() |
| { |
| for (var i = 0; i < this._textModel.linesCount; ++i) |
| this._textModel.removeAttribute(i, "highlight"); |
| |
| WebInspector.TextEditorChunkedPanel.prototype._buildChunks.call(this); |
| }, |
| |
| _createNewChunk: function(startLine, endLine) |
| { |
| return new WebInspector.TextEditorMainChunk(this, startLine, endLine); |
| }, |
| |
| _expandChunks: function(fromIndex, toIndex) |
| { |
| var lastChunk = this._textChunks[toIndex - 1]; |
| var lastVisibleLine = lastChunk.startLine + lastChunk.linesCount; |
| |
| var selection = this._getSelection(); |
| |
| this._muteHighlightListener = true; |
| this._highlighter.highlight(lastVisibleLine); |
| delete this._muteHighlightListener; |
| |
| this._restorePaintLinesOperationsCredit(); |
| WebInspector.TextEditorChunkedPanel.prototype._expandChunks.call(this, fromIndex, toIndex); |
| this._adjustPaintLinesOperationsRefreshValue(); |
| |
| this._restoreSelection(selection); |
| }, |
| |
| _highlightDataReady: function(fromLine, toLine) |
| { |
| if (this._muteHighlightListener) |
| return; |
| this._restorePaintLinesOperationsCredit(); |
| this._paintLines(fromLine, toLine, true /*restoreSelection*/); |
| }, |
| |
| _schedulePaintLines: function(startLine, endLine) |
| { |
| if (startLine >= endLine) |
| return; |
| |
| if (!this._scheduledPaintLines) { |
| this._scheduledPaintLines = [ { startLine: startLine, endLine: endLine } ]; |
| this._paintScheduledLinesTimer = setTimeout(this._paintScheduledLines.bind(this), 0); |
| } else { |
| for (var i = 0; i < this._scheduledPaintLines.length; ++i) { |
| var chunk = this._scheduledPaintLines[i]; |
| if (chunk.startLine <= endLine && chunk.endLine >= startLine) { |
| chunk.startLine = Math.min(chunk.startLine, startLine); |
| chunk.endLine = Math.max(chunk.endLine, endLine); |
| return; |
| } |
| if (chunk.startLine > endLine) { |
| this._scheduledPaintLines.splice(i, 0, { startLine: startLine, endLine: endLine }); |
| return; |
| } |
| } |
| this._scheduledPaintLines.push({ startLine: startLine, endLine: endLine }); |
| } |
| }, |
| |
| _paintScheduledLines: function(skipRestoreSelection) |
| { |
| if (this._paintScheduledLinesTimer) |
| clearTimeout(this._paintScheduledLinesTimer); |
| delete this._paintScheduledLinesTimer; |
| |
| if (!this._scheduledPaintLines) |
| return; |
| |
| // Reschedule the timer if we can not paint the lines yet, or the user is scrolling. |
| if (this._dirtyLines || this._repaintAllTimer) { |
| this._paintScheduledLinesTimer = setTimeout(this._paintScheduledLines.bind(this), 50); |
| return; |
| } |
| |
| var scheduledPaintLines = this._scheduledPaintLines; |
| delete this._scheduledPaintLines; |
| |
| this._restorePaintLinesOperationsCredit(); |
| this._paintLineChunks(scheduledPaintLines, !skipRestoreSelection); |
| this._adjustPaintLinesOperationsRefreshValue(); |
| }, |
| |
| _restorePaintLinesOperationsCredit: function() |
| { |
| if (!this._paintLinesOperationsRefreshValue) |
| this._paintLinesOperationsRefreshValue = 250; |
| this._paintLinesOperationsCredit = this._paintLinesOperationsRefreshValue; |
| this._paintLinesOperationsLastRefresh = Date.now(); |
| }, |
| |
| _adjustPaintLinesOperationsRefreshValue: function() |
| { |
| var operationsDone = this._paintLinesOperationsRefreshValue - this._paintLinesOperationsCredit; |
| if (operationsDone <= 0) |
| return; |
| var timePast = Date.now() - this._paintLinesOperationsLastRefresh; |
| if (timePast <= 0) |
| return; |
| // Make the synchronous CPU chunk for painting the lines 50 msec. |
| var value = Math.floor(operationsDone / timePast * 50); |
| this._paintLinesOperationsRefreshValue = Number.constrain(value, 150, 1500); |
| }, |
| |
| _paintLines: function(fromLine, toLine, restoreSelection) |
| { |
| this._paintLineChunks([ { startLine: fromLine, endLine: toLine } ], restoreSelection); |
| }, |
| |
| _paintLineChunks: function(lineChunks, restoreSelection) |
| { |
| // First, paint visible lines, so that in case of long lines we should start highlighting |
| // the visible area immediately, instead of waiting for the lines above the visible area. |
| var visibleFrom = this.element.scrollTop; |
| var firstVisibleLineNumber = this._findFirstVisibleLineNumber(visibleFrom); |
| |
| var chunk; |
| var selection; |
| var invisibleLineRows = []; |
| for (var i = 0; i < lineChunks.length; ++i) { |
| var lineChunk = lineChunks[i]; |
| if (this._dirtyLines || this._scheduledPaintLines) { |
| this._schedulePaintLines(lineChunk.startLine, lineChunk.endLine); |
| continue; |
| } |
| for (var lineNumber = lineChunk.startLine; lineNumber < lineChunk.endLine; ++lineNumber) { |
| if (!chunk || lineNumber < chunk.startLine || lineNumber >= chunk.startLine + chunk.linesCount) |
| chunk = this.chunkForLine(lineNumber); |
| var lineRow = chunk.getExpandedLineRow(lineNumber); |
| if (!lineRow) |
| continue; |
| if (lineNumber < firstVisibleLineNumber) { |
| invisibleLineRows.push(lineRow); |
| continue; |
| } |
| if (restoreSelection && !selection) |
| selection = this._getSelection(); |
| this._paintLine(lineRow); |
| if (this._paintLinesOperationsCredit < 0) { |
| this._schedulePaintLines(lineNumber + 1, lineChunk.endLine); |
| break; |
| } |
| } |
| } |
| |
| for (var i = 0; i < invisibleLineRows.length; ++i) { |
| if (restoreSelection && !selection) |
| selection = this._getSelection(); |
| this._paintLine(invisibleLineRows[i]); |
| } |
| |
| if (restoreSelection) |
| this._restoreSelection(selection); |
| }, |
| |
| _paintLine: function(lineRow) |
| { |
| var lineNumber = lineRow.lineNumber; |
| if (this._dirtyLines) { |
| this._schedulePaintLines(lineNumber, lineNumber + 1); |
| return; |
| } |
| |
| this.beginDomUpdates(); |
| try { |
| if (this._scheduledPaintLines || this._paintLinesOperationsCredit < 0) { |
| this._schedulePaintLines(lineNumber, lineNumber + 1); |
| return; |
| } |
| |
| var highlight = this._textModel.getAttribute(lineNumber, "highlight"); |
| if (!highlight) |
| return; |
| |
| lineRow.removeChildren(); |
| var line = this._textModel.line(lineNumber); |
| if (!line) |
| lineRow.appendChild(document.createElement("br")); |
| |
| var plainTextStart = -1; |
| for (var j = 0; j < line.length;) { |
| if (j > 1000) { |
| // This line is too long - do not waste cycles on minified js highlighting. |
| if (plainTextStart === -1) |
| plainTextStart = j; |
| break; |
| } |
| var attribute = highlight[j]; |
| if (!attribute || !attribute.tokenType) { |
| if (plainTextStart === -1) |
| plainTextStart = j; |
| j++; |
| } else { |
| if (plainTextStart !== -1) { |
| this._appendTextNode(lineRow, line.substring(plainTextStart, j)); |
| plainTextStart = -1; |
| --this._paintLinesOperationsCredit; |
| } |
| this._appendSpan(lineRow, line.substring(j, j + attribute.length), attribute.tokenType); |
| j += attribute.length; |
| --this._paintLinesOperationsCredit; |
| } |
| } |
| if (plainTextStart !== -1) { |
| this._appendTextNode(lineRow, line.substring(plainTextStart, line.length)); |
| --this._paintLinesOperationsCredit; |
| } |
| if (lineRow.decorationsElement) |
| lineRow.appendChild(lineRow.decorationsElement); |
| } finally { |
| if (this._rangeToMark && this._rangeToMark.startLine === lineNumber) |
| this._markedRangeElement = highlightSearchResult(lineRow, this._rangeToMark.startColumn, this._rangeToMark.endColumn - this._rangeToMark.startColumn); |
| this.endDomUpdates(); |
| } |
| }, |
| |
| _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); |
| }, |
| |
| _getSelection: function() |
| { |
| var selection = window.getSelection(); |
| if (!selection.rangeCount) |
| return null; |
| var selectionRange = selection.getRangeAt(0); |
| // Selection may be outside of the viewer. |
| if (!this._container.isAncestor(selectionRange.startContainer) || !this._container.isAncestor(selectionRange.endContainer)) |
| return null; |
| var start = this._selectionToPosition(selectionRange.startContainer, selectionRange.startOffset); |
| var end = selectionRange.collapsed ? start : this._selectionToPosition(selectionRange.endContainer, selectionRange.endOffset); |
| if (selection.anchorNode === selectionRange.startContainer && selection.anchorOffset === selectionRange.startOffset) |
| return new WebInspector.TextRange(start.line, start.column, end.line, end.column); |
| else |
| return new WebInspector.TextRange(end.line, end.column, start.line, start.column); |
| }, |
| |
| _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; |
| } |
| } |
| } |
| }, |
| |
| _setCaretLocation: function(line, column, scrollIntoView) |
| { |
| var range = new WebInspector.TextRange(line, column, line, column); |
| this._restoreSelection(range, scrollIntoView); |
| }, |
| |
| _selectionToPosition: function(container, offset) |
| { |
| 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) }; |
| |
| var lineRow = this._enclosingLineRowOrSelf(container); |
| var lineNumber = lineRow.lineNumber; |
| if (container === lineRow && offset === 0) |
| return { line: lineNumber, column: 0 }; |
| |
| // This may be chunk and chunks may contain \n. |
| var column = 0; |
| var node = lineRow.nodeType === Node.TEXT_NODE ? lineRow : lineRow.traverseNextTextNode(lineRow); |
| while (node && node !== container) { |
| var text = node.textContent; |
| for (var i = 0; i < text.length; ++i) { |
| if (text.charAt(i) === "\n") { |
| lineNumber++; |
| column = 0; |
| } else |
| column++; |
| } |
| node = node.traverseNextTextNode(lineRow); |
| } |
| |
| if (node === container && offset) { |
| var text = node.textContent; |
| for (var i = 0; i < offset; ++i) { |
| if (text.charAt(i) === "\n") { |
| lineNumber++; |
| column = 0; |
| } else |
| column++; |
| } |
| } |
| return { line: lineNumber, column: column }; |
| }, |
| |
| _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.getExpandedLineRow(line); |
| if (lineRow) |
| var rangeBoundary = lineRow.rangeBoundaryForOffset(column); |
| else { |
| var offset = column; |
| for (var i = chunk.startLine; i < line; ++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; |
| }, |
| |
| _enclosingLineRowOrSelf: function(element) |
| { |
| var lineRow = element.enclosingNodeOrSelfWithClass("webkit-line-content"); |
| if (lineRow) |
| return lineRow; |
| for (var lineRow = element; lineRow; lineRow = lineRow.parentElement) { |
| if (lineRow.parentElement === this._container) |
| return lineRow; |
| } |
| return null; |
| }, |
| |
| _appendSpan: function(element, content, className) |
| { |
| if (className === "html-resource-link" || className === "html-external-link") { |
| element.appendChild(this._createLink(content, className === "html-external-link")); |
| return; |
| } |
| |
| var span = this._cachedSpans.pop() || document.createElement("span"); |
| span.className = "webkit-" + className; |
| span.textContent = content; |
| element.appendChild(span); |
| if (!("spans" in element)) |
| element.spans = []; |
| element.spans.push(span); |
| }, |
| |
| _appendTextNode: function(element, text) |
| { |
| var textNode = this._cachedTextNodes.pop(); |
| if (textNode) |
| textNode.nodeValue = text; |
| else |
| textNode = document.createTextNode(text); |
| element.appendChild(textNode); |
| if (!("textNodes" in element)) |
| element.textNodes = []; |
| element.textNodes.push(textNode); |
| }, |
| |
| _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 a = WebInspector.linkifyURLAsNode(this._rewriteHref(content), content, null, isExternal); |
| var span = document.createElement("span"); |
| span.className = "webkit-html-attribute-value"; |
| if (quote) |
| span.appendChild(document.createTextNode(quote)); |
| span.appendChild(a); |
| if (quote) |
| span.appendChild(document.createTextNode(quote)); |
| return span; |
| }, |
| |
| _rewriteHref: function(hrefValue, isExternal) |
| { |
| if (!this._url || !hrefValue || hrefValue.indexOf("://") > 0) |
| return hrefValue; |
| return WebInspector.completeURL(this._url, hrefValue); |
| }, |
| |
| _handleDOMUpdates: function(e) |
| { |
| if (this._domUpdateCoalescingLevel) |
| return; |
| |
| var target = e.target; |
| if (target === this._container) |
| return; |
| |
| var lineRow = this._enclosingLineRowOrSelf(target); |
| if (!lineRow) |
| return; |
| |
| if (lineRow.decorationsElement && (lineRow.decorationsElement === target || lineRow.decorationsElement.isAncestor(target))) { |
| if (this._syncDecorationsForLineListener) |
| this._syncDecorationsForLineListener(lineRow.lineNumber); |
| return; |
| } |
| |
| if (this._readOnly) |
| return; |
| |
| if (target === lineRow && e.type === "DOMNodeInserted") { |
| // Ensure that the newly inserted line row has no lineNumber. |
| delete lineRow.lineNumber; |
| } |
| |
| var startLine = 0; |
| for (var row = lineRow; row; row = row.previousSibling) { |
| if (typeof row.lineNumber === "number") { |
| startLine = row.lineNumber; |
| break; |
| } |
| } |
| |
| var endLine = startLine + 1; |
| for (var row = lineRow.nextSibling; row; row = row.nextSibling) { |
| if (typeof row.lineNumber === "number" && row.lineNumber > startLine) { |
| endLine = row.lineNumber; |
| break; |
| } |
| } |
| |
| if (target === lineRow && e.type === "DOMNodeRemoved") { |
| // Now this will no longer be valid. |
| delete lineRow.lineNumber; |
| } |
| |
| if (this._dirtyLines) { |
| this._dirtyLines.start = Math.min(this._dirtyLines.start, startLine); |
| this._dirtyLines.end = Math.max(this._dirtyLines.end, endLine); |
| } else { |
| this._dirtyLines = { start: startLine, end: endLine }; |
| setTimeout(this._applyDomUpdates.bind(this), 0); |
| // Remove marked ranges, if any. |
| this.markAndRevealRange(null); |
| } |
| }, |
| |
| _applyDomUpdates: function() |
| { |
| if (!this._dirtyLines) |
| return; |
| |
| // Check if the editor had been set readOnly by the moment when this async callback got executed. |
| if (this._readOnly) { |
| delete this._dirtyLines; |
| return; |
| } |
| |
| // This is a "foreign" call outside of this class. Should be before we delete the dirty lines flag. |
| this._enterTextChangeMode(); |
| |
| var dirtyLines = this._dirtyLines; |
| delete this._dirtyLines; |
| |
| var firstChunkNumber = this._chunkNumberForLine(dirtyLines.start); |
| var startLine = this._textChunks[firstChunkNumber].startLine; |
| var endLine = this._textModel.linesCount; |
| |
| // Collect lines. |
| var firstLineRow; |
| if (firstChunkNumber) { |
| var chunk = this._textChunks[firstChunkNumber - 1]; |
| firstLineRow = chunk.expanded ? chunk.getExpandedLineRow(chunk.startLine + chunk.linesCount - 1) : chunk.element; |
| firstLineRow = firstLineRow.nextSibling; |
| } else |
| firstLineRow = this._container.firstChild; |
| |
| var lines = []; |
| for (var lineRow = firstLineRow; lineRow; lineRow = lineRow.nextSibling) { |
| if (typeof lineRow.lineNumber === "number" && lineRow.lineNumber >= dirtyLines.end) { |
| endLine = lineRow.lineNumber; |
| break; |
| } |
| // Update with the newest lineNumber, so that the call to the _getSelection method below should work. |
| lineRow.lineNumber = startLine + lines.length; |
| this._collectLinesFromDiv(lines, lineRow); |
| } |
| |
| // Try to decrease the range being replaced, if possible. |
| var startOffset = 0; |
| while (startLine < dirtyLines.start && startOffset < lines.length) { |
| if (this._textModel.line(startLine) !== lines[startOffset]) |
| break; |
| ++startOffset; |
| ++startLine; |
| } |
| |
| var endOffset = lines.length; |
| while (endLine > dirtyLines.end && endOffset > startOffset) { |
| if (this._textModel.line(endLine - 1) !== lines[endOffset - 1]) |
| break; |
| --endOffset; |
| --endLine; |
| } |
| |
| lines = lines.slice(startOffset, endOffset); |
| |
| // Try to decrease the range being replaced by column offsets, if possible. |
| var startColumn = 0; |
| var endColumn = this._textModel.lineLength(endLine - 1); |
| if (lines.length > 0) { |
| var line1 = this._textModel.line(startLine); |
| var line2 = lines[0]; |
| while (line1[startColumn] && line1[startColumn] === line2[startColumn]) |
| ++startColumn; |
| lines[0] = line2.substring(startColumn); |
| |
| var line1 = this._textModel.line(endLine - 1); |
| var line2 = lines[lines.length - 1]; |
| for (var i = 0; i < endColumn && i < line2.length; ++i) { |
| if (startLine === endLine - 1 && endColumn - i <= startColumn) |
| break; |
| if (line1[endColumn - i - 1] !== line2[line2.length - i - 1]) |
| break; |
| } |
| if (i) { |
| endColumn -= i; |
| lines[lines.length - 1] = line2.substring(0, line2.length - i); |
| } |
| } |
| |
| var selection = this._getSelection(); |
| |
| if (lines.length === 0 && endLine < this._textModel.linesCount) |
| var oldRange = new WebInspector.TextRange(startLine, 0, endLine, 0); |
| else if (lines.length === 0 && startLine > 0) |
| var oldRange = new WebInspector.TextRange(startLine - 1, this._textModel.lineLength(startLine - 1), endLine - 1, this._textModel.lineLength(endLine - 1)); |
| else |
| var oldRange = new WebInspector.TextRange(startLine, startColumn, endLine - 1, endColumn); |
| |
| var newRange = this._setText(oldRange, lines.join("\n")); |
| |
| this._paintScheduledLines(true); |
| this._restoreSelection(selection); |
| |
| this._exitTextChangeMode(oldRange, newRange); |
| }, |
| |
| textChanged: function(oldRange, newRange) |
| { |
| this.beginDomUpdates(); |
| this._removeDecorationsInRange(oldRange); |
| this._updateChunksForRanges(oldRange, newRange); |
| this._updateHighlightsForRange(newRange); |
| this.endDomUpdates(); |
| }, |
| |
| _setText: function(range, text) |
| { |
| if (this._lastEditedRange && (!text || text.indexOf("\n") !== -1 || this._lastEditedRange.endLine !== range.startLine || this._lastEditedRange.endColumn !== range.startColumn)) |
| this._textModel.markUndoableState(); |
| |
| var newRange = this._textModel.setText(range, text); |
| this._lastEditedRange = newRange; |
| |
| return newRange; |
| }, |
| |
| _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(); |
| } |
| }, |
| |
| _updateChunksForRanges: function(oldRange, newRange) |
| { |
| // Update the chunks in range: firstChunkNumber <= index <= lastChunkNumber |
| var firstChunkNumber = this._chunkNumberForLine(oldRange.startLine); |
| var lastChunkNumber = firstChunkNumber; |
| while (lastChunkNumber + 1 < this._textChunks.length) { |
| if (this._textChunks[lastChunkNumber + 1].startLine > oldRange.endLine) |
| break; |
| ++lastChunkNumber; |
| } |
| |
| var startLine = this._textChunks[firstChunkNumber].startLine; |
| var linesCount = this._textChunks[lastChunkNumber].startLine + this._textChunks[lastChunkNumber].linesCount - startLine; |
| var linesDiff = newRange.linesCount - oldRange.linesCount; |
| linesCount += linesDiff; |
| |
| if (linesDiff) { |
| // Lines shifted, update the line numbers of the chunks below. |
| for (var chunkNumber = lastChunkNumber + 1; chunkNumber < this._textChunks.length; ++chunkNumber) |
| this._textChunks[chunkNumber].startLine += linesDiff; |
| } |
| |
| var firstLineRow; |
| if (firstChunkNumber) { |
| var chunk = this._textChunks[firstChunkNumber - 1]; |
| firstLineRow = chunk.expanded ? chunk.getExpandedLineRow(chunk.startLine + chunk.linesCount - 1) : chunk.element; |
| firstLineRow = firstLineRow.nextSibling; |
| } else |
| firstLineRow = this._container.firstChild; |
| |
| // Most frequent case: a chunk remained the same. |
| for (var chunkNumber = firstChunkNumber; chunkNumber <= lastChunkNumber; ++chunkNumber) { |
| var chunk = this._textChunks[chunkNumber]; |
| if (chunk.startLine + chunk.linesCount > this._textModel.linesCount) |
| break; |
| var lineNumber = chunk.startLine; |
| for (var lineRow = firstLineRow; lineRow && lineNumber < chunk.startLine + chunk.linesCount; lineRow = lineRow.nextSibling) { |
| if (lineRow.lineNumber !== lineNumber || lineRow !== chunk.getExpandedLineRow(lineNumber) || lineRow.textContent !== this._textModel.line(lineNumber) || !lineRow.firstChild) |
| break; |
| ++lineNumber; |
| } |
| if (lineNumber < chunk.startLine + chunk.linesCount) |
| break; |
| chunk.updateCollapsedLineRow(); |
| ++firstChunkNumber; |
| firstLineRow = lineRow; |
| startLine += chunk.linesCount; |
| linesCount -= chunk.linesCount; |
| } |
| |
| if (firstChunkNumber > lastChunkNumber && linesCount === 0) |
| return; |
| |
| // Maybe merge with the next chunk, so that we should not create 1-sized chunks when appending new lines one by one. |
| var chunk = this._textChunks[lastChunkNumber + 1]; |
| var linesInLastChunk = linesCount % this._defaultChunkSize; |
| if (chunk && !chunk.decorated && linesInLastChunk > 0 && linesInLastChunk + chunk.linesCount <= this._defaultChunkSize) { |
| ++lastChunkNumber; |
| linesCount += chunk.linesCount; |
| } |
| |
| var scrollTop = this.element.scrollTop; |
| var scrollLeft = this.element.scrollLeft; |
| |
| // Delete all DOM elements that were either controlled by the old chunks, or have just been inserted. |
| var firstUnmodifiedLineRow = null; |
| var chunk = this._textChunks[lastChunkNumber + 1]; |
| if (chunk) { |
| firstUnmodifiedLineRow = chunk.expanded ? chunk.getExpandedLineRow(chunk.startLine) : chunk.element; |
| } |
| while (firstLineRow && firstLineRow !== firstUnmodifiedLineRow) { |
| var lineRow = firstLineRow; |
| firstLineRow = firstLineRow.nextSibling; |
| this._container.removeChild(lineRow); |
| } |
| |
| // Replace old chunks with the new ones. |
| for (var chunkNumber = firstChunkNumber; linesCount > 0; ++chunkNumber) { |
| var chunkLinesCount = Math.min(this._defaultChunkSize, linesCount); |
| var newChunk = this._createNewChunk(startLine, startLine + chunkLinesCount); |
| this._container.insertBefore(newChunk.element, firstUnmodifiedLineRow); |
| |
| if (chunkNumber <= lastChunkNumber) |
| this._textChunks[chunkNumber] = newChunk; |
| else |
| this._textChunks.splice(chunkNumber, 0, newChunk); |
| startLine += chunkLinesCount; |
| linesCount -= chunkLinesCount; |
| } |
| if (chunkNumber <= lastChunkNumber) |
| this._textChunks.splice(chunkNumber, lastChunkNumber - chunkNumber + 1); |
| |
| this.element.scrollTop = scrollTop; |
| this.element.scrollLeft = scrollLeft; |
| }, |
| |
| _updateHighlightsForRange: function(range) |
| { |
| var visibleFrom = this.element.scrollTop; |
| var visibleTo = this.element.scrollTop + this.element.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].expanded = false; |
| } |
| |
| this._repaintAll(); |
| }, |
| |
| _collectLinesFromDiv: function(lines, element) |
| { |
| var textContents = []; |
| var node = element.nodeType === Node.TEXT_NODE ? element : element.traverseNextNode(element); |
| while (node) { |
| if (element.decorationsElement === node) { |
| node = node.nextSibling; |
| continue; |
| } |
| if (node.nodeName.toLowerCase() === "br") |
| textContents.push("\n"); |
| else if (node.nodeType === Node.TEXT_NODE) |
| textContents.push(node.textContent); |
| node = node.traverseNextNode(element); |
| } |
| |
| var textContent = textContents.join(""); |
| // The last \n (if any) does not "count" in a DIV. |
| textContent = textContent.replace(/\n$/, ""); |
| |
| textContents = textContent.split("\n"); |
| for (var i = 0; i < textContents.length; ++i) |
| lines.push(textContents[i]); |
| } |
| } |
| |
| WebInspector.TextEditorMainPanel.prototype.__proto__ = WebInspector.TextEditorChunkedPanel.prototype; |
| |
| WebInspector.TextEditorMainChunk = function(textViewer, startLine, endLine) |
| { |
| this._textViewer = textViewer; |
| this._textModel = textViewer._textModel; |
| |
| this.element = document.createElement("div"); |
| this.element.lineNumber = startLine; |
| this.element.className = "webkit-line-content"; |
| this.element.addEventListener("DOMNodeRemoved", this._textViewer._handleDOMUpdatesCallback, false); |
| |
| this._startLine = startLine; |
| endLine = Math.min(this._textModel.linesCount, endLine); |
| this.linesCount = endLine - startLine; |
| |
| this._expanded = false; |
| |
| this.updateCollapsedLineRow(); |
| } |
| |
| WebInspector.TextEditorMainChunk.prototype = { |
| addDecoration: function(decoration) |
| { |
| this._textViewer.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.appendChild(this.element.decorationsElement); |
| } |
| this.element.decorationsElement.appendChild(decoration); |
| } |
| this._textViewer.endDomUpdates(); |
| }, |
| |
| removeDecoration: function(decoration) |
| { |
| this._textViewer.beginDomUpdates(); |
| if (typeof decoration === "string") |
| this.element.removeStyleClass(decoration); |
| else if (this.element.decorationsElement) |
| this.element.decorationsElement.removeChild(decoration); |
| this._textViewer.endDomUpdates(); |
| }, |
| |
| removeAllDecorations: function() |
| { |
| this._textViewer.beginDomUpdates(); |
| this.element.className = "webkit-line-content"; |
| if (this.element.decorationsElement) { |
| this.element.removeChild(this.element.decorationsElement); |
| delete this.element.decorationsElement; |
| } |
| this._textViewer.endDomUpdates(); |
| }, |
| |
| get decorated() |
| { |
| return this.element.className !== "webkit-line-content" || !!(this.element.decorationsElement && this.element.decorationsElement.firstChild); |
| }, |
| |
| get startLine() |
| { |
| return this._startLine; |
| }, |
| |
| 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; |
| } |
| }, |
| |
| get expanded() |
| { |
| return this._expanded; |
| }, |
| |
| set expanded(expanded) |
| { |
| if (this._expanded === expanded) |
| return; |
| |
| this._expanded = expanded; |
| |
| if (this.linesCount === 1) { |
| if (expanded) |
| this._textViewer._paintLine(this.element); |
| return; |
| } |
| |
| this._textViewer.beginDomUpdates(); |
| |
| if (expanded) { |
| 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._textViewer._paintLines(this.startLine, this.startLine + this.linesCount); |
| } else { |
| 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._textViewer._releaseLinesHighlight(lineRow); |
| } |
| delete this._expandedLineRows; |
| } |
| |
| this._textViewer.endDomUpdates(); |
| }, |
| |
| get height() |
| { |
| if (!this._expandedLineRows) |
| return this._textViewer._totalHeight(this.element); |
| return this._textViewer._totalHeight(this._expandedLineRows[0], this._expandedLineRows[this._expandedLineRows.length - 1]); |
| }, |
| |
| get offsetTop() |
| { |
| return (this._expandedLineRows && this._expandedLineRows.length) ? this._expandedLineRows[0].offsetTop : this.element.offsetTop; |
| }, |
| |
| _createRow: function(lineNumber) |
| { |
| var lineRow = this._textViewer._cachedRows.pop() || document.createElement("div"); |
| lineRow.lineNumber = lineNumber; |
| lineRow.className = "webkit-line-content"; |
| lineRow.addEventListener("DOMNodeRemoved", this._textViewer._handleDOMUpdatesCallback, false); |
| lineRow.textContent = this._textModel.line(lineNumber); |
| if (!lineRow.textContent) |
| lineRow.appendChild(document.createElement("br")); |
| return lineRow; |
| }, |
| |
| getExpandedLineRow: 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)); |
| |
| 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")); |
| } |
| } |