UNPKG

occaecatidicta

Version:
1,440 lines (1,195 loc) 80.6 kB
/* * 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. */ /** * @extends {WebInspector.View} * @constructor */ WebInspector.TextViewer = function(textModel, platform, url, delegate) { WebInspector.View.call(this); this.registerRequiredCSS("textViewer.css"); 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); var syncLineHeightListener = this._syncLineHeight.bind(this); this._mainPanel = new WebInspector.TextEditorMainPanel(this._textModel, url, syncScrollListener, syncDecorationsForLineListener, enterTextChangeMode, exitTextChangeMode); this._gutterPanel = new WebInspector.TextEditorGutterPanel(this._textModel, syncDecorationsForLineListener, syncLineHeightListener); 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._registerShortcuts(); } WebInspector.TextViewer.prototype = { set mimeType(mimeType) { this._mainPanel.mimeType = mimeType; }, set readOnly(readOnly) { if (this._mainPanel.readOnly === readOnly) return; this._mainPanel.readOnly = readOnly; WebInspector.markBeingEdited(this.element, !readOnly); }, get readOnly() { return this._mainPanel.readOnly; }, get textModel() { return this._textModel; }, focus: function() { this._mainPanel.focus(); }, 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; lineNumber = Math.min(lineNumber, this._textModel.linesCount - 1); this._mainPanel.highlightLine(lineNumber); }, clearLineHighlight: function() { this._mainPanel.clearLineHighlight(); }, freeCachedElements: function() { this._mainPanel.freeCachedElements(); this._gutterPanel.freeCachedElements(); }, elementsToRestoreScrollPositionsFor: function() { return [this._mainPanel.element]; }, inheritScrollPositions: function(textViewer) { this._mainPanel.element._scrollTop = textViewer._mainPanel.element.scrollTop; this._mainPanel.element._scrollLeft = textViewer._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(); }, // 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.beforeTextChanged(); }, _exitInternalTextChangeMode: function(oldRange, newRange) { this._internalTextChangeMode = false; this._delegate.afterTextChanged(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() { 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; }, _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"); } }, _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 = {}; var commitEditing = this._commitEditing.bind(this); this._shortcuts[WebInspector.KeyboardShortcut.makeKey("s", modifiers.CtrlOrMeta)] = commitEditing; var handleEnterKey = this._mainPanel.handleEnterKey.bind(this._mainPanel); this._shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Enter.code, WebInspector.KeyboardShortcut.Modifiers.None)] = handleEnterKey; 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) { if (this.readOnly) return; var shortcutKey = WebInspector.KeyboardShortcut.makeKeyFromEvent(e); var handler = this._shortcuts[shortcutKey]; if (handler && handler()) e.consume(true); }, _contextMenu: function(event) { var contextMenu = new WebInspector.ContextMenu(); var target = event.target.enclosingNodeOrSelfWithClass("webkit-line-number"); if (target) this._delegate.populateLineGutterContextMenu(contextMenu, target.lineNumber); else { target = this._mainPanel._enclosingLineRowOrSelf(event.target); this._delegate.populateTextAreaContextMenu(contextMenu, target && target.lineNumber); } var fileName = this._delegate.suggestedFileName(); if (fileName) contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Save as..." : "Save As..."), InspectorFrontendHost.saveAs.bind(InspectorFrontendHost, fileName, this._textModel.text)); contextMenu.show(event); }, _commitEditing: function() { if (this.readOnly) return false; this._delegate.commitEditing(); return true; }, wasShown: function() { if (!this.readOnly) WebInspector.markBeingEdited(this.element, true); }, willHide: function() { if (!this.readOnly) WebInspector.markBeingEdited(this.element, false); } } WebInspector.TextViewer.prototype.__proto__ = WebInspector.View.prototype; /** * @interface */ WebInspector.TextViewerDelegate = function() { } WebInspector.TextViewerDelegate.prototype = { beforeTextChanged: function() { }, afterTextChanged: function(oldRange, newRange) { }, commitEditing: function() { }, populateLineGutterContextMenu: function(contextMenu, lineNumber) { }, populateTextAreaContextMenu: function(contextMenu, lineNumber) { }, suggestedFileName: function() { } } /** * @constructor */ 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, true); }, _splitChunkOnALine: function(lineNumber, chunkNumber, createSuffixChunk) { 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); prefixChunk.readOnly = oldChunk.readOnly; 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); lineChunk.readOnly = oldChunk.readOnly; 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); suffixChunk.readOnly = oldChunk.readOnly; 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(); } } /** * @constructor * @extends {WebInspector.TextEditorChunkedPanel} */ WebInspector.TextEditorGutterPanel = function(textModel, syncDecorationsForLineListener, syncLineHeightListener) { WebInspector.TextEditorChunkedPanel.call(this, textModel); this._syncDecorationsForLineListener = syncDecorationsForLineListener; this._syncLineHeightListener = syncLineHeightListener; 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(); this._decorations = {}; } 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); } // 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; 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(); }, 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"); }, 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); }, 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]; } } } WebInspector.TextEditorGutterPanel.prototype.__proto__ = WebInspector.TextEditorChunkedPanel.prototype; /** * @constructor */ 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); this._textViewer._syncLineHeightListener(this._expandedLineRows[0]); } 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; } } /** * @constructor * @extends {WebInspector.TextEditorChunkedPanel} */ 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); this.element.addEventListener("focus", this._handleElementFocus.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 // // OPTIMIZATION. It is very expensive to listen to the DOM mutation events, thus we remove the // listeners whenever we do any internal DOM manipulations (such as expand/collapse line rows) // and set the listeners back when we are finished. 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._updateSelectionOnStartEditing(); } this.endDomUpdates(); }, get readOnly() { return this._readOnly; }, _handleElementFocus: function() { if (!this._readOnly) this._container.focus(); }, focus: function() { if (this._readOnly) this.element.focus(); else this._container.focus(); }, _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); }, setEditableRange: function(startLine, endLine) { this.beginDomUpdates(); var firstChunkNumber = this._chunkNumberForLine(startLine); var firstChunk = this._textChunks[firstChunkNumber]; if (firstChunk.startLine !== startLine) { this._splitChunkOnALine(startLine, firstChunkNumber); firstChunkNumber += 1; } var lastChunkNumber = this._textChunks.length; if (endLine !== this._textModel.linesCount) { lastChunkNumber = this._chunkNumberForLine(endLine); var lastChunk = this._textChunks[lastChunkNumber]; if (lastChunk && lastChunk.startLine !== endLine) { this._splitChunkOnALine(endLine, lastChunkNumber); lastChunkNumber += 1; } } for (var chunkNumber = 0; chunkNumber < firstChunkNumber; ++chunkNumber) this._textChunks[chunkNumber].readOnly = true; for (var chunkNumber = firstChunkNumber; chunkNumber < lastChunkNumber; ++chunkNumber) this._textChunks[chunkNumber].readOnly = false; for (var chunkNumber = lastChunkNumber; chunkNumber < this._textChunks.length; ++chunkNumber) this._textChunks[chunkNumber].readOnly = true; this.endDomUpdates(); }, clearEditableRange: function() { for (var chunkNumber = 0; chunkNumber < this._textChunks.length; ++chunkNumber) this._textChunks[chunkNumber].readOnly = false; }, 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); if (!this._readOnly) this._restoreSelection(new WebInspector.TextRange(lineNumber, 0, 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 = []; }, handleUndoRedo: function(redo) { if (this._dirtyLines) return false; this.beginUpdates(); function before() { this._enterTextChangeMode(); } function after(oldRange, newRange) { this._exitTextChangeMode(oldRange, newRange); } var range = redo ? this._textModel.redo(before.bind(this), after.bind(this)) : this._textModel.undo(before.bind(this), after.bind(this)); this.endUpdates(); // Restore location post-repaint. if (range) this._setCaretLocation(range.endLine, range.endColumn, true); return true; }, handleTabKeyPress: function(shiftKey) { if (this._dirtyLines) return false; var selection = this._getSelection(); if (!selection) return false; var range = selection.normalize(); this.beginUpdates(); this._enterTextChangeMode(); var newRange; if (shiftKey) newRange = this._unindentLines(range); else { if (range.isEmpty()) { newRange = this._setText(range, WebInspector.settings.textEditorIndent.get()); newRange.startColumn = newRange.endColumn; } else newRange = this._indentLines(range); } this._exitTextChangeMode(range, newRange); this.endUpdates(); this._restoreSelection(newRange, true); return true; }, _indentLines: function(range) { var indent = WebInspector.settings.textEditorIndent.get(); if (this._lastEditedRange) this._textModel.markUndoableState(); var newRange = range.clone(); // Do not change a selection start position when it is at the beginning of a line if (range.startColumn) newRange.startColumn += indent.length; var indentEndLine = range.endLine; if (range.endColumn) newRange.endColumn += indent.length; else indentEndLine--; for (var lineNumber = range.startLine; lineNumber <= indentEndLine; lineNumber++) this._textModel.setText(new WebInspector.TextRange(lineNumber, 0, lineNumber, 0), indent); this._lastEditedRange = newRange; return newRange; }, _unindentLines: function(range) { if (this._lastEditedRange) this._textModel.markUndoableState(); var indent = WebInspector.settings.textEditorIndent.get(); var indentLength = indent === WebInspector.TextEditorModel.Indent.TabCharacter ? 4 : indent.length; var lineIndentRegex = new RegExp("^ {1," + indentLength + "}"); var newRange = range.clone(); var indentEndLine = range.endLine; if (!range.endColumn) indentEndLine--; for (var lineNumber = range.startLine; lineNumber <= indentEndLine; lineNumber++) { var line = this._textModel.line(lineNumber); var firstCharacter = line.charAt(0); var lineIndentLength; if (firstCharacter === " ") lineIndentLength = line.match(lineIndentRegex)[0].length; else if (firstCharacter === "\t") lineIndentLength = 1; else continue; this._textModel.setText(new WebInspector.TextRange(lineNumber, 0, lineNumber, lineIndentLength), ""); if (lineNumber === range.startLine) newRange.startColumn = Math.max(0, newRange.startColumn - lineIndentLength); } if (lineIndentLength) newRange.endColumn = Math.max(0, newRange.endColumn - lineIndentLength); this._lastEditedRange = newRange; return newRange; }, handleEnterKey: function() { if (this._dirtyLines) return false; var range = this._getSelection(); if (!range) return false; 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.beginUpdates(); this._enterTextChangeMode(); var lineBreak = this._textModel.lineBreak; var newRange; if (range.isEmpty() && line.substr(range.endColumn - 1, 2) === '{}') { // {|} // becomes // { // | // } newRange = this._setText(range, lineBreak + indent + lineBreak + currentIndent); newRange.endLine--; newRange.endColumn += textEditorIndent.length; } else newRange = this._setText(range, lineBreak + indent); this._exitTextChangeMode(range, newRange); this.endUpdates(); this._restoreSelection(newRange.collapseToEnd(), true); return true; }, _splitChunkOnALine: function(lineNumber, chunkNumber, createSuffixChunk) { var selection = this._getSelection(); var chunk = WebInspector.TextEditorChunkedPanel.prototype._splitChunkOnALine.call(this, lineNumber, chunkNumber, createSuffixChunk); this._restoreSelection(selection); return chunk; }, beginDomUpdates: function() { WebInspector.TextEditorChunkedPanel.prototype.beginDomUpdates.call(this); if (this._domUpdateCoalescingLevel === 1) { this._container.removeEventListener("DOMCharacterDataModified", this._handleDOMUpdatesCallback, false); this._container.removeEventListener("DOMNodeInserted", this._handleDOMUpdatesCallback, false); this._container.removeEventListener("DOMSubtreeModified", this._handleDOMUpdatesCallback, false); } }, endDomUpdates: function() { WebInspector.TextEditorChunkedPanel.prototype.endDomUpdates.call(this); if (this._domUpdateCoalescingLevel === 0) { this._container.addEventListener("DOMCharacterDataModified", this._handleDOMUpdatesCallback, false); this._container.addEventListener("DOMNodeInserted", this._handleDOMUpdatesCallback, false); this._container.addEventListener("DOMSubtreeModified", this._handleDOMUpdatesCallback, false); } }, _enableDOMNodeRemovedListener: function(lineRow, enable) { if (enable) lineRow.addEventListener("DOMNodeRemoved", this._handleDOMUpdatesCallback, false); else lineRow.removeEventListener("DOMNodeRemoved", this._handleDOMUpdatesCallback, false); }, _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); }, /** * @param {boolean=} restoreSelection */ _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);