Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/bracketPairsTree.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 { Emitter } from '../../../../../base/common/event.js';
7
import { Disposable } from '../../../../../base/common/lifecycle.js';
8
import { Range } from '../../../core/range.js';
9
import { ITextModel } from '../../../model.js';
10
import { BracketInfo, BracketPairWithMinIndentationInfo, IFoundBracket } from '../../../textModelBracketPairs.js';
11
import { TextModel } from '../../textModel.js';
12
import { IModelContentChangedEvent, IModelTokensChangedEvent } from '../../../textModelEvents.js';
13
import { ResolvedLanguageConfiguration } from '../../../languages/languageConfigurationRegistry.js';
14
import { AstNode, AstNodeKind } from './ast.js';
15
import { TextEditInfo } from './beforeEditPositionMapper.js';
16
import { LanguageAgnosticBracketTokens } from './brackets.js';
17
import { Length, lengthAdd, lengthGreaterThanEqual, lengthLessThan, lengthLessThanEqual, lengthsToRange, lengthZero, positionToLength, toLength } from './length.js';
18
import { parseDocument } from './parser.js';
19
import { DenseKeyProvider } from './smallImmutableSet.js';
20
import { FastTokenizer, TextBufferTokenizer } from './tokenizer.js';
21
import { BackgroundTokenizationState } from '../../../tokenizationTextModelPart.js';
22
import { Position } from '../../../core/position.js';
23
import { CallbackIterable } from '../../../../../base/common/arrays.js';
24
import { combineTextEditInfos } from './combineTextEditInfos.js';
25
import { ClosingBracketKind, OpeningBracketKind } from '../../../languages/supports/languageBracketsConfiguration.js';
26
27
export class BracketPairsTree extends Disposable {
28
private readonly didChangeEmitter;
29
30
/*
31
There are two trees:
32
* The initial tree that has no token information and is used for performant initial bracket colorization.
33
* The tree that used token information to detect bracket pairs.
34
35
To prevent flickering, we only switch from the initial tree to tree with token information
36
when tokenization completes.
37
Since the text can be edited while background tokenization is in progress, we need to update both trees.
38
*/
39
private initialAstWithoutTokens: AstNode | undefined;
40
private astWithTokens: AstNode | undefined;
41
42
private readonly denseKeyProvider;
43
private readonly brackets;
44
45
public didLanguageChange(languageId: string): boolean {
46
return this.brackets.didLanguageChange(languageId);
47
}
48
49
public readonly onDidChange;
50
private queuedTextEditsForInitialAstWithoutTokens: TextEditInfo[];
51
private queuedTextEdits: TextEditInfo[];
52
53
public constructor(
54
private readonly textModel: TextModel,
55
private readonly getLanguageConfiguration: (languageId: string) => ResolvedLanguageConfiguration
56
) {
57
super();
58
this.didChangeEmitter = new Emitter<void>();
59
this.denseKeyProvider = new DenseKeyProvider<string>();
60
this.brackets = new LanguageAgnosticBracketTokens(this.denseKeyProvider, this.getLanguageConfiguration);
61
this.onDidChange = this.didChangeEmitter.event;
62
this.queuedTextEditsForInitialAstWithoutTokens = [];
63
this.queuedTextEdits = [];
64
65
if (!textModel.tokenization.hasTokens) {
66
const brackets = this.brackets.getSingleLanguageBracketTokens(this.textModel.getLanguageId());
67
const tokenizer = new FastTokenizer(this.textModel.getValue(), brackets);
68
this.initialAstWithoutTokens = parseDocument(tokenizer, [], undefined, true);
69
this.astWithTokens = this.initialAstWithoutTokens;
70
} else if (textModel.tokenization.backgroundTokenizationState === BackgroundTokenizationState.Completed) {
71
// Skip the initial ast, as there is no flickering.
72
// Directly create the tree with token information.
73
this.initialAstWithoutTokens = undefined;
74
this.astWithTokens = this.parseDocumentFromTextBuffer([], undefined, false);
75
} else {
76
// We missed some token changes already, so we cannot use the fast tokenizer + delta increments
77
this.initialAstWithoutTokens = this.parseDocumentFromTextBuffer([], undefined, true);
78
this.astWithTokens = this.initialAstWithoutTokens;
79
}
80
}
81
82
//#region TextModel events
83
84
public handleDidChangeBackgroundTokenizationState(): void {
85
if (this.textModel.tokenization.backgroundTokenizationState === BackgroundTokenizationState.Completed) {
86
const wasUndefined = this.initialAstWithoutTokens === undefined;
87
// Clear the initial tree as we can use the tree with token information now.
88
this.initialAstWithoutTokens = undefined;
89
if (!wasUndefined) {
90
this.didChangeEmitter.fire();
91
}
92
}
93
}
94
95
public handleDidChangeTokens({ ranges }: IModelTokensChangedEvent): void {
96
const edits = ranges.map(r =>
97
new TextEditInfo(
98
toLength(r.fromLineNumber - 1, 0),
99
toLength(r.toLineNumber, 0),
100
toLength(r.toLineNumber - r.fromLineNumber + 1, 0)
101
)
102
);
103
104
this.handleEdits(edits, true);
105
106
if (!this.initialAstWithoutTokens) {
107
this.didChangeEmitter.fire();
108
}
109
}
110
111
public handleContentChanged(change: IModelContentChangedEvent) {
112
const edits = TextEditInfo.fromModelContentChanges(change.changes);
113
this.handleEdits(edits, false);
114
}
115
116
private handleEdits(edits: TextEditInfo[], tokenChange: boolean): void {
117
// Lazily queue the edits and only apply them when the tree is accessed.
118
const result = combineTextEditInfos(this.queuedTextEdits, edits);
119
120
this.queuedTextEdits = result;
121
if (this.initialAstWithoutTokens && !tokenChange) {
122
this.queuedTextEditsForInitialAstWithoutTokens = combineTextEditInfos(this.queuedTextEditsForInitialAstWithoutTokens, edits);
123
}
124
}
125
126
//#endregion
127
128
private flushQueue() {
129
if (this.queuedTextEdits.length > 0) {
130
this.astWithTokens = this.parseDocumentFromTextBuffer(this.queuedTextEdits, this.astWithTokens, false);
131
this.queuedTextEdits = [];
132
}
133
if (this.queuedTextEditsForInitialAstWithoutTokens.length > 0) {
134
if (this.initialAstWithoutTokens) {
135
this.initialAstWithoutTokens = this.parseDocumentFromTextBuffer(this.queuedTextEditsForInitialAstWithoutTokens, this.initialAstWithoutTokens, false);
136
}
137
this.queuedTextEditsForInitialAstWithoutTokens = [];
138
}
139
}
140
141
/**
142
* @pure (only if isPure = true)
143
*/
144
private parseDocumentFromTextBuffer(edits: TextEditInfo[], previousAst: AstNode | undefined, immutable: boolean): AstNode {
145
// Is much faster if `isPure = false`.
146
const isPure = false;
147
const previousAstClone = isPure ? previousAst?.deepClone() : previousAst;
148
const tokenizer = new TextBufferTokenizer(this.textModel, this.brackets);
149
const result = parseDocument(tokenizer, edits, previousAstClone, immutable);
150
return result;
151
}
152
153
public getBracketsInRange(range: Range, onlyColorizedBrackets: boolean): CallbackIterable<BracketInfo> {
154
this.flushQueue();
155
156
const startOffset = toLength(range.startLineNumber - 1, range.startColumn - 1);
157
const endOffset = toLength(range.endLineNumber - 1, range.endColumn - 1);
158
return new CallbackIterable(cb => {
159
const node = this.initialAstWithoutTokens || this.astWithTokens!;
160
collectBrackets(node, lengthZero, node.length, startOffset, endOffset, cb, 0, 0, new Map(), onlyColorizedBrackets);
161
});
162
}
163
164
public getBracketPairsInRange(range: Range, includeMinIndentation: boolean): CallbackIterable<BracketPairWithMinIndentationInfo> {
165
this.flushQueue();
166
167
const startLength = positionToLength(range.getStartPosition());
168
const endLength = positionToLength(range.getEndPosition());
169
170
return new CallbackIterable(cb => {
171
const node = this.initialAstWithoutTokens || this.astWithTokens!;
172
const context = new CollectBracketPairsContext(cb, includeMinIndentation, this.textModel);
173
collectBracketPairs(node, lengthZero, node.length, startLength, endLength, context, 0, new Map());
174
});
175
}
176
177
public getFirstBracketAfter(position: Position): IFoundBracket | null {
178
this.flushQueue();
179
180
const node = this.initialAstWithoutTokens || this.astWithTokens!;
181
return getFirstBracketAfter(node, lengthZero, node.length, positionToLength(position));
182
}
183
184
public getFirstBracketBefore(position: Position): IFoundBracket | null {
185
this.flushQueue();
186
187
const node = this.initialAstWithoutTokens || this.astWithTokens!;
188
return getFirstBracketBefore(node, lengthZero, node.length, positionToLength(position));
189
}
190
}
191
192
function getFirstBracketBefore(node: AstNode, nodeOffsetStart: Length, nodeOffsetEnd: Length, position: Length): IFoundBracket | null {
193
if (node.kind === AstNodeKind.List || node.kind === AstNodeKind.Pair) {
194
const lengths: { nodeOffsetStart: Length; nodeOffsetEnd: Length }[] = [];
195
for (const child of node.children) {
196
nodeOffsetEnd = lengthAdd(nodeOffsetStart, child.length);
197
lengths.push({ nodeOffsetStart, nodeOffsetEnd });
198
nodeOffsetStart = nodeOffsetEnd;
199
}
200
for (let i = lengths.length - 1; i >= 0; i--) {
201
const { nodeOffsetStart, nodeOffsetEnd } = lengths[i];
202
if (lengthLessThan(nodeOffsetStart, position)) {
203
const result = getFirstBracketBefore(node.children[i], nodeOffsetStart, nodeOffsetEnd, position);
204
if (result) {
205
return result;
206
}
207
}
208
}
209
return null;
210
} else if (node.kind === AstNodeKind.UnexpectedClosingBracket) {
211
return null;
212
} else if (node.kind === AstNodeKind.Bracket) {
213
const range = lengthsToRange(nodeOffsetStart, nodeOffsetEnd);
214
return {
215
bracketInfo: node.bracketInfo,
216
range
217
};
218
}
219
return null;
220
}
221
222
function getFirstBracketAfter(node: AstNode, nodeOffsetStart: Length, nodeOffsetEnd: Length, position: Length): IFoundBracket | null {
223
if (node.kind === AstNodeKind.List || node.kind === AstNodeKind.Pair) {
224
for (const child of node.children) {
225
nodeOffsetEnd = lengthAdd(nodeOffsetStart, child.length);
226
if (lengthLessThan(position, nodeOffsetEnd)) {
227
const result = getFirstBracketAfter(child, nodeOffsetStart, nodeOffsetEnd, position);
228
if (result) {
229
return result;
230
}
231
}
232
nodeOffsetStart = nodeOffsetEnd;
233
}
234
return null;
235
} else if (node.kind === AstNodeKind.UnexpectedClosingBracket) {
236
return null;
237
} else if (node.kind === AstNodeKind.Bracket) {
238
const range = lengthsToRange(nodeOffsetStart, nodeOffsetEnd);
239
return {
240
bracketInfo: node.bracketInfo,
241
range
242
};
243
}
244
return null;
245
}
246
247
function collectBrackets(
248
node: AstNode,
249
nodeOffsetStart: Length,
250
nodeOffsetEnd: Length,
251
startOffset: Length,
252
endOffset: Length,
253
push: (item: BracketInfo) => boolean,
254
level: number,
255
nestingLevelOfEqualBracketType: number,
256
levelPerBracketType: Map<string, number>,
257
onlyColorizedBrackets: boolean,
258
parentPairIsIncomplete: boolean = false,
259
): boolean {
260
if (level > 200) {
261
return true;
262
}
263
264
whileLoop:
265
while (true) {
266
switch (node.kind) {
267
case AstNodeKind.List: {
268
const childCount = node.childrenLength;
269
for (let i = 0; i < childCount; i++) {
270
const child = node.getChild(i);
271
if (!child) {
272
continue;
273
}
274
nodeOffsetEnd = lengthAdd(nodeOffsetStart, child.length);
275
if (
276
lengthLessThanEqual(nodeOffsetStart, endOffset) &&
277
lengthGreaterThanEqual(nodeOffsetEnd, startOffset)
278
) {
279
const childEndsAfterEnd = lengthGreaterThanEqual(nodeOffsetEnd, endOffset);
280
if (childEndsAfterEnd) {
281
// No child after this child in the requested window, don't recurse
282
node = child;
283
continue whileLoop;
284
}
285
286
const shouldContinue = collectBrackets(child, nodeOffsetStart, nodeOffsetEnd, startOffset, endOffset, push, level, 0, levelPerBracketType, onlyColorizedBrackets);
287
if (!shouldContinue) {
288
return false;
289
}
290
}
291
nodeOffsetStart = nodeOffsetEnd;
292
}
293
return true;
294
}
295
case AstNodeKind.Pair: {
296
const colorize = !onlyColorizedBrackets || !node.closingBracket || (node.closingBracket.bracketInfo as ClosingBracketKind).closesColorized(node.openingBracket.bracketInfo as OpeningBracketKind);
297
298
let levelPerBracket = 0;
299
if (levelPerBracketType) {
300
let existing = levelPerBracketType.get(node.openingBracket.text);
301
if (existing === undefined) {
302
existing = 0;
303
}
304
levelPerBracket = existing;
305
if (colorize) {
306
existing++;
307
levelPerBracketType.set(node.openingBracket.text, existing);
308
}
309
}
310
311
const childCount = node.childrenLength;
312
for (let i = 0; i < childCount; i++) {
313
const child = node.getChild(i);
314
if (!child) {
315
continue;
316
}
317
nodeOffsetEnd = lengthAdd(nodeOffsetStart, child.length);
318
if (
319
lengthLessThanEqual(nodeOffsetStart, endOffset) &&
320
lengthGreaterThanEqual(nodeOffsetEnd, startOffset)
321
) {
322
const childEndsAfterEnd = lengthGreaterThanEqual(nodeOffsetEnd, endOffset);
323
if (childEndsAfterEnd && child.kind !== AstNodeKind.Bracket) {
324
// No child after this child in the requested window, don't recurse
325
// Don't do this for brackets because of unclosed/unopened brackets
326
node = child;
327
if (colorize) {
328
level++;
329
nestingLevelOfEqualBracketType = levelPerBracket + 1;
330
} else {
331
nestingLevelOfEqualBracketType = levelPerBracket;
332
}
333
continue whileLoop;
334
}
335
336
if (colorize || child.kind !== AstNodeKind.Bracket || !node.closingBracket) {
337
const shouldContinue = collectBrackets(
338
child,
339
nodeOffsetStart,
340
nodeOffsetEnd,
341
startOffset,
342
endOffset,
343
push,
344
colorize ? level + 1 : level,
345
colorize ? levelPerBracket + 1 : levelPerBracket,
346
levelPerBracketType,
347
onlyColorizedBrackets,
348
!node.closingBracket,
349
);
350
if (!shouldContinue) {
351
return false;
352
}
353
}
354
}
355
nodeOffsetStart = nodeOffsetEnd;
356
}
357
358
levelPerBracketType?.set(node.openingBracket.text, levelPerBracket);
359
360
return true;
361
}
362
case AstNodeKind.UnexpectedClosingBracket: {
363
const range = lengthsToRange(nodeOffsetStart, nodeOffsetEnd);
364
return push(new BracketInfo(range, level - 1, 0, true));
365
}
366
case AstNodeKind.Bracket: {
367
const range = lengthsToRange(nodeOffsetStart, nodeOffsetEnd);
368
return push(new BracketInfo(range, level - 1, nestingLevelOfEqualBracketType - 1, parentPairIsIncomplete));
369
}
370
case AstNodeKind.Text:
371
return true;
372
}
373
}
374
}
375
376
class CollectBracketPairsContext {
377
constructor(
378
public readonly push: (item: BracketPairWithMinIndentationInfo) => boolean,
379
public readonly includeMinIndentation: boolean,
380
public readonly textModel: ITextModel,
381
) {
382
}
383
}
384
385
function collectBracketPairs(
386
node: AstNode,
387
nodeOffsetStart: Length,
388
nodeOffsetEnd: Length,
389
startOffset: Length,
390
endOffset: Length,
391
context: CollectBracketPairsContext,
392
level: number,
393
levelPerBracketType: Map<string, number>
394
): boolean {
395
if (level > 200) {
396
return true;
397
}
398
399
let shouldContinue = true;
400
401
if (node.kind === AstNodeKind.Pair) {
402
let levelPerBracket = 0;
403
if (levelPerBracketType) {
404
let existing = levelPerBracketType.get(node.openingBracket.text);
405
if (existing === undefined) {
406
existing = 0;
407
}
408
levelPerBracket = existing;
409
existing++;
410
levelPerBracketType.set(node.openingBracket.text, existing);
411
}
412
413
const openingBracketEnd = lengthAdd(nodeOffsetStart, node.openingBracket.length);
414
let minIndentation = -1;
415
if (context.includeMinIndentation) {
416
minIndentation = node.computeMinIndentation(
417
nodeOffsetStart,
418
context.textModel
419
);
420
}
421
422
shouldContinue = context.push(
423
new BracketPairWithMinIndentationInfo(
424
lengthsToRange(nodeOffsetStart, nodeOffsetEnd),
425
lengthsToRange(nodeOffsetStart, openingBracketEnd),
426
node.closingBracket
427
? lengthsToRange(
428
lengthAdd(openingBracketEnd, node.child?.length || lengthZero),
429
nodeOffsetEnd
430
)
431
: undefined,
432
level,
433
levelPerBracket,
434
node,
435
minIndentation
436
)
437
);
438
439
nodeOffsetStart = openingBracketEnd;
440
if (shouldContinue && node.child) {
441
const child = node.child;
442
nodeOffsetEnd = lengthAdd(nodeOffsetStart, child.length);
443
if (
444
lengthLessThanEqual(nodeOffsetStart, endOffset) &&
445
lengthGreaterThanEqual(nodeOffsetEnd, startOffset)
446
) {
447
shouldContinue = collectBracketPairs(
448
child,
449
nodeOffsetStart,
450
nodeOffsetEnd,
451
startOffset,
452
endOffset,
453
context,
454
level + 1,
455
levelPerBracketType
456
);
457
if (!shouldContinue) {
458
return false;
459
}
460
}
461
}
462
463
levelPerBracketType?.set(node.openingBracket.text, levelPerBracket);
464
} else {
465
let curOffset = nodeOffsetStart;
466
for (const child of node.children) {
467
const childOffset = curOffset;
468
curOffset = lengthAdd(curOffset, child.length);
469
470
if (
471
lengthLessThanEqual(childOffset, endOffset) &&
472
lengthLessThanEqual(startOffset, curOffset)
473
) {
474
shouldContinue = collectBracketPairs(
475
child,
476
childOffset,
477
curOffset,
478
startOffset,
479
endOffset,
480
context,
481
level,
482
levelPerBracketType
483
);
484
if (!shouldContinue) {
485
return false;
486
}
487
}
488
}
489
}
490
return shouldContinue;
491
}
492
493