blob: c8bad06e8ea509c10ba997eabf5bfe7981c62da5 [file] [log] [blame]
/*
* Copyright (C) 2012 Google 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.
*/
/**
* @implements {UI.TextEditor}
* @unrestricted
*/
export class CodeMirrorTextEditor extends UI.VBox {
/**
* @param {!UI.TextEditor.Options} options
*/
constructor(options) {
super();
this._options = options;
this.registerRequiredCSS('cm/codemirror.css');
this.registerRequiredCSS('text_editor/cmdevtools.css');
const {indentWithTabs, indentUnit} =
CodeMirrorTextEditor._getIndentation(Common.moduleSetting('textEditorIndent').get());
this._codeMirror = new CodeMirror(this.element, {
devtoolsAccessibleName: options.devtoolsAccessibleName,
lineNumbers: options.lineNumbers,
matchBrackets: true,
smartIndent: true,
styleSelectedText: true,
electricChars: true,
styleActiveLine: true,
indentUnit,
indentWithTabs,
lineWrapping: options.lineWrapping,
lineWiseCopyCut: false,
tabIndex: 0,
pollInterval: Math.pow(2, 31) - 1, // ~25 days
inputStyle: 'devToolsAccessibleTextArea'
});
this._codeMirrorElement = this.element.lastElementChild;
this._codeMirror._codeMirrorTextEditor = this;
Common.moduleSetting('textEditorIndent').addChangeListener(this._updateIndentSize.bind(this));
CodeMirror.keyMap['devtools-common'] = {
'Left': 'goCharLeft',
'Right': 'goCharRight',
'Up': 'goLineUp',
'Down': 'goLineDown',
'End': 'goLineEnd',
'Home': 'goLineStartSmart',
'PageUp': 'goSmartPageUp',
'PageDown': 'goSmartPageDown',
'Delete': 'delCharAfter',
'Backspace': 'delCharBefore',
'Tab': 'UserIndent',
'Shift-Tab': 'indentLessOrPass',
'Enter': 'newlineAndIndent',
'Ctrl-Space': 'autocomplete',
'Esc': 'dismiss',
'Ctrl-M': 'gotoMatchingBracket'
};
CodeMirror.keyMap['devtools-pc'] = {
'Ctrl-A': 'selectAll',
'Ctrl-Z': 'undoAndReveal',
'Shift-Ctrl-Z': 'redoAndReveal',
'Ctrl-Y': 'redo',
'Ctrl-Home': 'goDocStart',
'Ctrl-Up': 'goDocStart',
'Ctrl-End': 'goDocEnd',
'Ctrl-Down': 'goDocEnd',
'Ctrl-Left': 'goGroupLeft',
'Ctrl-Right': 'goGroupRight',
'Alt-Left': 'moveCamelLeft',
'Alt-Right': 'moveCamelRight',
'Shift-Alt-Left': 'selectCamelLeft',
'Shift-Alt-Right': 'selectCamelRight',
'Ctrl-Backspace': 'delGroupBefore',
'Ctrl-Delete': 'delGroupAfter',
'Ctrl-/': 'toggleComment',
'Ctrl-D': 'selectNextOccurrence',
'Ctrl-U': 'undoLastSelection',
fallthrough: 'devtools-common'
};
CodeMirror.keyMap['devtools-mac'] = {
'Cmd-A': 'selectAll',
'Cmd-Z': 'undoAndReveal',
'Shift-Cmd-Z': 'redoAndReveal',
'Cmd-Up': 'goDocStart',
'Cmd-Down': 'goDocEnd',
'Alt-Left': 'goGroupLeft',
'Alt-Right': 'goGroupRight',
'Ctrl-Left': 'moveCamelLeft',
'Ctrl-Right': 'moveCamelRight',
'Ctrl-A': 'goLineLeft',
'Ctrl-E': 'goLineRight',
'Ctrl-B': 'goCharLeft',
'Ctrl-F': 'goCharRight',
'Ctrl-Alt-B': 'goGroupLeft',
'Ctrl-Alt-F': 'goGroupRight',
'Ctrl-H': 'delCharBefore',
'Ctrl-D': 'delCharAfter',
'Ctrl-K': 'killLine',
'Ctrl-T': 'transposeChars',
'Ctrl-P': 'goLineUp',
'Ctrl-N': 'goLineDown',
'Shift-Ctrl-Left': 'selectCamelLeft',
'Shift-Ctrl-Right': 'selectCamelRight',
'Cmd-Left': 'goLineStartSmart',
'Cmd-Right': 'goLineEnd',
'Cmd-Backspace': 'delLineLeft',
'Alt-Backspace': 'delGroupBefore',
'Alt-Delete': 'delGroupAfter',
'Cmd-/': 'toggleComment',
'Cmd-D': 'selectNextOccurrence',
'Cmd-U': 'undoLastSelection',
fallthrough: 'devtools-common'
};
if (options.bracketMatchingSetting) {
options.bracketMatchingSetting.addChangeListener(this._enableBracketMatchingIfNeeded, this);
}
this._enableBracketMatchingIfNeeded();
this._codeMirror.setOption('keyMap', Host.isMac() ? 'devtools-mac' : 'devtools-pc');
this._codeMirror.setOption('flattenSpans', false);
let maxHighlightLength = options.maxHighlightLength;
if (typeof maxHighlightLength !== 'number') {
maxHighlightLength = CodeMirrorTextEditor.maxHighlightLength;
}
this._codeMirror.setOption('maxHighlightLength', maxHighlightLength);
this._codeMirror.setOption('mode', null);
this._codeMirror.setOption('crudeMeasuringFrom', 1000);
this._shouldClearHistory = true;
this._lineSeparator = '\n';
CodeMirrorTextEditor._fixWordMovement(this._codeMirror);
this._selectNextOccurrenceController = new SelectNextOccurrenceController(this, this._codeMirror);
this._codeMirror.on('changes', this._changes.bind(this));
this._codeMirror.on('beforeSelectionChange', this._beforeSelectionChange.bind(this));
this._codeMirror.on('cursorActivity', () => {
this.dispatchEventToListeners(UI.TextEditor.Events.CursorChanged);
});
this.element.style.overflow = 'hidden';
this._codeMirrorElement.classList.add('source-code');
this._codeMirrorElement.classList.add('fill');
/** @type {!Platform.Multimap<number, !TextEditor.CodeMirrorTextEditor.Decoration>} */
this._decorations = new Platform.Multimap();
this.element.addEventListener('keydown', this._handleKeyDown.bind(this), true);
this.element.addEventListener('keydown', this._handlePostKeyDown.bind(this), false);
this._needsRefresh = true;
this._readOnly = false;
this._mimeType = '';
if (options.mimeType) {
this.setMimeType(options.mimeType);
}
if (options.autoHeight) {
this._codeMirror.setSize(null, 'auto');
}
this._placeholderElement = null;
if (options.placeholder) {
this._placeholderElement = createElement('pre');
this._placeholderElement.classList.add('placeholder-text');
this._placeholderElement.textContent = options.placeholder;
this._updatePlaceholder();
}
}
/**
* @param {!CodeMirror} codeMirror
*/
static autocompleteCommand(codeMirror) {
const autocompleteController = codeMirror._codeMirrorTextEditor._autocompleteController;
if (autocompleteController) {
autocompleteController.autocomplete(true);
}
}
/**
* @param {!CodeMirror} codeMirror
*/
static undoLastSelectionCommand(codeMirror) {
codeMirror._codeMirrorTextEditor._selectNextOccurrenceController.undoLastSelection();
}
/**
* @param {!CodeMirror} codeMirror
*/
static selectNextOccurrenceCommand(codeMirror) {
codeMirror._codeMirrorTextEditor._selectNextOccurrenceController.selectNextOccurrence();
}
/**
* @param {boolean} shift
* @param {!CodeMirror} codeMirror
*/
static moveCamelLeftCommand(shift, codeMirror) {
codeMirror._codeMirrorTextEditor._doCamelCaseMovement(-1, shift);
}
/**
* @param {boolean} shift
* @param {!CodeMirror} codeMirror
*/
static moveCamelRightCommand(shift, codeMirror) {
codeMirror._codeMirrorTextEditor._doCamelCaseMovement(1, shift);
}
/**
* @param {string} indentationValue
*/
static _getIndentation(indentationValue) {
const indentWithTabs = /\t/.test(indentationValue);
const indentUnit = indentWithTabs ? 4 : indentationValue.length;
return {indentWithTabs, indentUnit};
}
/**
* @param {string} modeName
* @param {string} tokenPrefix
*/
static _overrideModeWithPrefixedTokens(modeName, tokenPrefix) {
const oldModeName = modeName + '-old';
if (CodeMirror.modes[oldModeName]) {
return;
}
CodeMirror.defineMode(oldModeName, CodeMirror.modes[modeName]);
CodeMirror.defineMode(modeName, modeConstructor);
function modeConstructor(config, parserConfig) {
const innerConfig = {};
for (const i in parserConfig) {
innerConfig[i] = parserConfig[i];
}
innerConfig.name = oldModeName;
const codeMirrorMode = CodeMirror.getMode(config, innerConfig);
codeMirrorMode.name = modeName;
codeMirrorMode.token = tokenOverride.bind(null, codeMirrorMode.token);
return codeMirrorMode;
}
function tokenOverride(superToken, stream, state) {
const token = superToken(stream, state);
return token ? tokenPrefix + token.split(/ +/).join(' ' + tokenPrefix) : token;
}
}
/**
* @param {string} mimeType
* @return {!Array<!Root.Runtime.Extension>}}
*/
static _collectUninstalledModes(mimeType) {
const installed = _loadedMimeModeExtensions;
const nameToExtension = new Map();
const extensions = self.runtime.extensions(CodeMirrorMimeMode);
for (const extension of extensions) {
nameToExtension.set(extension.descriptor()['fileName'], extension);
}
const modesToLoad = new Set();
for (const extension of extensions) {
const descriptor = extension.descriptor();
if (installed.has(extension) || descriptor['mimeTypes'].indexOf(mimeType) === -1) {
continue;
}
modesToLoad.add(extension);
const deps = descriptor['dependencies'] || [];
for (let i = 0; i < deps.length; ++i) {
const extension = nameToExtension.get(deps[i]);
if (extension && !installed.has(extension)) {
modesToLoad.add(extension);
}
}
}
return Array.from(modesToLoad);
}
/**
* @param {!Array<!Root.Runtime.Extension>} extensions
* @return {!Promise}
*/
static _installMimeTypeModes(extensions) {
const promises = extensions.map(extension => extension.instance().then(installMode.bind(null, extension)));
return Promise.all(promises);
/**
* @param {!Root.Runtime.Extension} extension
* @param {!Object} instance
*/
function installMode(extension, instance) {
if (_loadedMimeModeExtensions.has(extension)) {
return;
}
const mode = /** @type {!CodeMirrorMimeMode} */ (instance);
mode.install(extension);
_loadedMimeModeExtensions.add(extension);
}
}
/**
* @param {!CodeMirror} codeMirror
*/
static _fixWordMovement(codeMirror) {
function moveLeft(shift, codeMirror) {
codeMirror.setExtending(shift);
const cursor = codeMirror.getCursor('head');
codeMirror.execCommand('goGroupLeft');
const newCursor = codeMirror.getCursor('head');
if (newCursor.ch === 0 && newCursor.line !== 0) {
codeMirror.setExtending(false);
return;
}
const skippedText = codeMirror.getRange(newCursor, cursor, '#');
if (/^\s+$/.test(skippedText)) {
codeMirror.execCommand('goGroupLeft');
}
codeMirror.setExtending(false);
}
function moveRight(shift, codeMirror) {
codeMirror.setExtending(shift);
const cursor = codeMirror.getCursor('head');
codeMirror.execCommand('goGroupRight');
const newCursor = codeMirror.getCursor('head');
if (newCursor.ch === 0 && newCursor.line !== 0) {
codeMirror.setExtending(false);
return;
}
const skippedText = codeMirror.getRange(cursor, newCursor, '#');
if (/^\s+$/.test(skippedText)) {
codeMirror.execCommand('goGroupRight');
}
codeMirror.setExtending(false);
}
const modifierKey = Host.isMac() ? 'Alt' : 'Ctrl';
const leftKey = modifierKey + '-Left';
const rightKey = modifierKey + '-Right';
const keyMap = {};
keyMap[leftKey] = moveLeft.bind(null, false);
keyMap[rightKey] = moveRight.bind(null, false);
keyMap['Shift-' + leftKey] = moveLeft.bind(null, true);
keyMap['Shift-' + rightKey] = moveRight.bind(null, true);
codeMirror.addKeyMap(keyMap);
}
/**
* @protected
* @return {!CodeMirror}
*/
codeMirror() {
return this._codeMirror;
}
/**
* @override
* @return {!UI.Widget}
*/
widget() {
return this;
}
/**
* @override
* @param {string} placeholder
*/
setPlaceholder(placeholder) {
if (!this._placeholderElement) {
this._placeholderElement = createElement('pre');
this._placeholderElement.classList.add('placeholder-text');
}
this._placeholderElement.textContent = placeholder || '';
this._updatePlaceholder();
}
/**
* @param {number} lineNumber
* @param {number} lineLength
* @param {number} charNumber
* @return {{lineNumber: number, columnNumber: number}}
*/
_normalizePositionForOverlappingColumn(lineNumber, lineLength, charNumber) {
const linesCount = this._codeMirror.lineCount();
let columnNumber = charNumber;
if (charNumber < 0 && lineNumber > 0) {
--lineNumber;
columnNumber = this.line(lineNumber).length;
} else if (charNumber >= lineLength && lineNumber < linesCount - 1) {
++lineNumber;
columnNumber = 0;
} else {
columnNumber = Number.constrain(charNumber, 0, lineLength);
}
return {lineNumber: lineNumber, columnNumber: columnNumber};
}
/**
* @param {number} lineNumber
* @param {number} columnNumber
* @param {number} direction
* @return {{lineNumber: number, columnNumber: number}}
*/
_camelCaseMoveFromPosition(lineNumber, columnNumber, direction) {
/**
* @param {number} charNumber
* @param {number} length
* @return {boolean}
*/
function valid(charNumber, length) {
return charNumber >= 0 && charNumber < length;
}
/**
* @param {string} text
* @param {number} charNumber
* @return {boolean}
*/
function isWordStart(text, charNumber) {
const position = charNumber;
const nextPosition = charNumber + 1;
return valid(position, text.length) && valid(nextPosition, text.length) &&
TextUtils.TextUtils.isWordChar(text[position]) && TextUtils.TextUtils.isWordChar(text[nextPosition]) &&
TextUtils.TextUtils.isUpperCase(text[position]) && TextUtils.TextUtils.isLowerCase(text[nextPosition]);
}
/**
* @param {string} text
* @param {number} charNumber
* @return {boolean}
*/
function isWordEnd(text, charNumber) {
const position = charNumber;
const prevPosition = charNumber - 1;
return valid(position, text.length) && valid(prevPosition, text.length) &&
TextUtils.TextUtils.isWordChar(text[position]) && TextUtils.TextUtils.isWordChar(text[prevPosition]) &&
TextUtils.TextUtils.isUpperCase(text[position]) && TextUtils.TextUtils.isLowerCase(text[prevPosition]);
}
/**
* @param {number} lineNumber
* @param {number} lineLength
* @param {number} columnNumber
* @return {{lineNumber: number, columnNumber: number}}
*/
function constrainPosition(lineNumber, lineLength, columnNumber) {
return {lineNumber: lineNumber, columnNumber: Number.constrain(columnNumber, 0, lineLength)};
}
const text = this.line(lineNumber);
const length = text.length;
if ((columnNumber === length && direction === 1) || (columnNumber === 0 && direction === -1)) {
return this._normalizePositionForOverlappingColumn(lineNumber, length, columnNumber + direction);
}
let charNumber = direction === 1 ? columnNumber : columnNumber - 1;
// Move through initial spaces if any.
while (valid(charNumber, length) && TextUtils.TextUtils.isSpaceChar(text[charNumber])) {
charNumber += direction;
}
if (!valid(charNumber, length)) {
return constrainPosition(lineNumber, length, charNumber);
}
if (TextUtils.TextUtils.isStopChar(text[charNumber])) {
while (valid(charNumber, length) && TextUtils.TextUtils.isStopChar(text[charNumber])) {
charNumber += direction;
}
if (!valid(charNumber, length)) {
return constrainPosition(lineNumber, length, charNumber);
}
return {lineNumber: lineNumber, columnNumber: direction === -1 ? charNumber + 1 : charNumber};
}
charNumber += direction;
while (valid(charNumber, length) && !isWordStart(text, charNumber) && !isWordEnd(text, charNumber) &&
TextUtils.TextUtils.isWordChar(text[charNumber])) {
charNumber += direction;
}
if (!valid(charNumber, length)) {
return constrainPosition(lineNumber, length, charNumber);
}
if (isWordStart(text, charNumber) || isWordEnd(text, charNumber)) {
return {lineNumber: lineNumber, columnNumber: charNumber};
}
return {lineNumber: lineNumber, columnNumber: direction === -1 ? charNumber + 1 : charNumber};
}
/**
* @param {number} direction
* @param {boolean} shift
*/
_doCamelCaseMovement(direction, shift) {
const selections = this.selections();
for (let i = 0; i < selections.length; ++i) {
const selection = selections[i];
const move = this._camelCaseMoveFromPosition(selection.endLine, selection.endColumn, direction);
selection.endLine = move.lineNumber;
selection.endColumn = move.columnNumber;
if (!shift) {
selections[i] = selection.collapseToEnd();
}
}
this.setSelections(selections);
}
dispose() {
if (this._options.bracketMatchingSetting) {
this._options.bracketMatchingSetting.removeChangeListener(this._enableBracketMatchingIfNeeded, this);
}
}
_enableBracketMatchingIfNeeded() {
this._codeMirror.setOption(
'autoCloseBrackets', (this._options.bracketMatchingSetting && this._options.bracketMatchingSetting.get()) ?
{explode: false} :
false);
}
/**
* @override
*/
wasShown() {
if (this._needsRefresh) {
this.refresh();
}
}
/**
* @protected
*/
refresh() {
if (this.isShowing()) {
this._codeMirror.refresh();
this._needsRefresh = false;
return;
}
this._needsRefresh = true;
}
/**
* @override
*/
willHide() {
delete this._editorSizeInSync;
}
undo() {
this._codeMirror.undo();
}
redo() {
this._codeMirror.redo();
}
/**
* @param {!Event} e
*/
_handleKeyDown(e) {
if (e.key === 'Tab' && Common.moduleSetting('textEditorTabMovesFocus').get()) {
e.consume(false);
return;
}
if (this._autocompleteController && this._autocompleteController.keyDown(e)) {
e.consume(true);
}
}
/**
* @param {!Event} e
*/
_handlePostKeyDown(e) {
if (e.defaultPrevented) {
e.consume(true);
}
}
/**
* @override
* @param {?UI.AutocompleteConfig} config
*/
configureAutocomplete(config) {
if (this._autocompleteController) {
this._autocompleteController.dispose();
delete this._autocompleteController;
}
if (config) {
this._autocompleteController = new TextEditor.TextEditorAutocompleteController(this, this._codeMirror, config);
}
}
/**
* @param {number} lineNumber
* @param {number} column
* @return {?{x: number, y: number, height: number}}
*/
cursorPositionToCoordinates(lineNumber, column) {
if (lineNumber >= this._codeMirror.lineCount() || lineNumber < 0 || column < 0 ||
column > this._codeMirror.getLine(lineNumber).length) {
return null;
}
const metrics = this._codeMirror.cursorCoords(new CodeMirror.Pos(lineNumber, column));
return {x: metrics.left, y: metrics.top, height: metrics.bottom - metrics.top};
}
/**
* @param {number} x
* @param {number} y
* @return {?TextUtils.TextRange}
*/
coordinatesToCursorPosition(x, y) {
const element = this.element.ownerDocument.elementFromPoint(x, y);
if (!element || !element.isSelfOrDescendant(this._codeMirror.getWrapperElement())) {
return null;
}
const gutterBox = this._codeMirror.getGutterElement().boxInWindow();
if (x >= gutterBox.x && x <= gutterBox.x + gutterBox.width && y >= gutterBox.y &&
y <= gutterBox.y + gutterBox.height) {
return null;
}
const coords = this._codeMirror.coordsChar({left: x, top: y});
return TextEditor.CodeMirrorUtils.toRange(coords, coords);
}
/**
* @override
* @param {number} lineNumber
* @param {number} columnNumber
* @return {!{x: number, y: number}}
*/
visualCoordinates(lineNumber, columnNumber) {
const metrics = this._codeMirror.cursorCoords(new CodeMirror.Pos(lineNumber, columnNumber));
return {x: metrics.left, y: metrics.top};
}
/**
* @override
* @param {number} lineNumber
* @param {number} columnNumber
* @return {?{startColumn: number, endColumn: number, type: string}}
*/
tokenAtTextPosition(lineNumber, columnNumber) {
if (lineNumber < 0 || lineNumber >= this._codeMirror.lineCount()) {
return null;
}
const token = this._codeMirror.getTokenAt(new CodeMirror.Pos(lineNumber, (columnNumber || 0) + 1));
if (!token) {
return null;
}
return {startColumn: token.start, endColumn: token.end, type: token.type};
}
/**
* @param {number} generation
* @return {boolean}
*/
isClean(generation) {
return this._codeMirror.isClean(generation);
}
/**
* @return {number}
*/
markClean() {
return this._codeMirror.changeGeneration(true);
}
/**
* @return {boolean}
*/
_hasLongLines() {
function lineIterator(lineHandle) {
if (lineHandle.text.length > CodeMirrorTextEditor.LongLineModeLineLengthThreshold) {
hasLongLines = true;
}
return hasLongLines;
}
let hasLongLines = false;
this._codeMirror.eachLine(lineIterator);
return hasLongLines;
}
_enableLongLinesMode() {
this._codeMirror.setOption('styleSelectedText', false);
}
_disableLongLinesMode() {
this._codeMirror.setOption('styleSelectedText', true);
}
/**
* @param {!{data: *}} updatedValue
*/
_updateIndentSize(updatedValue) {
const {indentWithTabs, indentUnit} =
CodeMirrorTextEditor._getIndentation(/** @type {string} */ (updatedValue.data));
this._codeMirror.setOption('indentUnit', indentUnit);
this._codeMirror.setOption('indentWithTabs', indentWithTabs);
}
/**
* @param {string} mimeType
*/
setMimeType(mimeType) {
this._mimeType = mimeType;
const modesToLoad = CodeMirrorTextEditor._collectUninstalledModes(mimeType);
if (!modesToLoad.length) {
setMode.call(this);
} else {
CodeMirrorTextEditor._installMimeTypeModes(modesToLoad).then(setMode.bind(this));
}
/**
* @this {CodeMirrorTextEditor}
*/
function setMode() {
const rewrittenMimeType = this.rewriteMimeType(mimeType);
if (this._codeMirror.options.mode !== rewrittenMimeType) {
this._codeMirror.setOption('mode', rewrittenMimeType);
}
}
}
/**
* @param {!Object} mode
*/
setHighlightMode(mode) {
this._mimeType = '';
this._codeMirror.setOption('mode', mode);
}
/**
* @protected
* @param {string} mimeType
*/
rewriteMimeType(mimeType) {
// Overridden in SourcesTextEditor
return mimeType;
}
/**
* @protected
* @return {string}
*/
mimeType() {
return this._mimeType;
}
/**
* @param {boolean} readOnly
*/
setReadOnly(readOnly) {
if (this._readOnly === readOnly) {
return;
}
this.clearPositionHighlight();
this._readOnly = readOnly;
this.element.classList.toggle('CodeMirror-readonly', readOnly);
this._codeMirror.setOption('readOnly', readOnly);
}
/**
* @return {boolean}
*/
readOnly() {
return !!this._codeMirror.getOption('readOnly');
}
/**
* @param {function(number):string} formatter
*/
setLineNumberFormatter(formatter) {
this._codeMirror.setOption('lineNumberFormatter', formatter);
}
/**
* @override
* @param {function(!KeyboardEvent)} handler
*/
addKeyDownHandler(handler) {
this._codeMirror.on('keydown', (CodeMirror, event) => handler(event));
}
/**
* @param {number} lineNumber
* @param {number} columnNumber
* @param {!Element} element
* @param {symbol} type
* @param {boolean=} insertBefore
* @return {!TextEditorBookMark}
*/
addBookmark(lineNumber, columnNumber, element, type, insertBefore) {
const bookmark = new TextEditorBookMark(
this._codeMirror.setBookmark(
new CodeMirror.Pos(lineNumber, columnNumber), {widget: element, insertLeft: insertBefore}),
type, this);
this._updateDecorations(lineNumber);
return bookmark;
}
/**
* @param {!TextUtils.TextRange} range
* @param {symbol=} type
* @return {!Array.<!TextEditorBookMark>}
*/
bookmarks(range, type) {
const pos = TextEditor.CodeMirrorUtils.toPos(range);
let markers = this._codeMirror.findMarksAt(pos.start);
if (!range.isEmpty()) {
const middleMarkers = this._codeMirror.findMarks(pos.start, pos.end);
const endMarkers = this._codeMirror.findMarksAt(pos.end);
markers = markers.concat(middleMarkers, endMarkers);
}
const bookmarks = [];
for (let i = 0; i < markers.length; i++) {
const bookmark = markers[i][TextEditorBookMark._symbol];
if (bookmark && (!type || bookmark.type() === type)) {
bookmarks.push(bookmark);
}
}
return bookmarks;
}
/**
* @override
*/
focus() {
this._codeMirror.focus();
}
/**
* @override
* @return {boolean}
*/
hasFocus() {
return this._codeMirror.hasFocus();
}
/**
* @param {function()} operation
*/
operation(operation) {
this._codeMirror.operation(operation);
}
/**
* @param {number} lineNumber
*/
scrollLineIntoView(lineNumber) {
this._innerRevealLine(lineNumber, this._codeMirror.getScrollInfo());
}
/**
* @param {number} lineNumber
* @param {!{left: number, top: number, width: number, height: number, clientWidth: number, clientHeight: number}} scrollInfo
*/
_innerRevealLine(lineNumber, scrollInfo) {
const topLine = this._codeMirror.lineAtHeight(scrollInfo.top, 'local');
const bottomLine = this._codeMirror.lineAtHeight(scrollInfo.top + scrollInfo.clientHeight, 'local');
const linesPerScreen = bottomLine - topLine + 1;
if (lineNumber < topLine) {
const topLineToReveal = Math.max(lineNumber - (linesPerScreen / 2) + 1, 0) | 0;
this._codeMirror.scrollIntoView(new CodeMirror.Pos(topLineToReveal, 0));
} else if (lineNumber > bottomLine) {
const bottomLineToReveal = Math.min(lineNumber + (linesPerScreen / 2) - 1, this.linesCount - 1) | 0;
this._codeMirror.scrollIntoView(new CodeMirror.Pos(bottomLineToReveal, 0));
}
}
/**
* @param {!Element} element
* @param {number} lineNumber
* @param {number=} startColumn
* @param {number=} endColumn
*/
addDecoration(element, lineNumber, startColumn, endColumn) {
const widget = this._codeMirror.addLineWidget(lineNumber, element);
let update = null;
if (typeof startColumn !== 'undefined') {
if (typeof endColumn === 'undefined') {
endColumn = Infinity;
}
update = this._updateFloatingDecoration.bind(this, element, lineNumber, startColumn, endColumn);
update();
}
this._decorations.set(lineNumber, {element: element, update: update, widget: widget});
}
/**
* @param {!Element} element
* @param {number} lineNumber
* @param {number} startColumn
* @param {number} endColumn
*/
_updateFloatingDecoration(element, lineNumber, startColumn, endColumn) {
const base = this._codeMirror.cursorCoords(new CodeMirror.Pos(lineNumber, 0), 'page');
const start = this._codeMirror.cursorCoords(new CodeMirror.Pos(lineNumber, startColumn), 'page');
const end = this._codeMirror.charCoords(new CodeMirror.Pos(lineNumber, endColumn), 'page');
element.style.width = (end.right - start.left) + 'px';
element.style.left = (start.left - base.left) + 'px';
}
/**
* @param {number} lineNumber
*/
_updateDecorations(lineNumber) {
this._decorations.get(lineNumber).forEach(innerUpdateDecorations);
/**
* @param {!TextEditor.CodeMirrorTextEditor.Decoration} decoration
*/
function innerUpdateDecorations(decoration) {
if (decoration.update) {
decoration.update();
}
}
}
/**
* @param {!Element} element
* @param {number} lineNumber
*/
removeDecoration(element, lineNumber) {
this._decorations.get(lineNumber).forEach(innerRemoveDecoration.bind(this));
/**
* @this {CodeMirrorTextEditor}
* @param {!TextEditor.CodeMirrorTextEditor.Decoration} decoration
*/
function innerRemoveDecoration(decoration) {
if (decoration.element !== element) {
return;
}
this._codeMirror.removeLineWidget(decoration.widget);
this._decorations.delete(lineNumber, decoration);
}
}
/**
* @param {number} lineNumber 0-based
* @param {number=} columnNumber
* @param {boolean=} shouldHighlight
*/
revealPosition(lineNumber, columnNumber, shouldHighlight) {
lineNumber = Number.constrain(lineNumber, 0, this._codeMirror.lineCount() - 1);
if (typeof columnNumber !== 'number') {
columnNumber = 0;
}
columnNumber = Number.constrain(columnNumber, 0, this._codeMirror.getLine(lineNumber).length);
this.clearPositionHighlight();
this._highlightedLine = this._codeMirror.getLineHandle(lineNumber);
if (!this._highlightedLine) {
return;
}
this.scrollLineIntoView(lineNumber);
if (shouldHighlight) {
this._codeMirror.addLineClass(
this._highlightedLine, null, this._readOnly ? 'cm-readonly-highlight' : 'cm-highlight');
if (!this._readOnly) {
this._clearHighlightTimeout = setTimeout(this.clearPositionHighlight.bind(this), 2000);
}
}
this.setSelection(TextUtils.TextRange.createFromLocation(lineNumber, columnNumber));
}
clearPositionHighlight() {
if (this._clearHighlightTimeout) {
clearTimeout(this._clearHighlightTimeout);
}
delete this._clearHighlightTimeout;
if (this._highlightedLine) {
this._codeMirror.removeLineClass(
this._highlightedLine, null, this._readOnly ? 'cm-readonly-highlight' : 'cm-highlight');
}
delete this._highlightedLine;
}
/**
* @override
* @return {!Array.<!Element>}
*/
elementsToRestoreScrollPositionsFor() {
return [];
}
/**
* @param {number} width
* @param {number} height
*/
_updatePaddingBottom(width, height) {
if (!this._options.padBottom) {
return;
}
const scrollInfo = this._codeMirror.getScrollInfo();
let newPaddingBottom;
const linesElement = this._codeMirrorElement.querySelector('.CodeMirror-lines');
const lineCount = this._codeMirror.lineCount();
if (lineCount <= 1) {
newPaddingBottom = 0;
} else {
newPaddingBottom =
Math.max(scrollInfo.clientHeight - this._codeMirror.getLineHandle(this._codeMirror.lastLine()).height, 0);
}
newPaddingBottom += 'px';
linesElement.style.paddingBottom = newPaddingBottom;
this._codeMirror.setSize(width, height);
}
_resizeEditor() {
const parentElement = this.element.parentElement;
if (!parentElement || !this.isShowing()) {
return;
}
this._codeMirror.operation(() => {
const scrollLeft = this._codeMirror.doc.scrollLeft;
const scrollTop = this._codeMirror.doc.scrollTop;
const width = parentElement.offsetWidth;
const height = parentElement.offsetHeight - this.element.offsetTop;
if (this._options.autoHeight) {
this._codeMirror.setSize(width, 'auto');
} else {
this._codeMirror.setSize(width, height);
this._updatePaddingBottom(width, height);
}
this._codeMirror.scrollTo(scrollLeft, scrollTop);
});
}
/**
* @override
*/
onResize() {
if (this._autocompleteController) {
this._autocompleteController.clearAutocomplete();
}
this._resizeEditor();
this._editorSizeInSync = true;
if (this._selectionSetScheduled) {
delete this._selectionSetScheduled;
this.setSelection(this._lastSelection);
}
}
/**
* @param {!TextUtils.TextRange} range
* @param {string} text
* @param {string=} origin
* @return {!TextUtils.TextRange}
*/
editRange(range, text, origin) {
const pos = TextEditor.CodeMirrorUtils.toPos(range);
this._codeMirror.replaceRange(text, pos.start, pos.end, origin);
const newRange = TextEditor.CodeMirrorUtils.toRange(
pos.start, this._codeMirror.posFromIndex(this._codeMirror.indexFromPos(pos.start) + text.length));
this.dispatchEventToListeners(UI.TextEditor.Events.TextChanged, {oldRange: range, newRange: newRange});
return newRange;
}
/**
* @override
*/
clearAutocomplete() {
if (this._autocompleteController) {
this._autocompleteController.clearAutocomplete();
}
}
/**
* @param {number} lineNumber
* @param {number} column
* @param {function(string):boolean} isWordChar
* @return {!TextUtils.TextRange}
*/
wordRangeForCursorPosition(lineNumber, column, isWordChar) {
const line = this.line(lineNumber);
let wordStart = column;
if (column !== 0 && isWordChar(line.charAt(column - 1))) {
wordStart = column - 1;
while (wordStart > 0 && isWordChar(line.charAt(wordStart - 1))) {
--wordStart;
}
}
let wordEnd = column;
while (wordEnd < line.length && isWordChar(line.charAt(wordEnd))) {
++wordEnd;
}
return new TextUtils.TextRange(lineNumber, wordStart, lineNumber, wordEnd);
}
/**
* @param {!CodeMirror} codeMirror
* @param {!Array.<!CodeMirror.ChangeObject>} changes
*/
_changes(codeMirror, changes) {
if (!changes.length) {
return;
}
this._updatePlaceholder();
// We do not show "scroll beyond end of file" span for one line documents, so we need to check if "document has one line" changed.
const hasOneLine = this._codeMirror.lineCount() === 1;
if (hasOneLine !== this._hasOneLine) {
this._resizeEditor();
}
this._hasOneLine = hasOneLine;
this._decorations.valuesArray().forEach(decoration => this._codeMirror.removeLineWidget(decoration.widget));
this._decorations.clear();
const edits = [];
let currentEdit;
for (let changeIndex = 0; changeIndex < changes.length; ++changeIndex) {
const changeObject = changes[changeIndex];
const edit = TextEditor.CodeMirrorUtils.changeObjectToEditOperation(changeObject);
if (currentEdit && edit.oldRange.equal(currentEdit.newRange)) {
currentEdit.newRange = edit.newRange;
} else {
currentEdit = edit;
edits.push(currentEdit);
}
}
for (let i = 0; i < edits.length; i++) {
this.dispatchEventToListeners(
UI.TextEditor.Events.TextChanged, {oldRange: edits[i].oldRange, newRange: edits[i].newRange});
}
}
/**
* @param {!CodeMirror} codeMirror
* @param {{ranges: !Array.<{head: !CodeMirror.Pos, anchor: !CodeMirror.Pos}>}} selection
*/
_beforeSelectionChange(codeMirror, selection) {
this._selectNextOccurrenceController.selectionWillChange();
}
/**
* @param {number} lineNumber
*/
scrollToLine(lineNumber) {
const pos = new CodeMirror.Pos(lineNumber, 0);
const coords = this._codeMirror.charCoords(pos, 'local');
this._codeMirror.scrollTo(0, coords.top);
}
/**
* @return {number}
*/
firstVisibleLine() {
return this._codeMirror.lineAtHeight(this._codeMirror.getScrollInfo().top, 'local');
}
/**
* @return {number}
*/
scrollTop() {
return this._codeMirror.getScrollInfo().top;
}
/**
* @param {number} scrollTop
*/
setScrollTop(scrollTop) {
this._codeMirror.scrollTo(0, scrollTop);
}
/**
* @return {number}
*/
lastVisibleLine() {
const scrollInfo = this._codeMirror.getScrollInfo();
return this._codeMirror.lineAtHeight(scrollInfo.top + scrollInfo.clientHeight, 'local');
}
/**
* @override
* @return {!TextUtils.TextRange}
*/
selection() {
const start = this._codeMirror.getCursor('anchor');
const end = this._codeMirror.getCursor('head');
return TextEditor.CodeMirrorUtils.toRange(start, end);
}
/**
* @return {!Array.<!TextUtils.TextRange>}
*/
selections() {
const selectionList = this._codeMirror.listSelections();
const result = [];
for (let i = 0; i < selectionList.length; ++i) {
const selection = selectionList[i];
result.push(TextEditor.CodeMirrorUtils.toRange(selection.anchor, selection.head));
}
return result;
}
/**
* @return {?TextUtils.TextRange}
*/
lastSelection() {
return this._lastSelection;
}
/**
* @override
* @param {!TextUtils.TextRange} textRange
* @param {boolean=} dontScroll
*/
setSelection(textRange, dontScroll) {
this._lastSelection = textRange;
if (!this._editorSizeInSync) {
this._selectionSetScheduled = true;
return;
}
const pos = TextEditor.CodeMirrorUtils.toPos(textRange);
this._codeMirror.setSelection(pos.start, pos.end, {scroll: !dontScroll});
}
/**
* @param {!Array.<!TextUtils.TextRange>} ranges
* @param {number=} primarySelectionIndex
*/
setSelections(ranges, primarySelectionIndex) {
const selections = [];
for (let i = 0; i < ranges.length; ++i) {
const selection = TextEditor.CodeMirrorUtils.toPos(ranges[i]);
selections.push({anchor: selection.start, head: selection.end});
}
primarySelectionIndex = primarySelectionIndex || 0;
this._codeMirror.setSelections(selections, primarySelectionIndex, {scroll: false});
}
/**
* @param {string} text
*/
_detectLineSeparator(text) {
this._lineSeparator = text.indexOf('\r\n') >= 0 ? '\r\n' : '\n';
}
/**
* @override
* @param {string} text
*/
setText(text) {
if (text.length > CodeMirrorTextEditor.MaxEditableTextSize) {
this.configureAutocomplete(null);
this.setReadOnly(true);
}
this._codeMirror.setValue(text);
if (this._shouldClearHistory) {
this._codeMirror.clearHistory();
this._shouldClearHistory = false;
}
this._detectLineSeparator(text);
if (this._hasLongLines()) {
this._enableLongLinesMode();
} else {
this._disableLongLinesMode();
}
if (!this.isShowing()) {
this.refresh();
}
}
/**
* @override
* @param {!TextUtils.TextRange=} textRange
* @return {string}
*/
text(textRange) {
if (!textRange) {
return this._codeMirror.getValue(this._lineSeparator);
}
const pos = TextEditor.CodeMirrorUtils.toPos(textRange.normalize());
return this._codeMirror.getRange(pos.start, pos.end, this._lineSeparator);
}
/**
* @override
* @return {string}
*/
textWithCurrentSuggestion() {
if (!this._autocompleteController) {
return this.text();
}
return this._autocompleteController.textWithCurrentSuggestion();
}
/**
* @override
* @return {!TextUtils.TextRange}
*/
fullRange() {
const lineCount = this.linesCount;
const lastLine = this._codeMirror.getLine(lineCount - 1);
return TextEditor.CodeMirrorUtils.toRange(
new CodeMirror.Pos(0, 0), new CodeMirror.Pos(lineCount - 1, lastLine.length));
}
/**
* @return {number}
*/
currentLineNumber() {
return this._codeMirror.getCursor().line;
}
/**
* @override
* @param {number} lineNumber
* @return {string}
*/
line(lineNumber) {
return this._codeMirror.getLine(lineNumber);
}
/**
* @return {number}
*/
get linesCount() {
return this._codeMirror.lineCount();
}
/**
* @override
*/
newlineAndIndent() {
this._codeMirror.execCommand('newlineAndIndent');
}
/**
* @param {number} lineNumber
* @param {number} columnNumber
* @return {!TextEditorPositionHandle}
*/
textEditorPositionHandle(lineNumber, columnNumber) {
return new CodeMirrorPositionHandle(this._codeMirror, new CodeMirror.Pos(lineNumber, columnNumber));
}
_updatePlaceholder() {
if (!this._placeholderElement) {
return;
}
this._placeholderElement.remove();
if (this.linesCount === 1 && !this.line(0)) {
this._codeMirror.display.lineSpace.insertBefore(
this._placeholderElement, this._codeMirror.display.lineSpace.firstChild);
}
}
}
CodeMirrorTextEditor.maxHighlightLength = 1000;
CodeMirrorTextEditor.LongLineModeLineLengthThreshold = 2000;
CodeMirrorTextEditor.MaxEditableTextSize = 1024 * 1024 * 10;
CodeMirrorTextEditor._overrideModeWithPrefixedTokens('css', 'css-');
CodeMirrorTextEditor._overrideModeWithPrefixedTokens('javascript', 'js-');
CodeMirrorTextEditor._overrideModeWithPrefixedTokens('xml', 'xml-');
CodeMirror.commands.autocomplete = CodeMirrorTextEditor.autocompleteCommand;
CodeMirror.commands.undoLastSelection = CodeMirrorTextEditor.undoLastSelectionCommand;
CodeMirror.commands.selectNextOccurrence = CodeMirrorTextEditor.selectNextOccurrenceCommand;
CodeMirror.commands.moveCamelLeft = CodeMirrorTextEditor.moveCamelLeftCommand.bind(null, false);
CodeMirror.commands.selectCamelLeft = CodeMirrorTextEditor.moveCamelLeftCommand.bind(null, true);
CodeMirror.commands.moveCamelRight = CodeMirrorTextEditor.moveCamelRightCommand.bind(null, false);
CodeMirror.commands.selectCamelRight = CodeMirrorTextEditor.moveCamelRightCommand.bind(null, true);
/**
* @param {!CodeMirror} codeMirror
*/
CodeMirror.commands.UserIndent = function(codeMirror) {
const ranges = codeMirror.listSelections();
if (ranges.length === 0) {
return;
}
if (codeMirror.somethingSelected()) {
codeMirror.indentSelection('add');
return;
}
const indentation = Common.moduleSetting('textEditorIndent').get();
codeMirror.replaceSelection(indentation);
};
/**
* @param {!CodeMirror} codeMirror
* @return {!Object|undefined}
*/
CodeMirror.commands.indentLessOrPass = function(codeMirror) {
const selections = codeMirror.listSelections();
if (selections.length === 1) {
const range = TextEditor.CodeMirrorUtils.toRange(selections[0].anchor, selections[0].head);
if (range.isEmpty() && !/^\s/.test(codeMirror.getLine(range.startLine))) {
return CodeMirror.Pass;
}
}
codeMirror.execCommand('indentLess');
};
/**
* @param {!CodeMirror} codeMirror
*/
CodeMirror.commands.gotoMatchingBracket = function(codeMirror) {
const updatedSelections = [];
const selections = codeMirror.listSelections();
for (let i = 0; i < selections.length; ++i) {
const selection = selections[i];
const cursor = selection.head;
const matchingBracket = codeMirror.findMatchingBracket(cursor, false, {maxScanLines: 10000});
let updatedHead = cursor;
if (matchingBracket && matchingBracket.match) {
const columnCorrection = CodeMirror.cmpPos(matchingBracket.from, cursor) === 0 ? 1 : 0;
updatedHead = new CodeMirror.Pos(matchingBracket.to.line, matchingBracket.to.ch + columnCorrection);
}
updatedSelections.push({anchor: updatedHead, head: updatedHead});
}
codeMirror.setSelections(updatedSelections);
};
/**
* @param {!CodeMirror} codemirror
*/
CodeMirror.commands.undoAndReveal = function(codemirror) {
const scrollInfo = codemirror.getScrollInfo();
codemirror.execCommand('undo');
const cursor = codemirror.getCursor('start');
codemirror._codeMirrorTextEditor._innerRevealLine(cursor.line, scrollInfo);
const autocompleteController = codemirror._codeMirrorTextEditor._autocompleteController;
if (autocompleteController) {
autocompleteController.clearAutocomplete();
}
};
/**
* @param {!CodeMirror} codemirror
*/
CodeMirror.commands.redoAndReveal = function(codemirror) {
const scrollInfo = codemirror.getScrollInfo();
codemirror.execCommand('redo');
const cursor = codemirror.getCursor('start');
codemirror._codeMirrorTextEditor._innerRevealLine(cursor.line, scrollInfo);
const autocompleteController = codemirror._codeMirrorTextEditor._autocompleteController;
if (autocompleteController) {
autocompleteController.clearAutocomplete();
}
};
/**
* @param {!CodeMirror} codemirror
* @return {!Object|undefined}
*/
CodeMirror.commands.dismiss = function(codemirror) {
const selections = codemirror.listSelections();
const selection = selections[0];
if (selections.length === 1) {
if (TextEditor.CodeMirrorUtils.toRange(selection.anchor, selection.head).isEmpty()) {
return CodeMirror.Pass;
}
codemirror.setSelection(selection.anchor, selection.anchor, {scroll: false});
codemirror._codeMirrorTextEditor.scrollLineIntoView(selection.anchor.line);
return;
}
codemirror.setSelection(selection.anchor, selection.head, {scroll: false});
codemirror._codeMirrorTextEditor.scrollLineIntoView(selection.anchor.line);
};
/**
* @param {!CodeMirror} codemirror
* @return {!Object|undefined}
*/
CodeMirror.commands.goSmartPageUp = function(codemirror) {
if (codemirror._codeMirrorTextEditor.selection().equal(TextUtils.TextRange.createFromLocation(0, 0))) {
return CodeMirror.Pass;
}
codemirror.execCommand('goPageUp');
};
/**
* @param {!CodeMirror} codemirror
* @return {!Object|undefined}
*/
CodeMirror.commands.goSmartPageDown = function(codemirror) {
if (codemirror._codeMirrorTextEditor.selection().equal(
codemirror._codeMirrorTextEditor.fullRange().collapseToEnd())) {
return CodeMirror.Pass;
}
codemirror.execCommand('goPageDown');
};
/**
* @implements {TextEditorPositionHandle}
* @unrestricted
*/
export class CodeMirrorPositionHandle {
/**
* @param {!CodeMirror} codeMirror
* @param {!CodeMirror.Pos} pos
*/
constructor(codeMirror, pos) {
this._codeMirror = codeMirror;
this._lineHandle = codeMirror.getLineHandle(pos.line);
this._columnNumber = pos.ch;
}
/**
* @override
* @return {?{lineNumber: number, columnNumber: number}}
*/
resolve() {
const lineNumber = this._lineHandle ? this._codeMirror.getLineNumber(this._lineHandle) : null;
if (typeof lineNumber !== 'number') {
return null;
}
return {lineNumber: lineNumber, columnNumber: this._columnNumber};
}
/**
* @override
* @param {!TextEditorPositionHandle} argPositionHandle
* @return {boolean}
*/
equal(argPositionHandle) {
const positionHandle = /** @type {!CodeMirrorPositionHandle} */ (argPositionHandle);
return positionHandle._lineHandle === this._lineHandle && positionHandle._columnNumber === this._columnNumber &&
positionHandle._codeMirror === this._codeMirror;
}
}
/**
* @unrestricted
*/
export class SelectNextOccurrenceController {
/**
* @param {!CodeMirrorTextEditor} textEditor
* @param {!CodeMirror} codeMirror
*/
constructor(textEditor, codeMirror) {
this._textEditor = textEditor;
this._codeMirror = codeMirror;
}
selectionWillChange() {
if (!this._muteSelectionListener) {
delete this._fullWordSelection;
}
}
/**
* @param {!Array.<!TextUtils.TextRange>} selections
* @param {!TextUtils.TextRange} range
* @return {boolean}
*/
_findRange(selections, range) {
for (let i = 0; i < selections.length; ++i) {
if (range.equal(selections[i])) {
return true;
}
}
return false;
}
undoLastSelection() {
this._muteSelectionListener = true;
this._codeMirror.execCommand('undoSelection');
this._muteSelectionListener = false;
}
selectNextOccurrence() {
const selections = this._textEditor.selections();
let anyEmptySelection = false;
for (let i = 0; i < selections.length; ++i) {
const selection = selections[i];
anyEmptySelection = anyEmptySelection || selection.isEmpty();
if (selection.startLine !== selection.endLine) {
return;
}
}
if (anyEmptySelection) {
this._expandSelectionsToWords(selections);
return;
}
const last = selections[selections.length - 1];
let next = last;
do {
next = this._findNextOccurrence(next, !!this._fullWordSelection);
} while (next && this._findRange(selections, next) && !next.equal(last));
if (!next) {
return;
}
selections.push(next);
this._muteSelectionListener = true;
this._textEditor.setSelections(selections, selections.length - 1);
delete this._muteSelectionListener;
this._textEditor.scrollLineIntoView(next.startLine);
}
/**
* @param {!Array.<!TextUtils.TextRange>} selections
*/
_expandSelectionsToWords(selections) {
const newSelections = [];
for (let i = 0; i < selections.length; ++i) {
const selection = selections[i];
const startRangeWord = this._textEditor.wordRangeForCursorPosition(
selection.startLine, selection.startColumn, TextUtils.TextUtils.isWordChar) ||
TextUtils.TextRange.createFromLocation(selection.startLine, selection.startColumn);
const endRangeWord = this._textEditor.wordRangeForCursorPosition(
selection.endLine, selection.endColumn, TextUtils.TextUtils.isWordChar) ||
TextUtils.TextRange.createFromLocation(selection.endLine, selection.endColumn);
const newSelection = new TextUtils.TextRange(
startRangeWord.startLine, startRangeWord.startColumn, endRangeWord.endLine, endRangeWord.endColumn);
newSelections.push(newSelection);
}
this._textEditor.setSelections(newSelections, newSelections.length - 1);
this._fullWordSelection = true;
}
/**
* @param {!TextUtils.TextRange} range
* @param {boolean} fullWord
* @return {?TextUtils.TextRange}
*/
_findNextOccurrence(range, fullWord) {
range = range.normalize();
let matchedLineNumber;
let matchedColumnNumber;
const textToFind = this._textEditor.text(range);
function findWordInLine(wordRegex, lineNumber, lineText, from, to) {
if (typeof matchedLineNumber === 'number') {
return true;
}
wordRegex.lastIndex = from;
const result = wordRegex.exec(lineText);
if (!result || result.index + textToFind.length > to) {
return false;
}
matchedLineNumber = lineNumber;
matchedColumnNumber = result.index;
return true;
}
let iteratedLineNumber;
function lineIterator(regex, lineHandle) {
if (findWordInLine(regex, iteratedLineNumber++, lineHandle.text, 0, lineHandle.text.length)) {
return true;
}
}
let regexSource = textToFind.escapeForRegExp();
if (fullWord) {
regexSource = '\\b' + regexSource + '\\b';
}
const wordRegex = new RegExp(regexSource, 'g');
const currentLineText = this._codeMirror.getLine(range.startLine);
findWordInLine(wordRegex, range.startLine, currentLineText, range.endColumn, currentLineText.length);
iteratedLineNumber = range.startLine + 1;
this._codeMirror.eachLine(range.startLine + 1, this._codeMirror.lineCount(), lineIterator.bind(null, wordRegex));
iteratedLineNumber = 0;
this._codeMirror.eachLine(0, range.startLine, lineIterator.bind(null, wordRegex));
findWordInLine(wordRegex, range.startLine, currentLineText, 0, range.startColumn);
if (typeof matchedLineNumber !== 'number') {
return null;
}
return new TextUtils.TextRange(
matchedLineNumber, matchedColumnNumber, matchedLineNumber, matchedColumnNumber + textToFind.length);
}
}
/**
* @interface
*/
export class TextEditorPositionHandle {
/**
* @return {?{lineNumber: number, columnNumber: number}}
*/
resolve() {
}
/**
* @param {!TextEditorPositionHandle} positionHandle
* @return {boolean}
*/
equal(positionHandle) {}
}
/** @type {!Set<!Root.Runtime.Extension>} */
export const _loadedMimeModeExtensions = new Set();
/**
* @interface
*/
export class CodeMirrorMimeMode {
/**
* @param {!Root.Runtime.Extension} extension
*/
async install(extension) {
}
}
/**
* @unrestricted
*/
export class TextEditorBookMark {
/**
* @param {!CodeMirror.TextMarker} marker
* @param {symbol} type
* @param {!CodeMirrorTextEditor} editor
*/
constructor(marker, type, editor) {
marker[TextEditorBookMark._symbol] = this;
this._marker = marker;
this._type = type;
this._editor = editor;
}
clear() {
const position = this._marker.find();
this._marker.clear();
if (position) {
this._editor._updateDecorations(position.line);
}
}
refresh() {
this._marker.changed();
const position = this._marker.find();
if (position) {
this._editor._updateDecorations(position.line);
}
}
/**
* @return {symbol}
*/
type() {
return this._type;
}
/**
* @return {?TextUtils.TextRange}
*/
position() {
const pos = this._marker.find();
return pos ? TextUtils.TextRange.createFromLocation(pos.line, pos.ch) : null;
}
}
TextEditorBookMark._symbol = Symbol('TextEditorBookMark');
/**
* @implements {UI.TextEditorFactory}
* @unrestricted
*/
export class CodeMirrorTextEditorFactory {
/**
* @override
* @param {!UI.TextEditor.Options} options
* @return {!CodeMirrorTextEditor}
*/
createEditor(options) {
return new CodeMirrorTextEditor(options);
}
}
// CodeMirror uses an offscreen <textarea> to detect input. Due to inconsistencies in the many browsers it supports,
// it simplifies things by regularly checking if something is in the textarea, adding those characters to the document,
// and then clearing the textarea. This breaks assistive technology that wants to read from CodeMirror, because the
// <textarea> that they interact with is constantly empty.
// Because we target up-to-date Chrome, we can guarantee consistent input events. This lets us leave the current
// line from the editor in our <textarea>. CodeMirror still expects a mostly empty <textarea>, so we pass CodeMirror a
// fake <textarea> that only contains the users input.
CodeMirror.inputStyles.devToolsAccessibleTextArea = class extends CodeMirror.inputStyles.textarea {
/**
* @override
* @param {!Object} display
*/
init(display) {
super.init(display);
UI.ARIAUtils.setAccessibleName(this.textarea, this.cm.options.devtoolsAccessibleName || ls`Code editor`);
this.textarea.addEventListener('compositionstart', this._onCompositionStart.bind(this));
}
_onCompositionStart() {
if (this.textarea.selectionEnd === this.textarea.value.length) {
return;
}
// CodeMirror always expects the caret to be at the end of the textarea
// When in IME composition mode, clip the textarea to how CodeMirror expects it,
// and then let CodeMirror do it's thing.
this.textarea.value = this.textarea.value.substring(0, this.textarea.selectionEnd);
this.textarea.setSelectionRange(this.textarea.value.length, this.textarea.value.length);
this.prevInput = this.textarea.value;
}
/**
* @override
* @param {boolean=} typing
*/
reset(typing) {
if (typing || this.contextMenuPending || this.composing || this.cm.somethingSelected()) {
super.reset(typing);
return;
}
// When navigating around the document, keep the current visual line in the textarea.
const cursor = this.cm.getCursor();
let start, end;
if (this.cm.options.lineWrapping) {
// To get the visual line, compute the leftmost and rightmost character positions.
const top = this.cm.charCoords(cursor, 'page').top;
start = this.cm.coordsChar({left: -Infinity, top});
end = this.cm.coordsChar({left: Infinity, top});
} else {
// Limit the line to 1000 characters to prevent lag.
const offset = Math.floor(cursor.ch / 1000) * 1000;
start = {ch: offset, line: cursor.line};
end = {ch: offset + 1000, line: cursor.line};
}
this.textarea.value = this.cm.getRange(start, end);
const caretPosition = cursor.ch - start.ch;
this.textarea.setSelectionRange(caretPosition, caretPosition);
this.prevInput = this.textarea.value;
}
/**
* @override
* @return {boolean}
*/
poll() {
if (this.contextMenuPending || this.composing) {
return super.poll();
}
const text = this.textarea.value;
let start = 0;
const length = Math.min(this.prevInput.length, text.length);
while (start < length && this.prevInput[start] === text[start]) {
++start;
}
let end = 0;
while (end < length - start && this.prevInput[this.prevInput.length - end - 1] === text[text.length - end - 1]) {
++end;
}
// CodeMirror expects the user to be typing into a blank <textarea>.
// Pass a fake textarea into super.poll that only contains the users input.
/** @type {!HTMLTextAreaElement} */
const placeholder = this.textarea;
this.textarea = /** @type {!HTMLTextAreaElement} */ (createElement('textarea'));
this.textarea.value = text.substring(start, text.length - end);
this.textarea.setSelectionRange(placeholder.selectionStart - start, placeholder.selectionEnd - start);
this.prevInput = '';
const result = super.poll();
this.prevInput = text;
this.textarea = placeholder;
return result;
}
};
/* Legacy exported object */
self.TextEditor = self.TextEditor || {};
/* Legacy exported object */
TextEditor = TextEditor || {};
/** @constructor */
TextEditor.CodeMirrorTextEditor = CodeMirrorTextEditor;
/** @constructor */
TextEditor.CodeMirrorTextEditor.SelectNextOccurrenceController = SelectNextOccurrenceController;
/** @interface */
TextEditor.TextEditorPositionHandle = TextEditorPositionHandle;
TextEditor.CodeMirrorTextEditor._loadedMimeModeExtensions = _loadedMimeModeExtensions;
/** @constructor */
TextEditor.CodeMirrorPositionHandle = CodeMirrorPositionHandle;
/** @interface */
TextEditor.CodeMirrorMimeMode = CodeMirrorMimeMode;
/** @constructor */
TextEditor.TextEditorBookMark = TextEditorBookMark;
/** @constructor */
TextEditor.CodeMirrorTextEditorFactory = CodeMirrorTextEditorFactory;
/**
* @typedef {{
* element: !Element,
* widget: !CodeMirror.LineWidget,
* update: ?function()
* }}
*/
TextEditor.CodeMirrorTextEditor.Decoration;