node-inspector/front-end/DefaultTextEditor.js

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