occaecatidicta
Version:
1,440 lines (1,195 loc) • 80.6 kB
JavaScript
/*
* 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);