mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-03 02:21:05 +08:00
This CL makes several improvements to the Infobar component. Most notably, action(s) can now be taken directly from the Infobar itself. Clients of the Infobar component can pass any number of action buttons during construction depending on the context. Infobar has three uses presently in DevTools; two of those use cases are actionable and have been upgraded with this new experience: - "Pretty print this minified file?" now has a "Pretty-print" button where users can directly pretty-print the file. - "This script is blackboxed in the debugger" now has an "Unblackbox" button, plus a "Configure" button that opens Blackboxing settings. The layout has been updated to reflow responsively for narrow window widths and high zoom. Accessibility considerations include: - Keyboard navigation: Infobar itself can now gain focus and be dismissed by pressing the Esc key. Pressing the Enter key will open the Learn More section. - Screen reader support: Infobar contents are read aloud in a natural and predictable fashion. The ARIA "alert" role is used on the Infobar text content. - High contrast support: Infobar text and buttons are visible in high contrast view. Note, the icons are not high-contrast compliant; HC icon updates throughout DevTools is a future task. Demo: https://i.imgur.com/kNjI4pA.gif Bug: 1056348 Change-Id: Id2e695af367fb8f5a1c489fa8d3d94c8974458fb Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/1981175 Commit-Queue: Brian Cui <brcui@microsoft.com> Reviewed-by: Fabio Rocha <fabio.rocha@microsoft.com>
1008 lines
32 KiB
JavaScript
1008 lines
32 KiB
JavaScript
// Copyright (c) 2016 The Chromium Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
import * as TextEditor from '../text_editor/text_editor.js';
|
|
import * as TextUtils from '../text_utils/text_utils.js';
|
|
import * as UI from '../ui/ui.js';
|
|
|
|
/**
|
|
* @unrestricted
|
|
*/
|
|
export class SourcesTextEditor extends TextEditor.CodeMirrorTextEditor.CodeMirrorTextEditor {
|
|
/**
|
|
* @param {!SourcesTextEditorDelegate} delegate
|
|
* @param {!UI.TextEditor.Options=} codeMirrorOptions
|
|
*/
|
|
constructor(delegate, codeMirrorOptions) {
|
|
const defaultCodeMirrorOptions = {
|
|
lineNumbers: true,
|
|
lineWrapping: false,
|
|
bracketMatchingSetting: self.Common.settings.moduleSetting('textEditorBracketMatching'),
|
|
padBottom: self.Common.settings.moduleSetting('allowScrollPastEof').get()
|
|
};
|
|
if (codeMirrorOptions) {
|
|
Object.assign(defaultCodeMirrorOptions, codeMirrorOptions);
|
|
}
|
|
|
|
super(defaultCodeMirrorOptions);
|
|
|
|
this.codeMirror().addKeyMap({'Enter': 'smartNewlineAndIndent', 'Esc': 'sourcesDismiss'});
|
|
|
|
this._delegate = delegate;
|
|
|
|
this.codeMirror().on('cursorActivity', this._cursorActivity.bind(this));
|
|
this.codeMirror().on('gutterClick', this._gutterClick.bind(this));
|
|
this.codeMirror().on('scroll', this._scroll.bind(this));
|
|
this.codeMirror().on('focus', this._focus.bind(this));
|
|
this.codeMirror().on('blur', this._blur.bind(this));
|
|
this.codeMirror().on('beforeSelectionChange', this._fireBeforeSelectionChanged.bind(this));
|
|
this.element.addEventListener('contextmenu', this._contextMenu.bind(this), false);
|
|
|
|
this._gutterMouseMove = event => {
|
|
this.element.classList.toggle(
|
|
'CodeMirror-gutter-hovered',
|
|
event.clientX < this.codeMirror().getGutterElement().getBoundingClientRect().right);
|
|
};
|
|
this._gutterMouseOut = event => {
|
|
this.element.classList.toggle('CodeMirror-gutter-hovered', false);
|
|
};
|
|
|
|
this.codeMirror().addKeyMap(_BlockIndentController);
|
|
this._tokenHighlighter = new TokenHighlighter(this, this.codeMirror());
|
|
|
|
/** @type {!Array<string>} */
|
|
this._gutters = [lineNumbersGutterType];
|
|
this.codeMirror().setOption('gutters', this._gutters.slice());
|
|
|
|
this.codeMirror().setOption('electricChars', false);
|
|
this.codeMirror().setOption('smartIndent', false);
|
|
|
|
/**
|
|
* @this {SourcesTextEditor}
|
|
*/
|
|
function updateAnticipateJumpFlag(value) {
|
|
this._isHandlingMouseDownEvent = value;
|
|
}
|
|
|
|
this.element.addEventListener('mousedown', updateAnticipateJumpFlag.bind(this, true), true);
|
|
this.element.addEventListener('mousedown', updateAnticipateJumpFlag.bind(this, false), false);
|
|
self.Common.settings.moduleSetting('textEditorIndent').addChangeListener(this._onUpdateEditorIndentation, this);
|
|
self.Common.settings.moduleSetting('textEditorAutoDetectIndent')
|
|
.addChangeListener(this._onUpdateEditorIndentation, this);
|
|
self.Common.settings.moduleSetting('showWhitespacesInEditor').addChangeListener(this._updateWhitespace, this);
|
|
self.Common.settings.moduleSetting('textEditorCodeFolding').addChangeListener(this._updateCodeFolding, this);
|
|
self.Common.settings.moduleSetting('allowScrollPastEof').addChangeListener(this._updateScrollPastEof, this);
|
|
this._updateCodeFolding();
|
|
|
|
/** @type {?UI.TextEditor.AutocompleteConfig} */
|
|
this._autocompleteConfig = {isWordChar: TextUtils.TextUtils.Utils.isWordChar};
|
|
self.Common.settings.moduleSetting('textEditorAutocompletion').addChangeListener(this._updateAutocomplete, this);
|
|
this._updateAutocomplete();
|
|
|
|
this._onUpdateEditorIndentation();
|
|
this._setupWhitespaceHighlight();
|
|
|
|
/** @type {?Element} */
|
|
this._infoBarDiv = null;
|
|
}
|
|
|
|
/**
|
|
* @param {!UI.Infobar.Infobar} infobar
|
|
*/
|
|
attachInfobar(infobar) {
|
|
if (!this._infoBarDiv) {
|
|
this._infoBarDiv = createElementWithClass('div', 'flex-none');
|
|
this.element.insertBefore(this._infoBarDiv, this.element.firstChild);
|
|
}
|
|
this._infoBarDiv.appendChild(infobar.element);
|
|
infobar.setParentView(this);
|
|
this.doResize();
|
|
}
|
|
|
|
/**
|
|
* @param {!Array.<string>} lines
|
|
* @return {string}
|
|
*/
|
|
static _guessIndentationLevel(lines) {
|
|
const tabRegex = /^\t+/;
|
|
let tabLines = 0;
|
|
const indents = {};
|
|
for (let lineNumber = 0; lineNumber < lines.length; ++lineNumber) {
|
|
const text = lines[lineNumber];
|
|
if (text.length === 0 || !TextUtils.TextUtils.Utils.isSpaceChar(text[0])) {
|
|
continue;
|
|
}
|
|
if (tabRegex.test(text)) {
|
|
++tabLines;
|
|
continue;
|
|
}
|
|
let i = 0;
|
|
while (i < text.length && TextUtils.TextUtils.Utils.isSpaceChar(text[i])) {
|
|
++i;
|
|
}
|
|
if (i % 2 !== 0) {
|
|
continue;
|
|
}
|
|
indents[i] = 1 + (indents[i] || 0);
|
|
}
|
|
const linesCountPerIndentThreshold = 3 * lines.length / 100;
|
|
if (tabLines && tabLines > linesCountPerIndentThreshold) {
|
|
return '\t';
|
|
}
|
|
let minimumIndent = Infinity;
|
|
for (const i in indents) {
|
|
if (indents[i] < linesCountPerIndentThreshold) {
|
|
continue;
|
|
}
|
|
const indent = parseInt(i, 10);
|
|
if (minimumIndent > indent) {
|
|
minimumIndent = indent;
|
|
}
|
|
}
|
|
if (minimumIndent === Infinity) {
|
|
return self.Common.settings.moduleSetting('textEditorIndent').get();
|
|
}
|
|
return ' '.repeat(minimumIndent);
|
|
}
|
|
|
|
/**
|
|
* @return {boolean}
|
|
*/
|
|
_isSearchActive() {
|
|
return !!this._tokenHighlighter.highlightedRegex();
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @param {number} lineNumber
|
|
*/
|
|
scrollToLine(lineNumber) {
|
|
super.scrollToLine(lineNumber);
|
|
this._scroll();
|
|
}
|
|
|
|
/**
|
|
* @param {!RegExp} regex
|
|
* @param {?TextUtils.TextRange.TextRange} range
|
|
*/
|
|
highlightSearchResults(regex, range) {
|
|
/**
|
|
* @this {TextEditor.CodeMirrorTextEditor.CodeMirrorTextEditor}
|
|
*/
|
|
function innerHighlightRegex() {
|
|
if (range) {
|
|
this.scrollLineIntoView(range.startLine);
|
|
if (range.endColumn > TextEditor.CodeMirrorTextEditor.CodeMirrorTextEditor.maxHighlightLength) {
|
|
this.setSelection(range);
|
|
} else {
|
|
this.setSelection(TextUtils.TextRange.TextRange.createFromLocation(range.startLine, range.startColumn));
|
|
}
|
|
}
|
|
this._tokenHighlighter.highlightSearchResults(regex, range);
|
|
}
|
|
|
|
if (!this._selectionBeforeSearch) {
|
|
this._selectionBeforeSearch = this.selection();
|
|
}
|
|
|
|
this.codeMirror().operation(innerHighlightRegex.bind(this));
|
|
}
|
|
|
|
cancelSearchResultsHighlight() {
|
|
this.codeMirror().operation(this._tokenHighlighter.highlightSelectedTokens.bind(this._tokenHighlighter));
|
|
|
|
if (this._selectionBeforeSearch) {
|
|
this._reportJump(this._selectionBeforeSearch, this.selection());
|
|
delete this._selectionBeforeSearch;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {!Object} highlightDescriptor
|
|
*/
|
|
removeHighlight(highlightDescriptor) {
|
|
highlightDescriptor.clear();
|
|
}
|
|
|
|
/**
|
|
* @param {!TextUtils.TextRange.TextRange} range
|
|
* @param {string} cssClass
|
|
* @return {!Object}
|
|
*/
|
|
highlightRange(range, cssClass) {
|
|
cssClass = 'CodeMirror-persist-highlight ' + cssClass;
|
|
const pos = TextEditor.CodeMirrorUtils.toPos(range);
|
|
++pos.end.ch;
|
|
return this.codeMirror().markText(
|
|
pos.start, pos.end, {className: cssClass, startStyle: cssClass + '-start', endStyle: cssClass + '-end'});
|
|
}
|
|
|
|
/**
|
|
* @param {string} type
|
|
* @param {boolean} leftToNumbers
|
|
*/
|
|
installGutter(type, leftToNumbers) {
|
|
if (this._gutters.indexOf(type) !== -1) {
|
|
return;
|
|
}
|
|
|
|
if (leftToNumbers) {
|
|
this._gutters.unshift(type);
|
|
} else {
|
|
this._gutters.push(type);
|
|
}
|
|
|
|
this.codeMirror().setOption('gutters', this._gutters.slice());
|
|
this.refresh();
|
|
}
|
|
|
|
/**
|
|
* @param {string} type
|
|
*/
|
|
uninstallGutter(type) {
|
|
const index = this._gutters.indexOf(type);
|
|
if (index === -1) {
|
|
return;
|
|
}
|
|
this.codeMirror().clearGutter(type);
|
|
this._gutters.splice(index, 1);
|
|
this.codeMirror().setOption('gutters', this._gutters.slice());
|
|
this.refresh();
|
|
}
|
|
|
|
/**
|
|
* @param {number} lineNumber
|
|
* @param {string} type
|
|
* @param {?Element} element
|
|
*/
|
|
setGutterDecoration(lineNumber, type, element) {
|
|
console.assert(this._gutters.indexOf(type) !== -1, 'Cannot decorate unexisting gutter.');
|
|
this.codeMirror().setGutterMarker(lineNumber, type, element);
|
|
}
|
|
|
|
/**
|
|
* @param {number} lineNumber
|
|
* @param {number} columnNumber
|
|
*/
|
|
setExecutionLocation(lineNumber, columnNumber) {
|
|
this.clearPositionHighlight();
|
|
|
|
this._executionLine = this.codeMirror().getLineHandle(lineNumber);
|
|
if (!this._executionLine) {
|
|
return;
|
|
}
|
|
|
|
this.showExecutionLineBackground();
|
|
this.codeMirror().addLineClass(this._executionLine, 'wrap', 'cm-execution-line-outline');
|
|
let token = this.tokenAtTextPosition(lineNumber, columnNumber);
|
|
|
|
if (token && !token.type && token.startColumn + 1 === token.endColumn) {
|
|
const tokenContent = this.codeMirror().getLine(lineNumber)[token.startColumn];
|
|
if (tokenContent === '.' || tokenContent === '(') {
|
|
token = this.tokenAtTextPosition(lineNumber, token.endColumn + 1);
|
|
}
|
|
}
|
|
|
|
let endColumn;
|
|
if (token && token.type) {
|
|
endColumn = token.endColumn;
|
|
} else {
|
|
endColumn = this.codeMirror().getLine(lineNumber).length;
|
|
}
|
|
|
|
this._executionLineTailMarker = this.codeMirror().markText(
|
|
{line: lineNumber, ch: columnNumber}, {line: lineNumber, ch: endColumn}, {className: 'cm-execution-line-tail'});
|
|
}
|
|
|
|
showExecutionLineBackground() {
|
|
if (this._executionLine) {
|
|
this.codeMirror().addLineClass(this._executionLine, 'wrap', 'cm-execution-line');
|
|
}
|
|
}
|
|
|
|
hideExecutionLineBackground() {
|
|
if (this._executionLine) {
|
|
this.codeMirror().removeLineClass(this._executionLine, 'wrap', 'cm-execution-line');
|
|
}
|
|
}
|
|
|
|
clearExecutionLine() {
|
|
this.clearPositionHighlight();
|
|
|
|
if (this._executionLine) {
|
|
this.hideExecutionLineBackground();
|
|
this.codeMirror().removeLineClass(this._executionLine, 'wrap', 'cm-execution-line-outline');
|
|
}
|
|
delete this._executionLine;
|
|
|
|
if (this._executionLineTailMarker) {
|
|
this._executionLineTailMarker.clear();
|
|
}
|
|
delete this._executionLineTailMarker;
|
|
}
|
|
|
|
/**
|
|
* @param {number} lineNumber
|
|
* @param {string} className
|
|
* @param {boolean} toggled
|
|
*/
|
|
toggleLineClass(lineNumber, className, toggled) {
|
|
if (this.hasLineClass(lineNumber, className) === toggled) {
|
|
return;
|
|
}
|
|
|
|
const lineHandle = this.codeMirror().getLineHandle(lineNumber);
|
|
if (!lineHandle) {
|
|
return;
|
|
}
|
|
|
|
if (toggled) {
|
|
this.codeMirror().addLineClass(lineHandle, 'gutter', className);
|
|
this.codeMirror().addLineClass(lineHandle, 'wrap', className);
|
|
} else {
|
|
this.codeMirror().removeLineClass(lineHandle, 'gutter', className);
|
|
this.codeMirror().removeLineClass(lineHandle, 'wrap', className);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {number} lineNumber
|
|
* @param {string} className
|
|
* @return {boolean}
|
|
*/
|
|
hasLineClass(lineNumber, className) {
|
|
const lineInfo = this.codeMirror().lineInfo(lineNumber);
|
|
const wrapClass = lineInfo.wrapClass || '';
|
|
const classNames = wrapClass.split(' ');
|
|
return classNames.indexOf(className) !== -1;
|
|
}
|
|
|
|
_gutterClick(instance, lineNumber, gutterType, event) {
|
|
this.dispatchEventToListeners(Events.GutterClick, {gutterType, lineNumber, event});
|
|
}
|
|
|
|
_contextMenu(event) {
|
|
const contextMenu = new UI.ContextMenu.ContextMenu(event);
|
|
event.consume(true); // Consume event now to prevent document from handling the async menu
|
|
const wrapper = event.target.enclosingNodeOrSelfWithClass('CodeMirror-gutter-wrapper');
|
|
const target = wrapper ? wrapper.querySelector('.CodeMirror-linenumber') : null;
|
|
let promise;
|
|
if (target) {
|
|
promise = this._delegate.populateLineGutterContextMenu(contextMenu, parseInt(target.textContent, 10) - 1);
|
|
} else {
|
|
const textSelection = this.selection();
|
|
promise =
|
|
this._delegate.populateTextAreaContextMenu(contextMenu, textSelection.startLine, textSelection.startColumn);
|
|
}
|
|
promise.then(showAsync.bind(this));
|
|
|
|
/**
|
|
* @this {SourcesTextEditor}
|
|
*/
|
|
function showAsync() {
|
|
contextMenu.appendApplicableItems(this);
|
|
contextMenu.show();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @param {!TextUtils.TextRange.TextRange} range
|
|
* @param {string} text
|
|
* @param {string=} origin
|
|
* @return {!TextUtils.TextRange.TextRange}
|
|
*/
|
|
editRange(range, text, origin) {
|
|
const newRange = super.editRange(range, text, origin);
|
|
if (self.Common.settings.moduleSetting('textEditorAutoDetectIndent').get()) {
|
|
this._onUpdateEditorIndentation();
|
|
}
|
|
|
|
return newRange;
|
|
}
|
|
|
|
_onUpdateEditorIndentation() {
|
|
this._setEditorIndentation(
|
|
TextEditor.CodeMirrorUtils.pullLines(this.codeMirror(), LinesToScanForIndentationGuessing));
|
|
}
|
|
|
|
/**
|
|
* @param {!Array.<string>} lines
|
|
*/
|
|
_setEditorIndentation(lines) {
|
|
const extraKeys = {};
|
|
let indent = self.Common.settings.moduleSetting('textEditorIndent').get();
|
|
if (self.Common.settings.moduleSetting('textEditorAutoDetectIndent').get()) {
|
|
indent = SourcesTextEditor._guessIndentationLevel(lines);
|
|
}
|
|
|
|
if (indent === TextUtils.TextUtils.Utils.Indent.TabCharacter) {
|
|
this.codeMirror().setOption('indentWithTabs', true);
|
|
this.codeMirror().setOption('indentUnit', 4);
|
|
} else {
|
|
this.codeMirror().setOption('indentWithTabs', false);
|
|
this.codeMirror().setOption('indentUnit', indent.length);
|
|
extraKeys.Tab = function(codeMirror) {
|
|
if (codeMirror.somethingSelected()) {
|
|
return CodeMirror.Pass;
|
|
}
|
|
const pos = codeMirror.getCursor('head');
|
|
codeMirror.replaceRange(indent.substring(pos.ch % indent.length), codeMirror.getCursor());
|
|
};
|
|
}
|
|
|
|
this.codeMirror().setOption('extraKeys', extraKeys);
|
|
this._indentationLevel = indent;
|
|
}
|
|
|
|
/**
|
|
* @return {string}
|
|
*/
|
|
indent() {
|
|
return this._indentationLevel;
|
|
}
|
|
|
|
_onAutoAppendedSpaces() {
|
|
this._autoAppendedSpaces = this._autoAppendedSpaces || [];
|
|
|
|
for (let i = 0; i < this._autoAppendedSpaces.length; ++i) {
|
|
const position = this._autoAppendedSpaces[i].resolve();
|
|
if (!position) {
|
|
continue;
|
|
}
|
|
const line = this.line(position.lineNumber);
|
|
if (line.length === position.columnNumber && TextUtils.TextUtils.Utils.lineIndent(line).length === line.length) {
|
|
this.codeMirror().replaceRange(
|
|
'', new CodeMirror.Pos(position.lineNumber, 0),
|
|
new CodeMirror.Pos(position.lineNumber, position.columnNumber));
|
|
}
|
|
}
|
|
|
|
this._autoAppendedSpaces = [];
|
|
const selections = this.selections();
|
|
for (let i = 0; i < selections.length; ++i) {
|
|
const selection = selections[i];
|
|
this._autoAppendedSpaces.push(this.textEditorPositionHandle(selection.startLine, selection.startColumn));
|
|
}
|
|
}
|
|
|
|
_cursorActivity() {
|
|
if (!this._isSearchActive()) {
|
|
this.codeMirror().operation(this._tokenHighlighter.highlightSelectedTokens.bind(this._tokenHighlighter));
|
|
}
|
|
|
|
const start = this.codeMirror().getCursor('anchor');
|
|
const end = this.codeMirror().getCursor('head');
|
|
this.dispatchEventToListeners(Events.SelectionChanged, TextEditor.CodeMirrorUtils.toRange(start, end));
|
|
}
|
|
|
|
/**
|
|
* @param {?TextUtils.TextRange.TextRange} from
|
|
* @param {?TextUtils.TextRange.TextRange} to
|
|
*/
|
|
_reportJump(from, to) {
|
|
if (from && to && from.equal(to)) {
|
|
return;
|
|
}
|
|
this.dispatchEventToListeners(Events.JumpHappened, {from: from, to: to});
|
|
}
|
|
|
|
_scroll() {
|
|
const topmostLineNumber = this.codeMirror().lineAtHeight(this.codeMirror().getScrollInfo().top, 'local');
|
|
this.dispatchEventToListeners(Events.ScrollChanged, topmostLineNumber);
|
|
}
|
|
|
|
_focus() {
|
|
this.dispatchEventToListeners(Events.EditorFocused);
|
|
}
|
|
|
|
_blur() {
|
|
this.dispatchEventToListeners(Events.EditorBlurred);
|
|
}
|
|
|
|
/**
|
|
* @param {!CodeMirror} codeMirror
|
|
* @param {{ranges: !Array.<{head: !CodeMirror.Pos, anchor: !CodeMirror.Pos}>}} selection
|
|
*/
|
|
_fireBeforeSelectionChanged(codeMirror, selection) {
|
|
if (!this._isHandlingMouseDownEvent) {
|
|
return;
|
|
}
|
|
if (!selection.ranges.length) {
|
|
return;
|
|
}
|
|
|
|
const primarySelection = selection.ranges[0];
|
|
this._reportJump(
|
|
this.selection(), TextEditor.CodeMirrorUtils.toRange(primarySelection.anchor, primarySelection.head));
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
*/
|
|
dispose() {
|
|
super.dispose();
|
|
self.Common.settings.moduleSetting('textEditorIndent').removeChangeListener(this._onUpdateEditorIndentation, this);
|
|
self.Common.settings.moduleSetting('textEditorAutoDetectIndent')
|
|
.removeChangeListener(this._onUpdateEditorIndentation, this);
|
|
self.Common.settings.moduleSetting('showWhitespacesInEditor').removeChangeListener(this._updateWhitespace, this);
|
|
self.Common.settings.moduleSetting('textEditorCodeFolding').removeChangeListener(this._updateCodeFolding, this);
|
|
self.Common.settings.moduleSetting('allowScrollPastEof').removeChangeListener(this._updateScrollPastEof, this);
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @param {string} text
|
|
*/
|
|
setText(text) {
|
|
this._setEditorIndentation(text.split('\n').slice(0, LinesToScanForIndentationGuessing));
|
|
super.setText(text);
|
|
}
|
|
|
|
_updateWhitespace() {
|
|
this.setMimeType(this.mimeType());
|
|
}
|
|
|
|
_updateCodeFolding() {
|
|
if (self.Common.settings.moduleSetting('textEditorCodeFolding').get()) {
|
|
this.installGutter('CodeMirror-foldgutter', false);
|
|
this.element.addEventListener('mousemove', this._gutterMouseMove);
|
|
this.element.addEventListener('mouseout', this._gutterMouseOut);
|
|
this.codeMirror().setOption('foldGutter', true);
|
|
this.codeMirror().setOption('foldOptions', {minFoldSize: 1});
|
|
} else {
|
|
this.codeMirror().execCommand('unfoldAll');
|
|
this.element.removeEventListener('mousemove', this._gutterMouseMove);
|
|
this.element.removeEventListener('mouseout', this._gutterMouseOut);
|
|
this.uninstallGutter('CodeMirror-foldgutter');
|
|
this.codeMirror().setOption('foldGutter', false);
|
|
}
|
|
}
|
|
|
|
_updateScrollPastEof() {
|
|
this.toggleScrollPastEof(self.Common.settings.moduleSetting('allowScrollPastEof').get());
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @param {string} mimeType
|
|
* @return {string}
|
|
*/
|
|
rewriteMimeType(mimeType) {
|
|
this._setupWhitespaceHighlight();
|
|
const whitespaceMode = self.Common.settings.moduleSetting('showWhitespacesInEditor').get();
|
|
this.element.classList.toggle('show-whitespaces', whitespaceMode === 'all');
|
|
|
|
if (whitespaceMode === 'all') {
|
|
return this._allWhitespaceOverlayMode(mimeType);
|
|
}
|
|
if (whitespaceMode === 'trailing') {
|
|
return this._trailingWhitespaceOverlayMode(mimeType);
|
|
}
|
|
|
|
return mimeType;
|
|
}
|
|
|
|
/**
|
|
* @param {string} mimeType
|
|
* @return {string}
|
|
*/
|
|
_allWhitespaceOverlayMode(mimeType) {
|
|
let modeName = CodeMirror.mimeModes[mimeType] ?
|
|
(CodeMirror.mimeModes[mimeType].name || CodeMirror.mimeModes[mimeType]) :
|
|
CodeMirror.mimeModes['text/plain'];
|
|
modeName += '+all-whitespaces';
|
|
if (CodeMirror.modes[modeName]) {
|
|
return modeName;
|
|
}
|
|
|
|
function modeConstructor(config, parserConfig) {
|
|
function nextToken(stream) {
|
|
if (stream.peek() === ' ') {
|
|
let spaces = 0;
|
|
while (spaces < MaximumNumberOfWhitespacesPerSingleSpan && stream.peek() === ' ') {
|
|
++spaces;
|
|
stream.next();
|
|
}
|
|
return 'whitespace whitespace-' + spaces;
|
|
}
|
|
while (!stream.eol() && stream.peek() !== ' ') {
|
|
stream.next();
|
|
}
|
|
return null;
|
|
}
|
|
const whitespaceMode = {token: nextToken};
|
|
return CodeMirror.overlayMode(CodeMirror.getMode(config, mimeType), whitespaceMode, false);
|
|
}
|
|
CodeMirror.defineMode(modeName, modeConstructor);
|
|
return modeName;
|
|
}
|
|
|
|
/**
|
|
* @param {string} mimeType
|
|
* @return {string}
|
|
*/
|
|
_trailingWhitespaceOverlayMode(mimeType) {
|
|
let modeName = CodeMirror.mimeModes[mimeType] ?
|
|
(CodeMirror.mimeModes[mimeType].name || CodeMirror.mimeModes[mimeType]) :
|
|
CodeMirror.mimeModes['text/plain'];
|
|
modeName += '+trailing-whitespaces';
|
|
if (CodeMirror.modes[modeName]) {
|
|
return modeName;
|
|
}
|
|
|
|
function modeConstructor(config, parserConfig) {
|
|
function nextToken(stream) {
|
|
if (stream.match(/^\s+$/, true)) {
|
|
return true ? 'trailing-whitespace' : null;
|
|
}
|
|
do {
|
|
stream.next();
|
|
} while (!stream.eol() && stream.peek() !== ' ');
|
|
return null;
|
|
}
|
|
const whitespaceMode = {token: nextToken};
|
|
return CodeMirror.overlayMode(CodeMirror.getMode(config, mimeType), whitespaceMode, false);
|
|
}
|
|
CodeMirror.defineMode(modeName, modeConstructor);
|
|
return modeName;
|
|
}
|
|
|
|
_setupWhitespaceHighlight() {
|
|
const doc = this.element.ownerDocument;
|
|
if (doc._codeMirrorWhitespaceStyleInjected ||
|
|
!self.Common.settings.moduleSetting('showWhitespacesInEditor').get()) {
|
|
return;
|
|
}
|
|
doc._codeMirrorWhitespaceStyleInjected = true;
|
|
const classBase = '.show-whitespaces .CodeMirror .cm-whitespace-';
|
|
const spaceChar = '·';
|
|
let spaceChars = '';
|
|
let rules = '';
|
|
for (let i = 1; i <= MaximumNumberOfWhitespacesPerSingleSpan; ++i) {
|
|
spaceChars += spaceChar;
|
|
const rule = classBase + i + '::before { content: \'' + spaceChars + '\';}\n';
|
|
rules += rule;
|
|
}
|
|
const style = doc.createElement('style');
|
|
style.textContent = rules;
|
|
doc.head.appendChild(style);
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @param {?UI.TextEditor.AutocompleteConfig} config
|
|
*/
|
|
configureAutocomplete(config) {
|
|
this._autocompleteConfig = config;
|
|
this._updateAutocomplete();
|
|
}
|
|
|
|
_updateAutocomplete() {
|
|
super.configureAutocomplete(
|
|
self.Common.settings.moduleSetting('textEditorAutocompletion').get() ? this._autocompleteConfig : null);
|
|
}
|
|
}
|
|
|
|
/** @enum {symbol} */
|
|
export const Events = {
|
|
GutterClick: Symbol('GutterClick'),
|
|
SelectionChanged: Symbol('SelectionChanged'),
|
|
ScrollChanged: Symbol('ScrollChanged'),
|
|
EditorFocused: Symbol('EditorFocused'),
|
|
EditorBlurred: Symbol('EditorBlurred'),
|
|
JumpHappened: Symbol('JumpHappened')
|
|
};
|
|
|
|
/**
|
|
* @interface
|
|
*/
|
|
export class SourcesTextEditorDelegate {
|
|
/**
|
|
* @param {!UI.ContextMenu.ContextMenu} contextMenu
|
|
* @param {number} lineNumber
|
|
* @return {!Promise}
|
|
*/
|
|
populateLineGutterContextMenu(contextMenu, lineNumber) {
|
|
}
|
|
|
|
/**
|
|
* @param {!UI.ContextMenu.ContextMenu} contextMenu
|
|
* @param {number} lineNumber
|
|
* @param {number} columnNumber
|
|
* @return {!Promise}
|
|
*/
|
|
populateTextAreaContextMenu(contextMenu, lineNumber, columnNumber) {
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {!CodeMirror} codeMirror
|
|
*/
|
|
CodeMirror.commands.smartNewlineAndIndent = function(codeMirror) {
|
|
codeMirror.operation(innerSmartNewlineAndIndent.bind(null, codeMirror));
|
|
function innerSmartNewlineAndIndent(codeMirror) {
|
|
const selections = codeMirror.listSelections();
|
|
const replacements = [];
|
|
for (let i = 0; i < selections.length; ++i) {
|
|
const selection = selections[i];
|
|
const cur = CodeMirror.cmpPos(selection.head, selection.anchor) < 0 ? selection.head : selection.anchor;
|
|
const line = codeMirror.getLine(cur.line);
|
|
const indent = TextUtils.TextUtils.Utils.lineIndent(line);
|
|
replacements.push('\n' + indent.substring(0, Math.min(cur.ch, indent.length)));
|
|
}
|
|
codeMirror.replaceSelections(replacements);
|
|
codeMirror._codeMirrorTextEditor._onAutoAppendedSpaces();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @return {!Object|undefined}
|
|
*/
|
|
CodeMirror.commands.sourcesDismiss = function(codemirror) {
|
|
if (codemirror.listSelections().length === 1 && codemirror._codeMirrorTextEditor._isSearchActive()) {
|
|
return CodeMirror.Pass;
|
|
}
|
|
return CodeMirror.commands.dismiss(codemirror);
|
|
};
|
|
|
|
export const _BlockIndentController = {
|
|
name: 'blockIndentKeymap',
|
|
|
|
/**
|
|
* @return {*}
|
|
*/
|
|
Enter: function(codeMirror) {
|
|
let selections = codeMirror.listSelections();
|
|
const replacements = [];
|
|
let allSelectionsAreCollapsedBlocks = false;
|
|
for (let i = 0; i < selections.length; ++i) {
|
|
const selection = selections[i];
|
|
const start = CodeMirror.cmpPos(selection.head, selection.anchor) < 0 ? selection.head : selection.anchor;
|
|
const line = codeMirror.getLine(start.line);
|
|
const indent = TextUtils.TextUtils.Utils.lineIndent(line);
|
|
let indentToInsert = '\n' + indent + codeMirror._codeMirrorTextEditor.indent();
|
|
let isCollapsedBlock = false;
|
|
if (selection.head.ch === 0) {
|
|
return CodeMirror.Pass;
|
|
}
|
|
if (line.substr(selection.head.ch - 1, 2) === '{}') {
|
|
indentToInsert += '\n' + indent;
|
|
isCollapsedBlock = true;
|
|
} else if (line.substr(selection.head.ch - 1, 1) !== '{') {
|
|
return CodeMirror.Pass;
|
|
}
|
|
if (i > 0 && allSelectionsAreCollapsedBlocks !== isCollapsedBlock) {
|
|
return CodeMirror.Pass;
|
|
}
|
|
replacements.push(indentToInsert);
|
|
allSelectionsAreCollapsedBlocks = isCollapsedBlock;
|
|
}
|
|
codeMirror.replaceSelections(replacements);
|
|
if (!allSelectionsAreCollapsedBlocks) {
|
|
codeMirror._codeMirrorTextEditor._onAutoAppendedSpaces();
|
|
return;
|
|
}
|
|
selections = codeMirror.listSelections();
|
|
const updatedSelections = [];
|
|
for (let i = 0; i < selections.length; ++i) {
|
|
const selection = selections[i];
|
|
const line = codeMirror.getLine(selection.head.line - 1);
|
|
const position = new CodeMirror.Pos(selection.head.line - 1, line.length);
|
|
updatedSelections.push({head: position, anchor: position});
|
|
}
|
|
codeMirror.setSelections(updatedSelections);
|
|
codeMirror._codeMirrorTextEditor._onAutoAppendedSpaces();
|
|
},
|
|
|
|
/**
|
|
* @return {*}
|
|
*/
|
|
'\'}\'': function(codeMirror) {
|
|
if (codeMirror.somethingSelected()) {
|
|
return CodeMirror.Pass;
|
|
}
|
|
let selections = codeMirror.listSelections();
|
|
let replacements = [];
|
|
for (let i = 0; i < selections.length; ++i) {
|
|
const selection = selections[i];
|
|
const line = codeMirror.getLine(selection.head.line);
|
|
if (line !== TextUtils.TextUtils.Utils.lineIndent(line)) {
|
|
return CodeMirror.Pass;
|
|
}
|
|
replacements.push('}');
|
|
}
|
|
codeMirror.replaceSelections(replacements);
|
|
selections = codeMirror.listSelections();
|
|
replacements = [];
|
|
const updatedSelections = [];
|
|
for (let i = 0; i < selections.length; ++i) {
|
|
const selection = selections[i];
|
|
const matchingBracket = codeMirror.findMatchingBracket(selection.head);
|
|
if (!matchingBracket || !matchingBracket.match) {
|
|
return;
|
|
}
|
|
updatedSelections.push({head: selection.head, anchor: new CodeMirror.Pos(selection.head.line, 0)});
|
|
const line = codeMirror.getLine(matchingBracket.to.line);
|
|
const indent = TextUtils.TextUtils.Utils.lineIndent(line);
|
|
replacements.push(indent + '}');
|
|
}
|
|
codeMirror.setSelections(updatedSelections);
|
|
codeMirror.replaceSelections(replacements);
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @unrestricted
|
|
*/
|
|
export class TokenHighlighter {
|
|
/**
|
|
* @param {!SourcesTextEditor} textEditor
|
|
* @param {!CodeMirror} codeMirror
|
|
*/
|
|
constructor(textEditor, codeMirror) {
|
|
this._textEditor = textEditor;
|
|
this._codeMirror = codeMirror;
|
|
}
|
|
|
|
/**
|
|
* @param {!RegExp} regex
|
|
* @param {?TextUtils.TextRange.TextRange} range
|
|
*/
|
|
highlightSearchResults(regex, range) {
|
|
const oldRegex = this._highlightRegex;
|
|
this._highlightRegex = regex;
|
|
this._highlightRange = range;
|
|
if (this._searchResultMarker) {
|
|
this._searchResultMarker.clear();
|
|
delete this._searchResultMarker;
|
|
}
|
|
if (this._highlightDescriptor && this._highlightDescriptor.selectionStart) {
|
|
this._codeMirror.removeLineClass(this._highlightDescriptor.selectionStart.line, 'wrap', 'cm-line-with-selection');
|
|
}
|
|
const selectionStart = this._highlightRange ?
|
|
new CodeMirror.Pos(this._highlightRange.startLine, this._highlightRange.startColumn) :
|
|
null;
|
|
if (selectionStart) {
|
|
this._codeMirror.addLineClass(selectionStart.line, 'wrap', 'cm-line-with-selection');
|
|
}
|
|
if (oldRegex && this._highlightRegex.toString() === oldRegex.toString()) {
|
|
// Do not re-add overlay mode if regex did not change for better performance.
|
|
if (this._highlightDescriptor) {
|
|
this._highlightDescriptor.selectionStart = selectionStart;
|
|
}
|
|
} else {
|
|
this._removeHighlight();
|
|
this._setHighlighter(this._searchHighlighter.bind(this, this._highlightRegex), selectionStart);
|
|
}
|
|
if (this._highlightRange) {
|
|
const pos = TextEditor.CodeMirrorUtils.toPos(this._highlightRange);
|
|
this._searchResultMarker = this._codeMirror.markText(pos.start, pos.end, {className: 'cm-column-with-selection'});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return {!RegExp|undefined}
|
|
*/
|
|
highlightedRegex() {
|
|
return this._highlightRegex;
|
|
}
|
|
|
|
highlightSelectedTokens() {
|
|
delete this._highlightRegex;
|
|
delete this._highlightRange;
|
|
if (this._highlightDescriptor && this._highlightDescriptor.selectionStart) {
|
|
this._codeMirror.removeLineClass(this._highlightDescriptor.selectionStart.line, 'wrap', 'cm-line-with-selection');
|
|
}
|
|
this._removeHighlight();
|
|
const selectionStart = this._codeMirror.getCursor('start');
|
|
const selectionEnd = this._codeMirror.getCursor('end');
|
|
if (selectionStart.line !== selectionEnd.line) {
|
|
return;
|
|
}
|
|
if (selectionStart.ch === selectionEnd.ch) {
|
|
return;
|
|
}
|
|
const selections = this._codeMirror.getSelections();
|
|
if (selections.length > 1) {
|
|
return;
|
|
}
|
|
const selectedText = selections[0];
|
|
if (this._isWord(selectedText, selectionStart.line, selectionStart.ch, selectionEnd.ch)) {
|
|
if (selectionStart) {
|
|
this._codeMirror.addLineClass(selectionStart.line, 'wrap', 'cm-line-with-selection');
|
|
}
|
|
this._setHighlighter(this._tokenHighlighter.bind(this, selectedText, selectionStart), selectionStart);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string} selectedText
|
|
* @param {number} lineNumber
|
|
* @param {number} startColumn
|
|
* @param {number} endColumn
|
|
*/
|
|
_isWord(selectedText, lineNumber, startColumn, endColumn) {
|
|
const line = this._codeMirror.getLine(lineNumber);
|
|
const leftBound = startColumn === 0 || !TextUtils.TextUtils.Utils.isWordChar(line.charAt(startColumn - 1));
|
|
const rightBound = endColumn === line.length || !TextUtils.TextUtils.Utils.isWordChar(line.charAt(endColumn));
|
|
return leftBound && rightBound && TextUtils.TextUtils.Utils.isWord(selectedText);
|
|
}
|
|
|
|
_removeHighlight() {
|
|
if (this._highlightDescriptor) {
|
|
this._codeMirror.removeOverlay(this._highlightDescriptor.overlay);
|
|
delete this._highlightDescriptor;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {!RegExp} regex
|
|
* @param {!CodeMirror.StringStream} stream
|
|
*/
|
|
_searchHighlighter(regex, stream) {
|
|
if (stream.column() === 0) {
|
|
delete this._searchMatchLength;
|
|
}
|
|
if (this._searchMatchLength) {
|
|
if (this._searchMatchLength > 2) {
|
|
for (let i = 0; i < this._searchMatchLength - 2; ++i) {
|
|
stream.next();
|
|
}
|
|
this._searchMatchLength = 1;
|
|
return 'search-highlight';
|
|
}
|
|
stream.next();
|
|
delete this._searchMatchLength;
|
|
return 'search-highlight search-highlight-end';
|
|
}
|
|
const match = stream.match(regex, false);
|
|
if (match) {
|
|
stream.next();
|
|
const matchLength = match[0].length;
|
|
if (matchLength === 1) {
|
|
return 'search-highlight search-highlight-full';
|
|
}
|
|
this._searchMatchLength = matchLength;
|
|
return 'search-highlight search-highlight-start';
|
|
}
|
|
while (!stream.match(regex, false) && stream.next()) {
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string} token
|
|
* @param {!CodeMirror.Pos} selectionStart
|
|
* @param {!CodeMirror.StringStream} stream
|
|
*/
|
|
_tokenHighlighter(token, selectionStart, stream) {
|
|
const tokenFirstChar = token.charAt(0);
|
|
if (stream.match(token) && (stream.eol() || !TextUtils.TextUtils.Utils.isWordChar(stream.peek()))) {
|
|
return stream.column() === selectionStart.ch ? 'token-highlight column-with-selection' : 'token-highlight';
|
|
}
|
|
let eatenChar;
|
|
do {
|
|
eatenChar = stream.next();
|
|
} while (eatenChar && (TextUtils.TextUtils.Utils.isWordChar(eatenChar) || stream.peek() !== tokenFirstChar));
|
|
}
|
|
|
|
/**
|
|
* @param {function(!CodeMirror.StringStream)} highlighter
|
|
* @param {?CodeMirror.Pos} selectionStart
|
|
*/
|
|
_setHighlighter(highlighter, selectionStart) {
|
|
const overlayMode = {token: highlighter};
|
|
this._codeMirror.addOverlay(overlayMode);
|
|
this._highlightDescriptor = {overlay: overlayMode, selectionStart: selectionStart};
|
|
}
|
|
}
|
|
|
|
const LinesToScanForIndentationGuessing = 1000;
|
|
const MaximumNumberOfWhitespacesPerSingleSpan = 16;
|
|
export const lineNumbersGutterType = 'CodeMirror-linenumbers';
|
|
|
|
/** @typedef {{gutterType: string, lineNumber: number, event: !Event}} */
|
|
export let GutterClickEventData;
|