mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-07 12:33:47 +08:00
R=szuend@chromium.org Bug: 1011811 Change-Id: I832ef16ba413d096378f6d71f17b8962958f66dd Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2450030 Commit-Queue: Tim van der Lippe <tvanderlippe@chromium.org> Reviewed-by: Paul Lewis <aerotwist@chromium.org> Auto-Submit: Tim van der Lippe <tvanderlippe@chromium.org>
1982 lines
59 KiB
JavaScript
1982 lines
59 KiB
JavaScript
/*
|
|
* 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.
|
|
*/
|
|
// @ts-nocheck
|
|
// TODO(crbug.com/1011811): Enable TypeScript compiler checks
|
|
|
|
import * as Common from '../common/common.js';
|
|
import * as Host from '../host/host.js';
|
|
import * as Platform from '../platform/platform.js';
|
|
import * as Root from '../root/root.js';
|
|
import * as TextUtils from '../text_utils/text_utils.js';
|
|
import * as UI from '../ui/ui.js';
|
|
|
|
import {changeObjectToEditOperation, toPos, toRange} from './CodeMirrorUtils.js';
|
|
import {TextEditorAutocompleteController} from './TextEditorAutocompleteController.js';
|
|
|
|
/**
|
|
* @implements {UI.TextEditor.TextEditor}
|
|
* @unrestricted
|
|
*/
|
|
export class CodeMirrorTextEditor extends UI.Widget.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.Settings.Settings.instance().moduleSetting('textEditorIndent').get());
|
|
|
|
this._codeMirror = new CodeMirror(this.element, {
|
|
screenReaderLabel: options.devtoolsAccessibleName || ls`Code editor`,
|
|
lineNumbers: options.lineNumbers,
|
|
matchBrackets: true,
|
|
smartIndent: true,
|
|
styleSelectedText: true,
|
|
electricChars: true,
|
|
styleActiveLine: true,
|
|
indentUnit,
|
|
indentWithTabs,
|
|
lineWrapping: options.lineWrapping,
|
|
lineWiseCopyCut: options.lineWiseCopyCut || false,
|
|
tabIndex: 0,
|
|
pollInterval: Math.pow(2, 31) - 1, // ~25 days
|
|
inputStyle: options.inputStyle || 'devToolsAccessibleTextArea'
|
|
});
|
|
this._codeMirrorElement = this.element.lastElementChild;
|
|
|
|
this._codeMirror._codeMirrorTextEditor = this;
|
|
|
|
Common.Settings.Settings.instance()
|
|
.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.Platform.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, !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.classList.add('CodeMirror-line-like');
|
|
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 = Root.Runtime.Runtime.instance().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.Platform.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);
|
|
}
|
|
|
|
/**
|
|
* @return {!CodeMirror}
|
|
*/
|
|
codeMirror() {
|
|
return this._codeMirror;
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @return {!UI.Widget.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.classList.add('CodeMirror-line-like');
|
|
}
|
|
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 = Platform.NumberUtilities.clamp(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.Utils.isWordChar(text[position]) &&
|
|
TextUtils.TextUtils.Utils.isWordChar(text[nextPosition]) &&
|
|
TextUtils.TextUtils.Utils.isUpperCase(text[position]) &&
|
|
TextUtils.TextUtils.Utils.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.Utils.isWordChar(text[position]) &&
|
|
TextUtils.TextUtils.Utils.isWordChar(text[prevPosition]) &&
|
|
TextUtils.TextUtils.Utils.isUpperCase(text[position]) &&
|
|
TextUtils.TextUtils.Utils.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: Platform.NumberUtilities.clamp(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.Utils.isSpaceChar(text[charNumber])) {
|
|
charNumber += direction;
|
|
}
|
|
if (!valid(charNumber, length)) {
|
|
return constrainPosition(lineNumber, length, charNumber);
|
|
}
|
|
|
|
if (TextUtils.TextUtils.Utils.isStopChar(text[charNumber])) {
|
|
while (valid(charNumber, length) && TextUtils.TextUtils.Utils.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.Utils.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.Settings.Settings.instance().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.TextEditor.AutocompleteConfig} config
|
|
*/
|
|
configureAutocomplete(config) {
|
|
if (this._autocompleteController) {
|
|
this._autocompleteController.dispose();
|
|
delete this._autocompleteController;
|
|
}
|
|
|
|
if (config) {
|
|
this._autocompleteController = new 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.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 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.TextRange} range
|
|
* @param {symbol=} type
|
|
* @return {!Array.<!TextEditorBookMark>}
|
|
*/
|
|
bookmarks(range, type) {
|
|
const pos = 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 {!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 {!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 = Platform.NumberUtilities.clamp(lineNumber, 0, this._codeMirror.lineCount() - 1);
|
|
if (typeof columnNumber !== 'number') {
|
|
columnNumber = 0;
|
|
}
|
|
columnNumber = Platform.NumberUtilities.clamp(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.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) {
|
|
let newPaddingBottom = 0;
|
|
const linesElement = this._codeMirrorElement.getElementsByClassName('CodeMirror-lines')[0];
|
|
|
|
if (this._options.padBottom) {
|
|
const scrollInfo = this._codeMirror.getScrollInfo();
|
|
const lineCount = this._codeMirror.lineCount();
|
|
if (lineCount > 1) {
|
|
newPaddingBottom =
|
|
Math.max(scrollInfo.clientHeight - this._codeMirror.getLineHandle(this._codeMirror.lastLine()).height, 0);
|
|
}
|
|
}
|
|
|
|
newPaddingBottom += 'px';
|
|
if (linesElement.style.paddingBottom !== newPaddingBottom) {
|
|
linesElement.style.paddingBottom = newPaddingBottom;
|
|
this._codeMirror.setSize(width, height);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {boolean} enableScrolling
|
|
*/
|
|
toggleScrollPastEof(enableScrolling) {
|
|
if (this._options.padBottom === enableScrolling) {
|
|
return;
|
|
}
|
|
|
|
this._options.padBottom = enableScrolling;
|
|
this._resizeEditor();
|
|
}
|
|
|
|
_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.TextRange} range
|
|
* @param {string} text
|
|
* @param {string=} origin
|
|
* @return {!TextUtils.TextRange.TextRange}
|
|
*/
|
|
editRange(range, text, origin) {
|
|
const pos = toPos(range);
|
|
this._codeMirror.replaceRange(text, pos.start, pos.end, origin);
|
|
const newRange =
|
|
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.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.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 = 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.TextRange}
|
|
*/
|
|
selection() {
|
|
const start = this._codeMirror.getCursor('anchor');
|
|
const end = this._codeMirror.getCursor('head');
|
|
|
|
return toRange(start, end);
|
|
}
|
|
|
|
/**
|
|
* @return {!Array.<!TextUtils.TextRange.TextRange>}
|
|
*/
|
|
selections() {
|
|
const selectionList = this._codeMirror.listSelections();
|
|
const result = [];
|
|
for (let i = 0; i < selectionList.length; ++i) {
|
|
const selection = selectionList[i];
|
|
result.push(toRange(selection.anchor, selection.head));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* @return {?TextUtils.TextRange.TextRange}
|
|
*/
|
|
lastSelection() {
|
|
return this._lastSelection;
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @param {!TextUtils.TextRange.TextRange} textRange
|
|
* @param {boolean=} dontScroll
|
|
*/
|
|
setSelection(textRange, dontScroll) {
|
|
this._lastSelection = textRange;
|
|
if (!this._editorSizeInSync) {
|
|
this._selectionSetScheduled = true;
|
|
return;
|
|
}
|
|
const pos = toPos(textRange);
|
|
this._codeMirror.setSelection(pos.start, pos.end, {scroll: !dontScroll});
|
|
}
|
|
|
|
/**
|
|
* @param {!Array.<!TextUtils.TextRange.TextRange>} ranges
|
|
* @param {number=} primarySelectionIndex
|
|
*/
|
|
setSelections(ranges, primarySelectionIndex) {
|
|
const selections = [];
|
|
for (let i = 0; i < ranges.length; ++i) {
|
|
const selection = 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=} textRange
|
|
* @return {string}
|
|
*/
|
|
text(textRange) {
|
|
if (!textRange) {
|
|
return this._codeMirror.getValue(this._lineSeparator);
|
|
}
|
|
const pos = 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.TextRange}
|
|
*/
|
|
fullRange() {
|
|
const lineCount = this.linesCount;
|
|
const lastLine = this._codeMirror.getLine(lineCount - 1);
|
|
return 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.Settings.Settings.instance().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 = 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 (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.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.TextRange>} selections
|
|
* @param {!TextUtils.TextRange.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.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.Utils.isWordChar) ||
|
|
TextUtils.TextRange.TextRange.createFromLocation(selection.startLine, selection.startColumn);
|
|
const endRangeWord = this._textEditor.wordRangeForCursorPosition(
|
|
selection.endLine, selection.endColumn, TextUtils.TextUtils.Utils.isWordChar) ||
|
|
TextUtils.TextRange.TextRange.createFromLocation(selection.endLine, selection.endColumn);
|
|
const newSelection = new TextUtils.TextRange.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.TextRange} range
|
|
* @param {boolean} fullWord
|
|
* @return {?TextUtils.TextRange.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.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.TextRange}
|
|
*/
|
|
position() {
|
|
const pos = this._marker.find();
|
|
return pos ? TextUtils.TextRange.TextRange.createFromLocation(pos.line, pos.ch) : null;
|
|
}
|
|
}
|
|
|
|
TextEditorBookMark._symbol = Symbol('TextEditorBookMark');
|
|
|
|
/**
|
|
* @implements {UI.TextEditor.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);
|
|
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 - whether the user is currently typing
|
|
*/
|
|
reset(typing) {
|
|
if (this.textAreaBusy(!!typing)) {
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* If the user is currently typing into the textarea or otherwise
|
|
* modifying it, we don't want to clobber their work.
|
|
*
|
|
* @protected
|
|
* @param {boolean} typing - whether the user is currently typing
|
|
* @return {boolean}
|
|
*/
|
|
textAreaBusy(typing) {
|
|
return typing || this.contextMenuPending || this.composing || this.cm.somethingSelected();
|
|
}
|
|
|
|
/**
|
|
* @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;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @typedef {{
|
|
* element: !Element,
|
|
* widget: !CodeMirror.LineWidget,
|
|
* update: ?function()
|
|
* }}
|
|
*/
|
|
export let Decoration;
|