Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/bracketMatching/browser/bracketMatching.ts
5241 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { RunOnceScheduler } from '../../../../base/common/async.js';
7
import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
8
import { Disposable } from '../../../../base/common/lifecycle.js';
9
import './bracketMatching.css';
10
import { ICodeEditor } from '../../../browser/editorBrowser.js';
11
import { EditorAction, EditorContributionInstantiation, registerEditorAction, registerEditorContribution, ServicesAccessor } from '../../../browser/editorExtensions.js';
12
import { EditorOption } from '../../../common/config/editorOptions.js';
13
import { Position } from '../../../common/core/position.js';
14
import { Range } from '../../../common/core/range.js';
15
import { Selection } from '../../../common/core/selection.js';
16
import { IEditorContribution, IEditorDecorationsCollection } from '../../../common/editorCommon.js';
17
import { EditorContextKeys } from '../../../common/editorContextKeys.js';
18
import { IModelDeltaDecoration, OverviewRulerLane, TrackedRangeStickiness } from '../../../common/model.js';
19
import { ModelDecorationOptions } from '../../../common/model/textModel.js';
20
import * as nls from '../../../../nls.js';
21
import { MenuId, MenuRegistry } from '../../../../platform/actions/common/actions.js';
22
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
23
import { registerColor } from '../../../../platform/theme/common/colorRegistry.js';
24
import { registerThemingParticipant, themeColorFromId } from '../../../../platform/theme/common/themeService.js';
25
import { editorBracketMatchForeground } from '../../../common/core/editorColorRegistry.js';
26
27
const overviewRulerBracketMatchForeground = registerColor('editorOverviewRuler.bracketMatchForeground', '#A0A0A0', nls.localize('overviewRulerBracketMatchForeground', 'Overview ruler marker color for matching brackets.'));
28
29
class JumpToBracketAction extends EditorAction {
30
constructor() {
31
super({
32
id: 'editor.action.jumpToBracket',
33
label: nls.localize2('smartSelect.jumpBracket', "Go to Bracket"),
34
precondition: undefined,
35
kbOpts: {
36
kbExpr: EditorContextKeys.editorTextFocus,
37
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Backslash,
38
weight: KeybindingWeight.EditorContrib
39
}
40
});
41
}
42
43
public run(accessor: ServicesAccessor, editor: ICodeEditor): void {
44
BracketMatchingController.get(editor)?.jumpToBracket();
45
}
46
}
47
48
class SelectToBracketAction extends EditorAction {
49
constructor() {
50
super({
51
id: 'editor.action.selectToBracket',
52
label: nls.localize2('smartSelect.selectToBracket', "Select to Bracket"),
53
precondition: undefined,
54
metadata: {
55
description: nls.localize2('smartSelect.selectToBracketDescription', "Select the text inside and including the brackets or curly braces"),
56
args: [{
57
name: 'args',
58
schema: {
59
type: 'object',
60
properties: {
61
'selectBrackets': {
62
type: 'boolean',
63
default: true
64
}
65
},
66
}
67
}]
68
}
69
});
70
}
71
72
public run(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void {
73
let selectBrackets = true;
74
if (args && args.selectBrackets === false) {
75
selectBrackets = false;
76
}
77
BracketMatchingController.get(editor)?.selectToBracket(selectBrackets);
78
}
79
}
80
81
class RemoveBracketsAction extends EditorAction {
82
constructor() {
83
super({
84
id: 'editor.action.removeBrackets',
85
label: nls.localize2('smartSelect.removeBrackets', "Remove Brackets"),
86
precondition: undefined,
87
kbOpts: {
88
kbExpr: EditorContextKeys.editorTextFocus,
89
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.Backspace,
90
weight: KeybindingWeight.EditorContrib
91
},
92
canTriggerInlineEdits: true,
93
});
94
}
95
96
public run(accessor: ServicesAccessor, editor: ICodeEditor): void {
97
BracketMatchingController.get(editor)?.removeBrackets(this.id);
98
}
99
}
100
101
type Brackets = [Range, Range];
102
103
class BracketsData {
104
public readonly position: Position;
105
public readonly brackets: Brackets | null;
106
public readonly options: ModelDecorationOptions;
107
108
constructor(position: Position, brackets: Brackets | null, options: ModelDecorationOptions) {
109
this.position = position;
110
this.brackets = brackets;
111
this.options = options;
112
}
113
}
114
115
export class BracketMatchingController extends Disposable implements IEditorContribution {
116
public static readonly ID = 'editor.contrib.bracketMatchingController';
117
118
public static get(editor: ICodeEditor): BracketMatchingController | null {
119
return editor.getContribution<BracketMatchingController>(BracketMatchingController.ID);
120
}
121
122
private readonly _editor: ICodeEditor;
123
124
private _lastBracketsData: BracketsData[];
125
private _lastVersionId: number;
126
private readonly _decorations: IEditorDecorationsCollection;
127
private readonly _updateBracketsSoon: RunOnceScheduler;
128
private _matchBrackets: 'never' | 'near' | 'always';
129
130
constructor(
131
editor: ICodeEditor
132
) {
133
super();
134
this._editor = editor;
135
this._lastBracketsData = [];
136
this._lastVersionId = 0;
137
this._decorations = this._editor.createDecorationsCollection();
138
this._updateBracketsSoon = this._register(new RunOnceScheduler(() => this._updateBrackets(), 50));
139
this._matchBrackets = this._editor.getOption(EditorOption.matchBrackets);
140
141
this._updateBracketsSoon.schedule();
142
this._register(editor.onDidChangeCursorPosition((e) => {
143
144
if (this._matchBrackets === 'never') {
145
// Early exit if nothing needs to be done!
146
// Leave some form of early exit check here if you wish to continue being a cursor position change listener ;)
147
return;
148
}
149
150
this._updateBracketsSoon.schedule();
151
}));
152
this._register(editor.onDidChangeModelContent((e) => {
153
this._updateBracketsSoon.schedule();
154
}));
155
this._register(editor.onDidChangeModel((e) => {
156
this._lastBracketsData = [];
157
this._updateBracketsSoon.schedule();
158
}));
159
this._register(editor.onDidChangeModelLanguageConfiguration((e) => {
160
this._lastBracketsData = [];
161
this._updateBracketsSoon.schedule();
162
}));
163
this._register(editor.onDidChangeConfiguration((e) => {
164
if (e.hasChanged(EditorOption.matchBrackets)) {
165
this._matchBrackets = this._editor.getOption(EditorOption.matchBrackets);
166
this._decorations.clear();
167
this._lastBracketsData = [];
168
this._lastVersionId = 0;
169
this._updateBracketsSoon.schedule();
170
}
171
}));
172
173
this._register(editor.onDidBlurEditorWidget(() => {
174
this._updateBracketsSoon.schedule();
175
}));
176
177
this._register(editor.onDidFocusEditorWidget(() => {
178
this._updateBracketsSoon.schedule();
179
}));
180
}
181
182
public jumpToBracket(): void {
183
if (!this._editor.hasModel()) {
184
return;
185
}
186
187
const model = this._editor.getModel();
188
const newSelections = this._editor.getSelections().map(selection => {
189
const position = selection.getStartPosition();
190
191
// find matching brackets if position is on a bracket
192
const brackets = model.bracketPairs.matchBracket(position);
193
let newCursorPosition: Position | null = null;
194
if (brackets) {
195
if (brackets[0].containsPosition(position) && !brackets[1].containsPosition(position)) {
196
newCursorPosition = brackets[1].getStartPosition();
197
} else if (brackets[1].containsPosition(position)) {
198
newCursorPosition = brackets[0].getStartPosition();
199
}
200
} else {
201
// find the enclosing brackets if the position isn't on a matching bracket
202
const enclosingBrackets = model.bracketPairs.findEnclosingBrackets(position);
203
if (enclosingBrackets) {
204
newCursorPosition = enclosingBrackets[1].getStartPosition();
205
} else {
206
// no enclosing brackets, try the very first next bracket
207
const nextBracket = model.bracketPairs.findNextBracket(position);
208
if (nextBracket && nextBracket.range) {
209
newCursorPosition = nextBracket.range.getStartPosition();
210
}
211
}
212
}
213
214
if (newCursorPosition) {
215
return new Selection(newCursorPosition.lineNumber, newCursorPosition.column, newCursorPosition.lineNumber, newCursorPosition.column);
216
}
217
return new Selection(position.lineNumber, position.column, position.lineNumber, position.column);
218
});
219
220
this._editor.setSelections(newSelections);
221
this._editor.revealRange(newSelections[0]);
222
}
223
224
public selectToBracket(selectBrackets: boolean): void {
225
if (!this._editor.hasModel()) {
226
return;
227
}
228
229
const model = this._editor.getModel();
230
const newSelections: Selection[] = [];
231
232
this._editor.getSelections().forEach(selection => {
233
const position = selection.getStartPosition();
234
let brackets = model.bracketPairs.matchBracket(position);
235
236
if (!brackets) {
237
brackets = model.bracketPairs.findEnclosingBrackets(position);
238
if (!brackets) {
239
const nextBracket = model.bracketPairs.findNextBracket(position);
240
if (nextBracket && nextBracket.range) {
241
brackets = model.bracketPairs.matchBracket(nextBracket.range.getStartPosition());
242
}
243
}
244
}
245
246
let selectFrom: Position | null = null;
247
let selectTo: Position | null = null;
248
249
if (brackets) {
250
brackets.sort(Range.compareRangesUsingStarts);
251
const [open, close] = brackets;
252
selectFrom = selectBrackets ? open.getStartPosition() : open.getEndPosition();
253
selectTo = selectBrackets ? close.getEndPosition() : close.getStartPosition();
254
255
if (close.containsPosition(position)) {
256
// select backwards if the cursor was on the closing bracket
257
const tmp = selectFrom;
258
selectFrom = selectTo;
259
selectTo = tmp;
260
}
261
}
262
263
if (selectFrom && selectTo) {
264
newSelections.push(new Selection(selectFrom.lineNumber, selectFrom.column, selectTo.lineNumber, selectTo.column));
265
}
266
});
267
268
if (newSelections.length > 0) {
269
this._editor.setSelections(newSelections);
270
this._editor.revealRange(newSelections[0]);
271
}
272
}
273
public removeBrackets(editSource?: string): void {
274
if (!this._editor.hasModel()) {
275
return;
276
}
277
278
const model = this._editor.getModel();
279
this._editor.getSelections().forEach((selection) => {
280
const position = selection.getPosition();
281
282
let brackets = model.bracketPairs.matchBracket(position);
283
if (!brackets) {
284
brackets = model.bracketPairs.findEnclosingBrackets(position);
285
}
286
if (brackets) {
287
this._editor.pushUndoStop();
288
this._editor.executeEdits(
289
editSource,
290
[
291
{ range: brackets[0], text: '' },
292
{ range: brackets[1], text: '' }
293
]
294
);
295
this._editor.pushUndoStop();
296
}
297
});
298
}
299
300
private static readonly _DECORATION_OPTIONS_WITH_OVERVIEW_RULER = ModelDecorationOptions.register({
301
description: 'bracket-match-overview',
302
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
303
className: 'bracket-match',
304
inlineClassName: 'bracket-match-inline',
305
overviewRuler: {
306
color: themeColorFromId(overviewRulerBracketMatchForeground),
307
position: OverviewRulerLane.Center
308
}
309
});
310
311
private static readonly _DECORATION_OPTIONS_WITHOUT_OVERVIEW_RULER = ModelDecorationOptions.register({
312
description: 'bracket-match-no-overview',
313
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
314
className: 'bracket-match',
315
inlineClassName: 'bracket-match-inline'
316
});
317
318
private _updateBrackets(): void {
319
if (this._matchBrackets === 'never') {
320
return;
321
}
322
this._recomputeBrackets();
323
324
const newDecorations: IModelDeltaDecoration[] = [];
325
let newDecorationsLen = 0;
326
for (const bracketData of this._lastBracketsData) {
327
const brackets = bracketData.brackets;
328
if (brackets) {
329
newDecorations[newDecorationsLen++] = { range: brackets[0], options: bracketData.options };
330
newDecorations[newDecorationsLen++] = { range: brackets[1], options: bracketData.options };
331
}
332
}
333
334
this._decorations.set(newDecorations);
335
}
336
337
private _recomputeBrackets(): void {
338
if (!this._editor.hasModel() || !this._editor.hasWidgetFocus()) {
339
// no model or no focus => no brackets!
340
this._lastBracketsData = [];
341
this._lastVersionId = 0;
342
return;
343
}
344
345
const selections = this._editor.getSelections();
346
if (selections.length > 100) {
347
// no bracket matching for high numbers of selections
348
this._lastBracketsData = [];
349
this._lastVersionId = 0;
350
return;
351
}
352
353
const model = this._editor.getModel();
354
const versionId = model.getVersionId();
355
let previousData: BracketsData[] = [];
356
if (this._lastVersionId === versionId) {
357
// use the previous data only if the model is at the same version id
358
previousData = this._lastBracketsData;
359
}
360
361
const positions: Position[] = [];
362
let positionsLen = 0;
363
for (let i = 0, len = selections.length; i < len; i++) {
364
const selection = selections[i];
365
366
if (selection.isEmpty()) {
367
// will bracket match a cursor only if the selection is collapsed
368
positions[positionsLen++] = selection.getStartPosition();
369
}
370
}
371
372
// sort positions for `previousData` cache hits
373
if (positions.length > 1) {
374
positions.sort(Position.compare);
375
}
376
377
const newData: BracketsData[] = [];
378
let newDataLen = 0;
379
let previousIndex = 0;
380
const previousLen = previousData.length;
381
for (let i = 0, len = positions.length; i < len; i++) {
382
const position = positions[i];
383
384
while (previousIndex < previousLen && previousData[previousIndex].position.isBefore(position)) {
385
previousIndex++;
386
}
387
388
if (previousIndex < previousLen && previousData[previousIndex].position.equals(position)) {
389
newData[newDataLen++] = previousData[previousIndex];
390
} else {
391
let brackets = model.bracketPairs.matchBracket(position, 20 /* give at most 20ms to compute */);
392
let options = BracketMatchingController._DECORATION_OPTIONS_WITH_OVERVIEW_RULER;
393
if (!brackets && this._matchBrackets === 'always') {
394
brackets = model.bracketPairs.findEnclosingBrackets(position, 20 /* give at most 20ms to compute */);
395
options = BracketMatchingController._DECORATION_OPTIONS_WITHOUT_OVERVIEW_RULER;
396
}
397
newData[newDataLen++] = new BracketsData(position, brackets, options);
398
}
399
}
400
401
this._lastBracketsData = newData;
402
this._lastVersionId = versionId;
403
}
404
}
405
406
registerEditorContribution(BracketMatchingController.ID, BracketMatchingController, EditorContributionInstantiation.AfterFirstRender);
407
registerEditorAction(SelectToBracketAction);
408
registerEditorAction(JumpToBracketAction);
409
registerEditorAction(RemoveBracketsAction);
410
411
// Go to menu
412
MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, {
413
group: '5_infile_nav',
414
command: {
415
id: 'editor.action.jumpToBracket',
416
title: nls.localize({ key: 'miGoToBracket', comment: ['&& denotes a mnemonic'] }, "Go to &&Bracket")
417
},
418
order: 2
419
});
420
421
// Theming participant to ensure bracket-match color overrides bracket pair colorization
422
registerThemingParticipant((theme, collector) => {
423
const bracketMatchForeground = theme.getColor(editorBracketMatchForeground);
424
if (bracketMatchForeground) {
425
// Use higher specificity to override bracket pair colorization
426
// Apply color to inline class to avoid layout jumps
427
collector.addRule(`.monaco-editor .bracket-match-inline { color: ${bracketMatchForeground} !important; }`);
428
}
429
});
430
431