Path: blob/main/src/vs/editor/contrib/bracketMatching/browser/bracketMatching.ts
5241 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { RunOnceScheduler } from '../../../../base/common/async.js';6import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';7import { Disposable } from '../../../../base/common/lifecycle.js';8import './bracketMatching.css';9import { ICodeEditor } from '../../../browser/editorBrowser.js';10import { EditorAction, EditorContributionInstantiation, registerEditorAction, registerEditorContribution, ServicesAccessor } from '../../../browser/editorExtensions.js';11import { EditorOption } from '../../../common/config/editorOptions.js';12import { Position } from '../../../common/core/position.js';13import { Range } from '../../../common/core/range.js';14import { Selection } from '../../../common/core/selection.js';15import { IEditorContribution, IEditorDecorationsCollection } from '../../../common/editorCommon.js';16import { EditorContextKeys } from '../../../common/editorContextKeys.js';17import { IModelDeltaDecoration, OverviewRulerLane, TrackedRangeStickiness } from '../../../common/model.js';18import { ModelDecorationOptions } from '../../../common/model/textModel.js';19import * as nls from '../../../../nls.js';20import { MenuId, MenuRegistry } from '../../../../platform/actions/common/actions.js';21import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';22import { registerColor } from '../../../../platform/theme/common/colorRegistry.js';23import { registerThemingParticipant, themeColorFromId } from '../../../../platform/theme/common/themeService.js';24import { editorBracketMatchForeground } from '../../../common/core/editorColorRegistry.js';2526const overviewRulerBracketMatchForeground = registerColor('editorOverviewRuler.bracketMatchForeground', '#A0A0A0', nls.localize('overviewRulerBracketMatchForeground', 'Overview ruler marker color for matching brackets.'));2728class JumpToBracketAction extends EditorAction {29constructor() {30super({31id: 'editor.action.jumpToBracket',32label: nls.localize2('smartSelect.jumpBracket', "Go to Bracket"),33precondition: undefined,34kbOpts: {35kbExpr: EditorContextKeys.editorTextFocus,36primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Backslash,37weight: KeybindingWeight.EditorContrib38}39});40}4142public run(accessor: ServicesAccessor, editor: ICodeEditor): void {43BracketMatchingController.get(editor)?.jumpToBracket();44}45}4647class SelectToBracketAction extends EditorAction {48constructor() {49super({50id: 'editor.action.selectToBracket',51label: nls.localize2('smartSelect.selectToBracket', "Select to Bracket"),52precondition: undefined,53metadata: {54description: nls.localize2('smartSelect.selectToBracketDescription', "Select the text inside and including the brackets or curly braces"),55args: [{56name: 'args',57schema: {58type: 'object',59properties: {60'selectBrackets': {61type: 'boolean',62default: true63}64},65}66}]67}68});69}7071public run(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void {72let selectBrackets = true;73if (args && args.selectBrackets === false) {74selectBrackets = false;75}76BracketMatchingController.get(editor)?.selectToBracket(selectBrackets);77}78}7980class RemoveBracketsAction extends EditorAction {81constructor() {82super({83id: 'editor.action.removeBrackets',84label: nls.localize2('smartSelect.removeBrackets', "Remove Brackets"),85precondition: undefined,86kbOpts: {87kbExpr: EditorContextKeys.editorTextFocus,88primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.Backspace,89weight: KeybindingWeight.EditorContrib90},91canTriggerInlineEdits: true,92});93}9495public run(accessor: ServicesAccessor, editor: ICodeEditor): void {96BracketMatchingController.get(editor)?.removeBrackets(this.id);97}98}99100type Brackets = [Range, Range];101102class BracketsData {103public readonly position: Position;104public readonly brackets: Brackets | null;105public readonly options: ModelDecorationOptions;106107constructor(position: Position, brackets: Brackets | null, options: ModelDecorationOptions) {108this.position = position;109this.brackets = brackets;110this.options = options;111}112}113114export class BracketMatchingController extends Disposable implements IEditorContribution {115public static readonly ID = 'editor.contrib.bracketMatchingController';116117public static get(editor: ICodeEditor): BracketMatchingController | null {118return editor.getContribution<BracketMatchingController>(BracketMatchingController.ID);119}120121private readonly _editor: ICodeEditor;122123private _lastBracketsData: BracketsData[];124private _lastVersionId: number;125private readonly _decorations: IEditorDecorationsCollection;126private readonly _updateBracketsSoon: RunOnceScheduler;127private _matchBrackets: 'never' | 'near' | 'always';128129constructor(130editor: ICodeEditor131) {132super();133this._editor = editor;134this._lastBracketsData = [];135this._lastVersionId = 0;136this._decorations = this._editor.createDecorationsCollection();137this._updateBracketsSoon = this._register(new RunOnceScheduler(() => this._updateBrackets(), 50));138this._matchBrackets = this._editor.getOption(EditorOption.matchBrackets);139140this._updateBracketsSoon.schedule();141this._register(editor.onDidChangeCursorPosition((e) => {142143if (this._matchBrackets === 'never') {144// Early exit if nothing needs to be done!145// Leave some form of early exit check here if you wish to continue being a cursor position change listener ;)146return;147}148149this._updateBracketsSoon.schedule();150}));151this._register(editor.onDidChangeModelContent((e) => {152this._updateBracketsSoon.schedule();153}));154this._register(editor.onDidChangeModel((e) => {155this._lastBracketsData = [];156this._updateBracketsSoon.schedule();157}));158this._register(editor.onDidChangeModelLanguageConfiguration((e) => {159this._lastBracketsData = [];160this._updateBracketsSoon.schedule();161}));162this._register(editor.onDidChangeConfiguration((e) => {163if (e.hasChanged(EditorOption.matchBrackets)) {164this._matchBrackets = this._editor.getOption(EditorOption.matchBrackets);165this._decorations.clear();166this._lastBracketsData = [];167this._lastVersionId = 0;168this._updateBracketsSoon.schedule();169}170}));171172this._register(editor.onDidBlurEditorWidget(() => {173this._updateBracketsSoon.schedule();174}));175176this._register(editor.onDidFocusEditorWidget(() => {177this._updateBracketsSoon.schedule();178}));179}180181public jumpToBracket(): void {182if (!this._editor.hasModel()) {183return;184}185186const model = this._editor.getModel();187const newSelections = this._editor.getSelections().map(selection => {188const position = selection.getStartPosition();189190// find matching brackets if position is on a bracket191const brackets = model.bracketPairs.matchBracket(position);192let newCursorPosition: Position | null = null;193if (brackets) {194if (brackets[0].containsPosition(position) && !brackets[1].containsPosition(position)) {195newCursorPosition = brackets[1].getStartPosition();196} else if (brackets[1].containsPosition(position)) {197newCursorPosition = brackets[0].getStartPosition();198}199} else {200// find the enclosing brackets if the position isn't on a matching bracket201const enclosingBrackets = model.bracketPairs.findEnclosingBrackets(position);202if (enclosingBrackets) {203newCursorPosition = enclosingBrackets[1].getStartPosition();204} else {205// no enclosing brackets, try the very first next bracket206const nextBracket = model.bracketPairs.findNextBracket(position);207if (nextBracket && nextBracket.range) {208newCursorPosition = nextBracket.range.getStartPosition();209}210}211}212213if (newCursorPosition) {214return new Selection(newCursorPosition.lineNumber, newCursorPosition.column, newCursorPosition.lineNumber, newCursorPosition.column);215}216return new Selection(position.lineNumber, position.column, position.lineNumber, position.column);217});218219this._editor.setSelections(newSelections);220this._editor.revealRange(newSelections[0]);221}222223public selectToBracket(selectBrackets: boolean): void {224if (!this._editor.hasModel()) {225return;226}227228const model = this._editor.getModel();229const newSelections: Selection[] = [];230231this._editor.getSelections().forEach(selection => {232const position = selection.getStartPosition();233let brackets = model.bracketPairs.matchBracket(position);234235if (!brackets) {236brackets = model.bracketPairs.findEnclosingBrackets(position);237if (!brackets) {238const nextBracket = model.bracketPairs.findNextBracket(position);239if (nextBracket && nextBracket.range) {240brackets = model.bracketPairs.matchBracket(nextBracket.range.getStartPosition());241}242}243}244245let selectFrom: Position | null = null;246let selectTo: Position | null = null;247248if (brackets) {249brackets.sort(Range.compareRangesUsingStarts);250const [open, close] = brackets;251selectFrom = selectBrackets ? open.getStartPosition() : open.getEndPosition();252selectTo = selectBrackets ? close.getEndPosition() : close.getStartPosition();253254if (close.containsPosition(position)) {255// select backwards if the cursor was on the closing bracket256const tmp = selectFrom;257selectFrom = selectTo;258selectTo = tmp;259}260}261262if (selectFrom && selectTo) {263newSelections.push(new Selection(selectFrom.lineNumber, selectFrom.column, selectTo.lineNumber, selectTo.column));264}265});266267if (newSelections.length > 0) {268this._editor.setSelections(newSelections);269this._editor.revealRange(newSelections[0]);270}271}272public removeBrackets(editSource?: string): void {273if (!this._editor.hasModel()) {274return;275}276277const model = this._editor.getModel();278this._editor.getSelections().forEach((selection) => {279const position = selection.getPosition();280281let brackets = model.bracketPairs.matchBracket(position);282if (!brackets) {283brackets = model.bracketPairs.findEnclosingBrackets(position);284}285if (brackets) {286this._editor.pushUndoStop();287this._editor.executeEdits(288editSource,289[290{ range: brackets[0], text: '' },291{ range: brackets[1], text: '' }292]293);294this._editor.pushUndoStop();295}296});297}298299private static readonly _DECORATION_OPTIONS_WITH_OVERVIEW_RULER = ModelDecorationOptions.register({300description: 'bracket-match-overview',301stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,302className: 'bracket-match',303inlineClassName: 'bracket-match-inline',304overviewRuler: {305color: themeColorFromId(overviewRulerBracketMatchForeground),306position: OverviewRulerLane.Center307}308});309310private static readonly _DECORATION_OPTIONS_WITHOUT_OVERVIEW_RULER = ModelDecorationOptions.register({311description: 'bracket-match-no-overview',312stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,313className: 'bracket-match',314inlineClassName: 'bracket-match-inline'315});316317private _updateBrackets(): void {318if (this._matchBrackets === 'never') {319return;320}321this._recomputeBrackets();322323const newDecorations: IModelDeltaDecoration[] = [];324let newDecorationsLen = 0;325for (const bracketData of this._lastBracketsData) {326const brackets = bracketData.brackets;327if (brackets) {328newDecorations[newDecorationsLen++] = { range: brackets[0], options: bracketData.options };329newDecorations[newDecorationsLen++] = { range: brackets[1], options: bracketData.options };330}331}332333this._decorations.set(newDecorations);334}335336private _recomputeBrackets(): void {337if (!this._editor.hasModel() || !this._editor.hasWidgetFocus()) {338// no model or no focus => no brackets!339this._lastBracketsData = [];340this._lastVersionId = 0;341return;342}343344const selections = this._editor.getSelections();345if (selections.length > 100) {346// no bracket matching for high numbers of selections347this._lastBracketsData = [];348this._lastVersionId = 0;349return;350}351352const model = this._editor.getModel();353const versionId = model.getVersionId();354let previousData: BracketsData[] = [];355if (this._lastVersionId === versionId) {356// use the previous data only if the model is at the same version id357previousData = this._lastBracketsData;358}359360const positions: Position[] = [];361let positionsLen = 0;362for (let i = 0, len = selections.length; i < len; i++) {363const selection = selections[i];364365if (selection.isEmpty()) {366// will bracket match a cursor only if the selection is collapsed367positions[positionsLen++] = selection.getStartPosition();368}369}370371// sort positions for `previousData` cache hits372if (positions.length > 1) {373positions.sort(Position.compare);374}375376const newData: BracketsData[] = [];377let newDataLen = 0;378let previousIndex = 0;379const previousLen = previousData.length;380for (let i = 0, len = positions.length; i < len; i++) {381const position = positions[i];382383while (previousIndex < previousLen && previousData[previousIndex].position.isBefore(position)) {384previousIndex++;385}386387if (previousIndex < previousLen && previousData[previousIndex].position.equals(position)) {388newData[newDataLen++] = previousData[previousIndex];389} else {390let brackets = model.bracketPairs.matchBracket(position, 20 /* give at most 20ms to compute */);391let options = BracketMatchingController._DECORATION_OPTIONS_WITH_OVERVIEW_RULER;392if (!brackets && this._matchBrackets === 'always') {393brackets = model.bracketPairs.findEnclosingBrackets(position, 20 /* give at most 20ms to compute */);394options = BracketMatchingController._DECORATION_OPTIONS_WITHOUT_OVERVIEW_RULER;395}396newData[newDataLen++] = new BracketsData(position, brackets, options);397}398}399400this._lastBracketsData = newData;401this._lastVersionId = versionId;402}403}404405registerEditorContribution(BracketMatchingController.ID, BracketMatchingController, EditorContributionInstantiation.AfterFirstRender);406registerEditorAction(SelectToBracketAction);407registerEditorAction(JumpToBracketAction);408registerEditorAction(RemoveBracketsAction);409410// Go to menu411MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, {412group: '5_infile_nav',413command: {414id: 'editor.action.jumpToBracket',415title: nls.localize({ key: 'miGoToBracket', comment: ['&& denotes a mnemonic'] }, "Go to &&Bracket")416},417order: 2418});419420// Theming participant to ensure bracket-match color overrides bracket pair colorization421registerThemingParticipant((theme, collector) => {422const bracketMatchForeground = theme.getColor(editorBracketMatchForeground);423if (bracketMatchForeground) {424// Use higher specificity to override bracket pair colorization425// Apply color to inline class to avoid layout jumps426collector.addRule(`.monaco-editor .bracket-match-inline { color: ${bracketMatchForeground} !important; }`);427}428});429430431