Path: blob/main/src/vs/editor/contrib/bracketMatching/browser/bracketMatching.ts
3296 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 { themeColorFromId } from '../../../../platform/theme/common/themeService.js';2425const overviewRulerBracketMatchForeground = registerColor('editorOverviewRuler.bracketMatchForeground', '#A0A0A0', nls.localize('overviewRulerBracketMatchForeground', 'Overview ruler marker color for matching brackets.'));2627class JumpToBracketAction extends EditorAction {28constructor() {29super({30id: 'editor.action.jumpToBracket',31label: nls.localize2('smartSelect.jumpBracket', "Go to Bracket"),32precondition: undefined,33kbOpts: {34kbExpr: EditorContextKeys.editorTextFocus,35primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Backslash,36weight: KeybindingWeight.EditorContrib37}38});39}4041public run(accessor: ServicesAccessor, editor: ICodeEditor): void {42BracketMatchingController.get(editor)?.jumpToBracket();43}44}4546class SelectToBracketAction extends EditorAction {47constructor() {48super({49id: 'editor.action.selectToBracket',50label: nls.localize2('smartSelect.selectToBracket', "Select to Bracket"),51precondition: undefined,52metadata: {53description: nls.localize2('smartSelect.selectToBracketDescription', "Select the text inside and including the brackets or curly braces"),54args: [{55name: 'args',56schema: {57type: 'object',58properties: {59'selectBrackets': {60type: 'boolean',61default: true62}63},64}65}]66}67});68}6970public run(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void {71let selectBrackets = true;72if (args && args.selectBrackets === false) {73selectBrackets = false;74}75BracketMatchingController.get(editor)?.selectToBracket(selectBrackets);76}77}78class RemoveBracketsAction extends EditorAction {79constructor() {80super({81id: 'editor.action.removeBrackets',82label: nls.localize2('smartSelect.removeBrackets', "Remove Brackets"),83precondition: undefined,84kbOpts: {85kbExpr: EditorContextKeys.editorTextFocus,86primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.Backspace,87weight: KeybindingWeight.EditorContrib88}89});90}9192public run(accessor: ServicesAccessor, editor: ICodeEditor): void {93BracketMatchingController.get(editor)?.removeBrackets(this.id);94}95}9697type Brackets = [Range, Range];9899class BracketsData {100public readonly position: Position;101public readonly brackets: Brackets | null;102public readonly options: ModelDecorationOptions;103104constructor(position: Position, brackets: Brackets | null, options: ModelDecorationOptions) {105this.position = position;106this.brackets = brackets;107this.options = options;108}109}110111export class BracketMatchingController extends Disposable implements IEditorContribution {112public static readonly ID = 'editor.contrib.bracketMatchingController';113114public static get(editor: ICodeEditor): BracketMatchingController | null {115return editor.getContribution<BracketMatchingController>(BracketMatchingController.ID);116}117118private readonly _editor: ICodeEditor;119120private _lastBracketsData: BracketsData[];121private _lastVersionId: number;122private readonly _decorations: IEditorDecorationsCollection;123private readonly _updateBracketsSoon: RunOnceScheduler;124private _matchBrackets: 'never' | 'near' | 'always';125126constructor(127editor: ICodeEditor128) {129super();130this._editor = editor;131this._lastBracketsData = [];132this._lastVersionId = 0;133this._decorations = this._editor.createDecorationsCollection();134this._updateBracketsSoon = this._register(new RunOnceScheduler(() => this._updateBrackets(), 50));135this._matchBrackets = this._editor.getOption(EditorOption.matchBrackets);136137this._updateBracketsSoon.schedule();138this._register(editor.onDidChangeCursorPosition((e) => {139140if (this._matchBrackets === 'never') {141// Early exit if nothing needs to be done!142// Leave some form of early exit check here if you wish to continue being a cursor position change listener ;)143return;144}145146this._updateBracketsSoon.schedule();147}));148this._register(editor.onDidChangeModelContent((e) => {149this._updateBracketsSoon.schedule();150}));151this._register(editor.onDidChangeModel((e) => {152this._lastBracketsData = [];153this._updateBracketsSoon.schedule();154}));155this._register(editor.onDidChangeModelLanguageConfiguration((e) => {156this._lastBracketsData = [];157this._updateBracketsSoon.schedule();158}));159this._register(editor.onDidChangeConfiguration((e) => {160if (e.hasChanged(EditorOption.matchBrackets)) {161this._matchBrackets = this._editor.getOption(EditorOption.matchBrackets);162this._decorations.clear();163this._lastBracketsData = [];164this._lastVersionId = 0;165this._updateBracketsSoon.schedule();166}167}));168169this._register(editor.onDidBlurEditorWidget(() => {170this._updateBracketsSoon.schedule();171}));172173this._register(editor.onDidFocusEditorWidget(() => {174this._updateBracketsSoon.schedule();175}));176}177178public jumpToBracket(): void {179if (!this._editor.hasModel()) {180return;181}182183const model = this._editor.getModel();184const newSelections = this._editor.getSelections().map(selection => {185const position = selection.getStartPosition();186187// find matching brackets if position is on a bracket188const brackets = model.bracketPairs.matchBracket(position);189let newCursorPosition: Position | null = null;190if (brackets) {191if (brackets[0].containsPosition(position) && !brackets[1].containsPosition(position)) {192newCursorPosition = brackets[1].getStartPosition();193} else if (brackets[1].containsPosition(position)) {194newCursorPosition = brackets[0].getStartPosition();195}196} else {197// find the enclosing brackets if the position isn't on a matching bracket198const enclosingBrackets = model.bracketPairs.findEnclosingBrackets(position);199if (enclosingBrackets) {200newCursorPosition = enclosingBrackets[1].getStartPosition();201} else {202// no enclosing brackets, try the very first next bracket203const nextBracket = model.bracketPairs.findNextBracket(position);204if (nextBracket && nextBracket.range) {205newCursorPosition = nextBracket.range.getStartPosition();206}207}208}209210if (newCursorPosition) {211return new Selection(newCursorPosition.lineNumber, newCursorPosition.column, newCursorPosition.lineNumber, newCursorPosition.column);212}213return new Selection(position.lineNumber, position.column, position.lineNumber, position.column);214});215216this._editor.setSelections(newSelections);217this._editor.revealRange(newSelections[0]);218}219220public selectToBracket(selectBrackets: boolean): void {221if (!this._editor.hasModel()) {222return;223}224225const model = this._editor.getModel();226const newSelections: Selection[] = [];227228this._editor.getSelections().forEach(selection => {229const position = selection.getStartPosition();230let brackets = model.bracketPairs.matchBracket(position);231232if (!brackets) {233brackets = model.bracketPairs.findEnclosingBrackets(position);234if (!brackets) {235const nextBracket = model.bracketPairs.findNextBracket(position);236if (nextBracket && nextBracket.range) {237brackets = model.bracketPairs.matchBracket(nextBracket.range.getStartPosition());238}239}240}241242let selectFrom: Position | null = null;243let selectTo: Position | null = null;244245if (brackets) {246brackets.sort(Range.compareRangesUsingStarts);247const [open, close] = brackets;248selectFrom = selectBrackets ? open.getStartPosition() : open.getEndPosition();249selectTo = selectBrackets ? close.getEndPosition() : close.getStartPosition();250251if (close.containsPosition(position)) {252// select backwards if the cursor was on the closing bracket253const tmp = selectFrom;254selectFrom = selectTo;255selectTo = tmp;256}257}258259if (selectFrom && selectTo) {260newSelections.push(new Selection(selectFrom.lineNumber, selectFrom.column, selectTo.lineNumber, selectTo.column));261}262});263264if (newSelections.length > 0) {265this._editor.setSelections(newSelections);266this._editor.revealRange(newSelections[0]);267}268}269public removeBrackets(editSource?: string): void {270if (!this._editor.hasModel()) {271return;272}273274const model = this._editor.getModel();275this._editor.getSelections().forEach((selection) => {276const position = selection.getPosition();277278let brackets = model.bracketPairs.matchBracket(position);279if (!brackets) {280brackets = model.bracketPairs.findEnclosingBrackets(position);281}282if (brackets) {283this._editor.pushUndoStop();284this._editor.executeEdits(285editSource,286[287{ range: brackets[0], text: '' },288{ range: brackets[1], text: '' }289]290);291this._editor.pushUndoStop();292}293});294}295296private static readonly _DECORATION_OPTIONS_WITH_OVERVIEW_RULER = ModelDecorationOptions.register({297description: 'bracket-match-overview',298stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,299className: 'bracket-match',300overviewRuler: {301color: themeColorFromId(overviewRulerBracketMatchForeground),302position: OverviewRulerLane.Center303}304});305306private static readonly _DECORATION_OPTIONS_WITHOUT_OVERVIEW_RULER = ModelDecorationOptions.register({307description: 'bracket-match-no-overview',308stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,309className: 'bracket-match'310});311312private _updateBrackets(): void {313if (this._matchBrackets === 'never') {314return;315}316this._recomputeBrackets();317318const newDecorations: IModelDeltaDecoration[] = [];319let newDecorationsLen = 0;320for (const bracketData of this._lastBracketsData) {321const brackets = bracketData.brackets;322if (brackets) {323newDecorations[newDecorationsLen++] = { range: brackets[0], options: bracketData.options };324newDecorations[newDecorationsLen++] = { range: brackets[1], options: bracketData.options };325}326}327328this._decorations.set(newDecorations);329}330331private _recomputeBrackets(): void {332if (!this._editor.hasModel() || !this._editor.hasWidgetFocus()) {333// no model or no focus => no brackets!334this._lastBracketsData = [];335this._lastVersionId = 0;336return;337}338339const selections = this._editor.getSelections();340if (selections.length > 100) {341// no bracket matching for high numbers of selections342this._lastBracketsData = [];343this._lastVersionId = 0;344return;345}346347const model = this._editor.getModel();348const versionId = model.getVersionId();349let previousData: BracketsData[] = [];350if (this._lastVersionId === versionId) {351// use the previous data only if the model is at the same version id352previousData = this._lastBracketsData;353}354355const positions: Position[] = [];356let positionsLen = 0;357for (let i = 0, len = selections.length; i < len; i++) {358const selection = selections[i];359360if (selection.isEmpty()) {361// will bracket match a cursor only if the selection is collapsed362positions[positionsLen++] = selection.getStartPosition();363}364}365366// sort positions for `previousData` cache hits367if (positions.length > 1) {368positions.sort(Position.compare);369}370371const newData: BracketsData[] = [];372let newDataLen = 0;373let previousIndex = 0;374const previousLen = previousData.length;375for (let i = 0, len = positions.length; i < len; i++) {376const position = positions[i];377378while (previousIndex < previousLen && previousData[previousIndex].position.isBefore(position)) {379previousIndex++;380}381382if (previousIndex < previousLen && previousData[previousIndex].position.equals(position)) {383newData[newDataLen++] = previousData[previousIndex];384} else {385let brackets = model.bracketPairs.matchBracket(position, 20 /* give at most 20ms to compute */);386let options = BracketMatchingController._DECORATION_OPTIONS_WITH_OVERVIEW_RULER;387if (!brackets && this._matchBrackets === 'always') {388brackets = model.bracketPairs.findEnclosingBrackets(position, 20 /* give at most 20ms to compute */);389options = BracketMatchingController._DECORATION_OPTIONS_WITHOUT_OVERVIEW_RULER;390}391newData[newDataLen++] = new BracketsData(position, brackets, options);392}393}394395this._lastBracketsData = newData;396this._lastVersionId = versionId;397}398}399400registerEditorContribution(BracketMatchingController.ID, BracketMatchingController, EditorContributionInstantiation.AfterFirstRender);401registerEditorAction(SelectToBracketAction);402registerEditorAction(JumpToBracketAction);403registerEditorAction(RemoveBracketsAction);404405// Go to menu406MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, {407group: '5_infile_nav',408command: {409id: 'editor.action.jumpToBracket',410title: nls.localize({ key: 'miGoToBracket', comment: ['&& denotes a mnemonic'] }, "Go to &&Bracket")411},412order: 2413});414415416