blob: 1c56781b286b6b428afc9bbc836c9dfa46d3be4f [file] [log] [blame]
// 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.
/**
* @unrestricted
*/
SourceFrame.SourcesTextEditor = class extends TextEditor.CodeMirrorTextEditor {
/**
* @param {!SourceFrame.SourcesTextEditorDelegate} delegate
*/
constructor(delegate) {
super({
lineNumbers: true,
lineWrapping: false,
bracketMatchingSetting: Common.moduleSetting('textEditorBracketMatching'),
padBottom: true
});
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.codeMirror().addKeyMap(SourceFrame.SourcesTextEditor._BlockIndentController);
this._tokenHighlighter = new SourceFrame.SourcesTextEditor.TokenHighlighter(this, this.codeMirror());
/** @type {!Array<string>} */
this._gutters = ['CodeMirror-linenumbers'];
this.codeMirror().setOption('gutters', this._gutters.slice());
this.codeMirror().setOption('electricChars', false);
this.codeMirror().setOption('smartIndent', false);
/**
* @this {SourceFrame.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);
Common.moduleSetting('textEditorIndent').addChangeListener(this._onUpdateEditorIndentation, this);
Common.moduleSetting('textEditorAutoDetectIndent').addChangeListener(this._onUpdateEditorIndentation, this);
Common.moduleSetting('showWhitespacesInEditor').addChangeListener(this._updateWhitespace, this);
Common.moduleSetting('textEditorCodeFolding').addChangeListener(this._updateCodeFolding, this);
this._updateCodeFolding();
/** @type {?UI.AutocompleteConfig} */
this._autocompleteConfig = {isWordChar: TextUtils.TextUtils.isWordChar};
Common.moduleSetting('textEditorAutocompletion').addChangeListener(this._updateAutocomplete, this);
this._updateAutocomplete();
this._onUpdateEditorIndentation();
this._setupWhitespaceHighlight();
}
/**
* @param {!UI.Infobar} infobar
*/
attachInfobar(infobar) {
this.element.insertBefore(infobar.element, this.element.firstChild);
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.isSpaceChar(text[0]))
continue;
if (tabRegex.test(text)) {
++tabLines;
continue;
}
let i = 0;
while (i < text.length && TextUtils.TextUtils.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 Common.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} range
*/
highlightSearchResults(regex, range) {
/**
* @this {TextEditor.CodeMirrorTextEditor}
*/
function innerHighlightRegex() {
if (range) {
this.scrollLineIntoView(range.startLine);
if (range.endColumn > TextEditor.CodeMirrorTextEditor.maxHighlightLength)
this.setSelection(range);
else
this.setSelection(TextUtils.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} 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, gutter, event) {
if (gutter !== 'CodeMirror-linenumbers')
return;
this.dispatchEventToListeners(
SourceFrame.SourcesTextEditor.Events.GutterClick, {lineNumber: lineNumber, event: event});
}
_contextMenu(event) {
const contextMenu = new UI.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 {SourceFrame.SourcesTextEditor}
*/
function showAsync() {
contextMenu.appendApplicableItems(this);
contextMenu.show();
}
}
/**
* @override
* @param {!TextUtils.TextRange} range
* @param {string} text
* @param {string=} origin
* @return {!TextUtils.TextRange}
*/
editRange(range, text, origin) {
const newRange = super.editRange(range, text, origin);
if (Common.moduleSetting('textEditorAutoDetectIndent').get())
this._onUpdateEditorIndentation();
return newRange;
}
_onUpdateEditorIndentation() {
this._setEditorIndentation(TextEditor.CodeMirrorUtils.pullLines(
this.codeMirror(), SourceFrame.SourcesTextEditor.LinesToScanForIndentationGuessing));
}
/**
* @param {!Array.<string>} lines
*/
_setEditorIndentation(lines) {
const extraKeys = {};
let indent = Common.moduleSetting('textEditorIndent').get();
if (Common.moduleSetting('textEditorAutoDetectIndent').get())
indent = SourceFrame.SourcesTextEditor._guessIndentationLevel(lines);
if (indent === TextUtils.TextUtils.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.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(
SourceFrame.SourcesTextEditor.Events.SelectionChanged, TextEditor.CodeMirrorUtils.toRange(start, end));
}
/**
* @param {?TextUtils.TextRange} from
* @param {?TextUtils.TextRange} to
*/
_reportJump(from, to) {
if (from && to && from.equal(to))
return;
this.dispatchEventToListeners(SourceFrame.SourcesTextEditor.Events.JumpHappened, {from: from, to: to});
}
_scroll() {
const topmostLineNumber = this.codeMirror().lineAtHeight(this.codeMirror().getScrollInfo().top, 'local');
this.dispatchEventToListeners(SourceFrame.SourcesTextEditor.Events.ScrollChanged, topmostLineNumber);
}
_focus() {
this.dispatchEventToListeners(SourceFrame.SourcesTextEditor.Events.EditorFocused);
}
_blur() {
this.dispatchEventToListeners(SourceFrame.SourcesTextEditor.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();
Common.moduleSetting('textEditorIndent').removeChangeListener(this._onUpdateEditorIndentation, this);
Common.moduleSetting('textEditorAutoDetectIndent').removeChangeListener(this._onUpdateEditorIndentation, this);
Common.moduleSetting('showWhitespacesInEditor').removeChangeListener(this._updateWhitespace, this);
Common.moduleSetting('textEditorCodeFolding').removeChangeListener(this._updateCodeFolding, this);
}
/**
* @override
* @param {string} text
*/
setText(text) {
this._setEditorIndentation(
text.split('\n').slice(0, SourceFrame.SourcesTextEditor.LinesToScanForIndentationGuessing));
super.setText(text);
}
_updateWhitespace() {
this.setMimeType(this.mimeType());
}
_updateCodeFolding() {
if (Common.moduleSetting('textEditorCodeFolding').get()) {
this.installGutter('CodeMirror-foldgutter', false);
} else {
this.codeMirror().execCommand('unfoldAll');
this.uninstallGutter('CodeMirror-foldgutter');
}
}
/**
* @override
* @param {string} mimeType
* @return {string}
*/
rewriteMimeType(mimeType) {
this._setupWhitespaceHighlight();
const whitespaceMode = Common.moduleSetting('showWhitespacesInEditor').get();
this.element.classList.toggle('show-whitespaces', whitespaceMode === 'all');
if (whitespaceMode === 'all')
return this._allWhitespaceOverlayMode(mimeType);
else 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 < SourceFrame.SourcesTextEditor.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 || !Common.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 <= SourceFrame.SourcesTextEditor.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.AutocompleteConfig} config
*/
configureAutocomplete(config) {
this._autocompleteConfig = config;
this._updateAutocomplete();
}
_updateAutocomplete() {
super.configureAutocomplete(
Common.moduleSetting('textEditorAutocompletion').get() ? this._autocompleteConfig : null);
}
};
/** @typedef {{lineNumber: number, event: !Event}} */
SourceFrame.SourcesTextEditor.GutterClickEventData;
/** @enum {symbol} */
SourceFrame.SourcesTextEditor.Events = {
GutterClick: Symbol('GutterClick'),
SelectionChanged: Symbol('SelectionChanged'),
ScrollChanged: Symbol('ScrollChanged'),
EditorFocused: Symbol('EditorFocused'),
EditorBlurred: Symbol('EditorBlurred'),
JumpHappened: Symbol('JumpHappened')
};
/**
* @interface
*/
SourceFrame.SourcesTextEditorDelegate = function() {};
SourceFrame.SourcesTextEditorDelegate.prototype = {
/**
* @param {!UI.ContextMenu} contextMenu
* @param {number} lineNumber
* @return {!Promise}
*/
populateLineGutterContextMenu(contextMenu, lineNumber) {},
/**
* @param {!UI.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.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);
};
SourceFrame.SourcesTextEditor._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.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.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.lineIndent(line);
replacements.push(indent + '}');
}
codeMirror.setSelections(updatedSelections);
codeMirror.replaceSelections(replacements);
}
};
/**
* @unrestricted
*/
SourceFrame.SourcesTextEditor.TokenHighlighter = class {
/**
* @param {!SourceFrame.SourcesTextEditor} textEditor
* @param {!CodeMirror} codeMirror
*/
constructor(textEditor, codeMirror) {
this._textEditor = textEditor;
this._codeMirror = codeMirror;
}
/**
* @param {!RegExp} regex
* @param {?TextUtils.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.isWordChar(line.charAt(startColumn - 1));
const rightBound = endColumn === line.length || !TextUtils.TextUtils.isWordChar(line.charAt(endColumn));
return leftBound && rightBound && TextUtils.TextUtils.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';
} else {
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.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.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};
}
};
SourceFrame.SourcesTextEditor.LinesToScanForIndentationGuessing = 1000;
SourceFrame.SourcesTextEditor.MaximumNumberOfWhitespacesPerSingleSpan = 16;