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