Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsImpl.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 { CallbackIterable, compareBy } from '../../../../base/common/arrays.js';
7
import { Emitter } from '../../../../base/common/event.js';
8
import { Disposable, DisposableStore, IDisposable, IReference, MutableDisposable } from '../../../../base/common/lifecycle.js';
9
import { IPosition, Position } from '../../core/position.js';
10
import { Range } from '../../core/range.js';
11
import { ILanguageConfigurationService, LanguageConfigurationServiceChangeEvent } from '../../languages/languageConfigurationRegistry.js';
12
import { ignoreBracketsInToken } from '../../languages/supports.js';
13
import { LanguageBracketsConfiguration } from '../../languages/supports/languageBracketsConfiguration.js';
14
import { BracketsUtils, RichEditBracket, RichEditBrackets } from '../../languages/supports/richEditBrackets.js';
15
import { BracketPairsTree } from './bracketPairsTree/bracketPairsTree.js';
16
import { TextModel } from '../textModel.js';
17
import { BracketInfo, BracketPairInfo, BracketPairWithMinIndentationInfo, IBracketPairsTextModelPart, IFoundBracket } from '../../textModelBracketPairs.js';
18
import { IModelContentChangedEvent, IModelLanguageChangedEvent, IModelOptionsChangedEvent, IModelTokensChangedEvent } from '../../textModelEvents.js';
19
import { LineTokens } from '../../tokens/lineTokens.js';
20
21
export class BracketPairsTextModelPart extends Disposable implements IBracketPairsTextModelPart {
22
private readonly bracketPairsTree = this._register(new MutableDisposable<IReference<BracketPairsTree>>());
23
24
private readonly onDidChangeEmitter = new Emitter<void>();
25
public readonly onDidChange = this.onDidChangeEmitter.event;
26
27
private get canBuildAST() {
28
const maxSupportedDocumentLength = /* max lines */ 50_000 * /* average column count */ 100;
29
return this.textModel.getValueLength() <= maxSupportedDocumentLength;
30
}
31
32
private bracketsRequested = false;
33
34
public constructor(
35
private readonly textModel: TextModel,
36
private readonly languageConfigurationService: ILanguageConfigurationService
37
) {
38
super();
39
}
40
41
//#region TextModel events
42
43
public handleLanguageConfigurationServiceChange(e: LanguageConfigurationServiceChangeEvent): void {
44
if (!e.languageId || this.bracketPairsTree.value?.object.didLanguageChange(e.languageId)) {
45
this.bracketPairsTree.clear();
46
this.updateBracketPairsTree();
47
}
48
}
49
50
public handleDidChangeOptions(e: IModelOptionsChangedEvent): void {
51
this.bracketPairsTree.clear();
52
this.updateBracketPairsTree();
53
}
54
55
public handleDidChangeLanguage(e: IModelLanguageChangedEvent): void {
56
this.bracketPairsTree.clear();
57
this.updateBracketPairsTree();
58
}
59
60
public handleDidChangeContent(change: IModelContentChangedEvent) {
61
this.bracketPairsTree.value?.object.handleContentChanged(change);
62
}
63
64
public handleDidChangeBackgroundTokenizationState(): void {
65
this.bracketPairsTree.value?.object.handleDidChangeBackgroundTokenizationState();
66
}
67
68
public handleDidChangeTokens(e: IModelTokensChangedEvent): void {
69
this.bracketPairsTree.value?.object.handleDidChangeTokens(e);
70
}
71
72
//#endregion
73
74
private updateBracketPairsTree() {
75
if (this.bracketsRequested && this.canBuildAST) {
76
if (!this.bracketPairsTree.value) {
77
const store = new DisposableStore();
78
79
this.bracketPairsTree.value = createDisposableRef(
80
store.add(
81
new BracketPairsTree(this.textModel, (languageId) => {
82
return this.languageConfigurationService.getLanguageConfiguration(languageId);
83
})
84
),
85
store
86
);
87
store.add(this.bracketPairsTree.value.object.onDidChange(e => this.onDidChangeEmitter.fire(e)));
88
this.onDidChangeEmitter.fire();
89
}
90
} else {
91
if (this.bracketPairsTree.value) {
92
this.bracketPairsTree.clear();
93
// Important: Don't call fire if there was no change!
94
this.onDidChangeEmitter.fire();
95
}
96
}
97
}
98
99
/**
100
* Returns all bracket pairs that intersect the given range.
101
* The result is sorted by the start position.
102
*/
103
public getBracketPairsInRange(range: Range): CallbackIterable<BracketPairInfo> {
104
this.bracketsRequested = true;
105
this.updateBracketPairsTree();
106
return this.bracketPairsTree.value?.object.getBracketPairsInRange(range, false) || CallbackIterable.empty;
107
}
108
109
public getBracketPairsInRangeWithMinIndentation(range: Range): CallbackIterable<BracketPairWithMinIndentationInfo> {
110
this.bracketsRequested = true;
111
this.updateBracketPairsTree();
112
return this.bracketPairsTree.value?.object.getBracketPairsInRange(range, true) || CallbackIterable.empty;
113
}
114
115
public getBracketsInRange(range: Range, onlyColorizedBrackets: boolean = false): CallbackIterable<BracketInfo> {
116
this.bracketsRequested = true;
117
this.updateBracketPairsTree();
118
return this.bracketPairsTree.value?.object.getBracketsInRange(range, onlyColorizedBrackets) || CallbackIterable.empty;
119
}
120
121
public findMatchingBracketUp(_bracket: string, _position: IPosition, maxDuration?: number): Range | null {
122
const position = this.textModel.validatePosition(_position);
123
const languageId = this.textModel.getLanguageIdAtPosition(position.lineNumber, position.column);
124
125
if (this.canBuildAST) {
126
const closingBracketInfo = this.languageConfigurationService
127
.getLanguageConfiguration(languageId)
128
.bracketsNew.getClosingBracketInfo(_bracket);
129
130
if (!closingBracketInfo) {
131
return null;
132
}
133
134
const bracketPair = this.getBracketPairsInRange(Range.fromPositions(_position, _position)).findLast((b) =>
135
closingBracketInfo.closes(b.openingBracketInfo)
136
);
137
138
if (bracketPair) {
139
return bracketPair.openingBracketRange;
140
}
141
return null;
142
} else {
143
// Fallback to old bracket matching code:
144
const bracket = _bracket.toLowerCase();
145
146
const bracketsSupport = this.languageConfigurationService.getLanguageConfiguration(languageId).brackets;
147
148
if (!bracketsSupport) {
149
return null;
150
}
151
152
const data = bracketsSupport.textIsBracket[bracket];
153
154
if (!data) {
155
return null;
156
}
157
158
return stripBracketSearchCanceled(this._findMatchingBracketUp(data, position, createTimeBasedContinueBracketSearchPredicate(maxDuration)));
159
}
160
}
161
162
public matchBracket(position: IPosition, maxDuration?: number): [Range, Range] | null {
163
if (this.canBuildAST) {
164
const bracketPair =
165
this.getBracketPairsInRange(
166
Range.fromPositions(position, position)
167
).filter(
168
(item) =>
169
item.closingBracketRange !== undefined &&
170
(item.openingBracketRange.containsPosition(position) ||
171
item.closingBracketRange.containsPosition(position))
172
).findLastMaxBy(
173
compareBy(
174
(item) =>
175
item.openingBracketRange.containsPosition(position)
176
? item.openingBracketRange
177
: item.closingBracketRange,
178
Range.compareRangesUsingStarts
179
)
180
);
181
if (bracketPair) {
182
return [bracketPair.openingBracketRange, bracketPair.closingBracketRange!];
183
}
184
return null;
185
} else {
186
// Fallback to old bracket matching code:
187
const continueSearchPredicate = createTimeBasedContinueBracketSearchPredicate(maxDuration);
188
return this._matchBracket(this.textModel.validatePosition(position), continueSearchPredicate);
189
}
190
}
191
192
private _establishBracketSearchOffsets(position: Position, lineTokens: LineTokens, modeBrackets: RichEditBrackets, tokenIndex: number) {
193
const tokenCount = lineTokens.getCount();
194
const currentLanguageId = lineTokens.getLanguageId(tokenIndex);
195
196
// limit search to not go before `maxBracketLength`
197
let searchStartOffset = Math.max(0, position.column - 1 - modeBrackets.maxBracketLength);
198
for (let i = tokenIndex - 1; i >= 0; i--) {
199
const tokenEndOffset = lineTokens.getEndOffset(i);
200
if (tokenEndOffset <= searchStartOffset) {
201
break;
202
}
203
if (ignoreBracketsInToken(lineTokens.getStandardTokenType(i)) || lineTokens.getLanguageId(i) !== currentLanguageId) {
204
searchStartOffset = tokenEndOffset;
205
break;
206
}
207
}
208
209
// limit search to not go after `maxBracketLength`
210
let searchEndOffset = Math.min(lineTokens.getLineContent().length, position.column - 1 + modeBrackets.maxBracketLength);
211
for (let i = tokenIndex + 1; i < tokenCount; i++) {
212
const tokenStartOffset = lineTokens.getStartOffset(i);
213
if (tokenStartOffset >= searchEndOffset) {
214
break;
215
}
216
if (ignoreBracketsInToken(lineTokens.getStandardTokenType(i)) || lineTokens.getLanguageId(i) !== currentLanguageId) {
217
searchEndOffset = tokenStartOffset;
218
break;
219
}
220
}
221
222
return { searchStartOffset, searchEndOffset };
223
}
224
225
private _matchBracket(position: Position, continueSearchPredicate: ContinueBracketSearchPredicate): [Range, Range] | null {
226
const lineNumber = position.lineNumber;
227
const lineTokens = this.textModel.tokenization.getLineTokens(lineNumber);
228
const lineText = this.textModel.getLineContent(lineNumber);
229
230
const tokenIndex = lineTokens.findTokenIndexAtOffset(position.column - 1);
231
if (tokenIndex < 0) {
232
return null;
233
}
234
const currentModeBrackets = this.languageConfigurationService.getLanguageConfiguration(lineTokens.getLanguageId(tokenIndex)).brackets;
235
236
// check that the token is not to be ignored
237
if (currentModeBrackets && !ignoreBracketsInToken(lineTokens.getStandardTokenType(tokenIndex))) {
238
239
let { searchStartOffset, searchEndOffset } = this._establishBracketSearchOffsets(position, lineTokens, currentModeBrackets, tokenIndex);
240
241
// it might be the case that [currentTokenStart -> currentTokenEnd] contains multiple brackets
242
// `bestResult` will contain the most right-side result
243
let bestResult: [Range, Range] | null = null;
244
while (true) {
245
const foundBracket = BracketsUtils.findNextBracketInRange(currentModeBrackets.forwardRegex, lineNumber, lineText, searchStartOffset, searchEndOffset);
246
if (!foundBracket) {
247
// there are no more brackets in this text
248
break;
249
}
250
251
// check that we didn't hit a bracket too far away from position
252
if (foundBracket.startColumn <= position.column && position.column <= foundBracket.endColumn) {
253
const foundBracketText = lineText.substring(foundBracket.startColumn - 1, foundBracket.endColumn - 1).toLowerCase();
254
const r = this._matchFoundBracket(foundBracket, currentModeBrackets.textIsBracket[foundBracketText], currentModeBrackets.textIsOpenBracket[foundBracketText], continueSearchPredicate);
255
if (r) {
256
if (r instanceof BracketSearchCanceled) {
257
return null;
258
}
259
bestResult = r;
260
}
261
}
262
263
searchStartOffset = foundBracket.endColumn - 1;
264
}
265
266
if (bestResult) {
267
return bestResult;
268
}
269
}
270
271
// If position is in between two tokens, try also looking in the previous token
272
if (tokenIndex > 0 && lineTokens.getStartOffset(tokenIndex) === position.column - 1) {
273
const prevTokenIndex = tokenIndex - 1;
274
const prevModeBrackets = this.languageConfigurationService.getLanguageConfiguration(lineTokens.getLanguageId(prevTokenIndex)).brackets;
275
276
// check that previous token is not to be ignored
277
if (prevModeBrackets && !ignoreBracketsInToken(lineTokens.getStandardTokenType(prevTokenIndex))) {
278
279
const { searchStartOffset, searchEndOffset } = this._establishBracketSearchOffsets(position, lineTokens, prevModeBrackets, prevTokenIndex);
280
281
const foundBracket = BracketsUtils.findPrevBracketInRange(prevModeBrackets.reversedRegex, lineNumber, lineText, searchStartOffset, searchEndOffset);
282
283
// check that we didn't hit a bracket too far away from position
284
if (foundBracket && foundBracket.startColumn <= position.column && position.column <= foundBracket.endColumn) {
285
const foundBracketText = lineText.substring(foundBracket.startColumn - 1, foundBracket.endColumn - 1).toLowerCase();
286
const r = this._matchFoundBracket(foundBracket, prevModeBrackets.textIsBracket[foundBracketText], prevModeBrackets.textIsOpenBracket[foundBracketText], continueSearchPredicate);
287
if (r) {
288
if (r instanceof BracketSearchCanceled) {
289
return null;
290
}
291
return r;
292
}
293
}
294
}
295
}
296
297
return null;
298
}
299
300
private _matchFoundBracket(foundBracket: Range, data: RichEditBracket, isOpen: boolean, continueSearchPredicate: ContinueBracketSearchPredicate): [Range, Range] | null | BracketSearchCanceled {
301
if (!data) {
302
return null;
303
}
304
305
const matched = (
306
isOpen
307
? this._findMatchingBracketDown(data, foundBracket.getEndPosition(), continueSearchPredicate)
308
: this._findMatchingBracketUp(data, foundBracket.getStartPosition(), continueSearchPredicate)
309
);
310
311
if (!matched) {
312
return null;
313
}
314
315
if (matched instanceof BracketSearchCanceled) {
316
return matched;
317
}
318
319
return [foundBracket, matched];
320
}
321
322
private _findMatchingBracketUp(bracket: RichEditBracket, position: Position, continueSearchPredicate: ContinueBracketSearchPredicate): Range | null | BracketSearchCanceled {
323
// console.log('_findMatchingBracketUp: ', 'bracket: ', JSON.stringify(bracket), 'startPosition: ', String(position));
324
325
const languageId = bracket.languageId;
326
const reversedBracketRegex = bracket.reversedRegex;
327
let count = -1;
328
329
let totalCallCount = 0;
330
const searchPrevMatchingBracketInRange = (lineNumber: number, lineText: string, searchStartOffset: number, searchEndOffset: number): Range | null | BracketSearchCanceled => {
331
while (true) {
332
if (continueSearchPredicate && (++totalCallCount) % 100 === 0 && !continueSearchPredicate()) {
333
return BracketSearchCanceled.INSTANCE;
334
}
335
const r = BracketsUtils.findPrevBracketInRange(reversedBracketRegex, lineNumber, lineText, searchStartOffset, searchEndOffset);
336
if (!r) {
337
break;
338
}
339
340
const hitText = lineText.substring(r.startColumn - 1, r.endColumn - 1).toLowerCase();
341
if (bracket.isOpen(hitText)) {
342
count++;
343
} else if (bracket.isClose(hitText)) {
344
count--;
345
}
346
347
if (count === 0) {
348
return r;
349
}
350
351
searchEndOffset = r.startColumn - 1;
352
}
353
354
return null;
355
};
356
357
for (let lineNumber = position.lineNumber; lineNumber >= 1; lineNumber--) {
358
const lineTokens = this.textModel.tokenization.getLineTokens(lineNumber);
359
const tokenCount = lineTokens.getCount();
360
const lineText = this.textModel.getLineContent(lineNumber);
361
362
let tokenIndex = tokenCount - 1;
363
let searchStartOffset = lineText.length;
364
let searchEndOffset = lineText.length;
365
if (lineNumber === position.lineNumber) {
366
tokenIndex = lineTokens.findTokenIndexAtOffset(position.column - 1);
367
searchStartOffset = position.column - 1;
368
searchEndOffset = position.column - 1;
369
}
370
371
let prevSearchInToken = true;
372
for (; tokenIndex >= 0; tokenIndex--) {
373
const searchInToken = (lineTokens.getLanguageId(tokenIndex) === languageId && !ignoreBracketsInToken(lineTokens.getStandardTokenType(tokenIndex)));
374
375
if (searchInToken) {
376
// this token should be searched
377
if (prevSearchInToken) {
378
// the previous token should be searched, simply extend searchStartOffset
379
searchStartOffset = lineTokens.getStartOffset(tokenIndex);
380
} else {
381
// the previous token should not be searched
382
searchStartOffset = lineTokens.getStartOffset(tokenIndex);
383
searchEndOffset = lineTokens.getEndOffset(tokenIndex);
384
}
385
} else {
386
// this token should not be searched
387
if (prevSearchInToken && searchStartOffset !== searchEndOffset) {
388
const r = searchPrevMatchingBracketInRange(lineNumber, lineText, searchStartOffset, searchEndOffset);
389
if (r) {
390
return r;
391
}
392
}
393
}
394
395
prevSearchInToken = searchInToken;
396
}
397
398
if (prevSearchInToken && searchStartOffset !== searchEndOffset) {
399
const r = searchPrevMatchingBracketInRange(lineNumber, lineText, searchStartOffset, searchEndOffset);
400
if (r) {
401
return r;
402
}
403
}
404
}
405
406
return null;
407
}
408
409
private _findMatchingBracketDown(bracket: RichEditBracket, position: Position, continueSearchPredicate: ContinueBracketSearchPredicate): Range | null | BracketSearchCanceled {
410
// console.log('_findMatchingBracketDown: ', 'bracket: ', JSON.stringify(bracket), 'startPosition: ', String(position));
411
412
const languageId = bracket.languageId;
413
const bracketRegex = bracket.forwardRegex;
414
let count = 1;
415
416
let totalCallCount = 0;
417
const searchNextMatchingBracketInRange = (lineNumber: number, lineText: string, searchStartOffset: number, searchEndOffset: number): Range | null | BracketSearchCanceled => {
418
while (true) {
419
if (continueSearchPredicate && (++totalCallCount) % 100 === 0 && !continueSearchPredicate()) {
420
return BracketSearchCanceled.INSTANCE;
421
}
422
const r = BracketsUtils.findNextBracketInRange(bracketRegex, lineNumber, lineText, searchStartOffset, searchEndOffset);
423
if (!r) {
424
break;
425
}
426
427
const hitText = lineText.substring(r.startColumn - 1, r.endColumn - 1).toLowerCase();
428
if (bracket.isOpen(hitText)) {
429
count++;
430
} else if (bracket.isClose(hitText)) {
431
count--;
432
}
433
434
if (count === 0) {
435
return r;
436
}
437
438
searchStartOffset = r.endColumn - 1;
439
}
440
441
return null;
442
};
443
444
const lineCount = this.textModel.getLineCount();
445
for (let lineNumber = position.lineNumber; lineNumber <= lineCount; lineNumber++) {
446
const lineTokens = this.textModel.tokenization.getLineTokens(lineNumber);
447
const tokenCount = lineTokens.getCount();
448
const lineText = this.textModel.getLineContent(lineNumber);
449
450
let tokenIndex = 0;
451
let searchStartOffset = 0;
452
let searchEndOffset = 0;
453
if (lineNumber === position.lineNumber) {
454
tokenIndex = lineTokens.findTokenIndexAtOffset(position.column - 1);
455
searchStartOffset = position.column - 1;
456
searchEndOffset = position.column - 1;
457
}
458
459
let prevSearchInToken = true;
460
for (; tokenIndex < tokenCount; tokenIndex++) {
461
const searchInToken = (lineTokens.getLanguageId(tokenIndex) === languageId && !ignoreBracketsInToken(lineTokens.getStandardTokenType(tokenIndex)));
462
463
if (searchInToken) {
464
// this token should be searched
465
if (prevSearchInToken) {
466
// the previous token should be searched, simply extend searchEndOffset
467
searchEndOffset = lineTokens.getEndOffset(tokenIndex);
468
} else {
469
// the previous token should not be searched
470
searchStartOffset = lineTokens.getStartOffset(tokenIndex);
471
searchEndOffset = lineTokens.getEndOffset(tokenIndex);
472
}
473
} else {
474
// this token should not be searched
475
if (prevSearchInToken && searchStartOffset !== searchEndOffset) {
476
const r = searchNextMatchingBracketInRange(lineNumber, lineText, searchStartOffset, searchEndOffset);
477
if (r) {
478
return r;
479
}
480
}
481
}
482
483
prevSearchInToken = searchInToken;
484
}
485
486
if (prevSearchInToken && searchStartOffset !== searchEndOffset) {
487
const r = searchNextMatchingBracketInRange(lineNumber, lineText, searchStartOffset, searchEndOffset);
488
if (r) {
489
return r;
490
}
491
}
492
}
493
494
return null;
495
}
496
497
public findPrevBracket(_position: IPosition): IFoundBracket | null {
498
const position = this.textModel.validatePosition(_position);
499
500
if (this.canBuildAST) {
501
this.bracketsRequested = true;
502
this.updateBracketPairsTree();
503
return this.bracketPairsTree.value?.object.getFirstBracketBefore(position) || null;
504
}
505
506
let languageId: string | null = null;
507
let modeBrackets: RichEditBrackets | null = null;
508
let bracketConfig: LanguageBracketsConfiguration | null = null;
509
for (let lineNumber = position.lineNumber; lineNumber >= 1; lineNumber--) {
510
const lineTokens = this.textModel.tokenization.getLineTokens(lineNumber);
511
const tokenCount = lineTokens.getCount();
512
const lineText = this.textModel.getLineContent(lineNumber);
513
514
let tokenIndex = tokenCount - 1;
515
let searchStartOffset = lineText.length;
516
let searchEndOffset = lineText.length;
517
if (lineNumber === position.lineNumber) {
518
tokenIndex = lineTokens.findTokenIndexAtOffset(position.column - 1);
519
searchStartOffset = position.column - 1;
520
searchEndOffset = position.column - 1;
521
const tokenLanguageId = lineTokens.getLanguageId(tokenIndex);
522
if (languageId !== tokenLanguageId) {
523
languageId = tokenLanguageId;
524
modeBrackets = this.languageConfigurationService.getLanguageConfiguration(languageId).brackets;
525
bracketConfig = this.languageConfigurationService.getLanguageConfiguration(languageId).bracketsNew;
526
}
527
}
528
529
let prevSearchInToken = true;
530
for (; tokenIndex >= 0; tokenIndex--) {
531
const tokenLanguageId = lineTokens.getLanguageId(tokenIndex);
532
533
if (languageId !== tokenLanguageId) {
534
// language id change!
535
if (modeBrackets && bracketConfig && prevSearchInToken && searchStartOffset !== searchEndOffset) {
536
const r = BracketsUtils.findPrevBracketInRange(modeBrackets.reversedRegex, lineNumber, lineText, searchStartOffset, searchEndOffset);
537
if (r) {
538
return this._toFoundBracket(bracketConfig, r);
539
}
540
prevSearchInToken = false;
541
}
542
languageId = tokenLanguageId;
543
modeBrackets = this.languageConfigurationService.getLanguageConfiguration(languageId).brackets;
544
bracketConfig = this.languageConfigurationService.getLanguageConfiguration(languageId).bracketsNew;
545
}
546
547
const searchInToken = (!!modeBrackets && !ignoreBracketsInToken(lineTokens.getStandardTokenType(tokenIndex)));
548
549
if (searchInToken) {
550
// this token should be searched
551
if (prevSearchInToken) {
552
// the previous token should be searched, simply extend searchStartOffset
553
searchStartOffset = lineTokens.getStartOffset(tokenIndex);
554
} else {
555
// the previous token should not be searched
556
searchStartOffset = lineTokens.getStartOffset(tokenIndex);
557
searchEndOffset = lineTokens.getEndOffset(tokenIndex);
558
}
559
} else {
560
// this token should not be searched
561
if (bracketConfig && modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) {
562
const r = BracketsUtils.findPrevBracketInRange(modeBrackets.reversedRegex, lineNumber, lineText, searchStartOffset, searchEndOffset);
563
if (r) {
564
return this._toFoundBracket(bracketConfig, r);
565
}
566
}
567
}
568
569
prevSearchInToken = searchInToken;
570
}
571
572
if (bracketConfig && modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) {
573
const r = BracketsUtils.findPrevBracketInRange(modeBrackets.reversedRegex, lineNumber, lineText, searchStartOffset, searchEndOffset);
574
if (r) {
575
return this._toFoundBracket(bracketConfig, r);
576
}
577
}
578
}
579
580
return null;
581
}
582
583
public findNextBracket(_position: IPosition): IFoundBracket | null {
584
const position = this.textModel.validatePosition(_position);
585
586
if (this.canBuildAST) {
587
this.bracketsRequested = true;
588
this.updateBracketPairsTree();
589
return this.bracketPairsTree.value?.object.getFirstBracketAfter(position) || null;
590
}
591
592
const lineCount = this.textModel.getLineCount();
593
594
let languageId: string | null = null;
595
let modeBrackets: RichEditBrackets | null = null;
596
let bracketConfig: LanguageBracketsConfiguration | null = null;
597
for (let lineNumber = position.lineNumber; lineNumber <= lineCount; lineNumber++) {
598
const lineTokens = this.textModel.tokenization.getLineTokens(lineNumber);
599
const tokenCount = lineTokens.getCount();
600
const lineText = this.textModel.getLineContent(lineNumber);
601
602
let tokenIndex = 0;
603
let searchStartOffset = 0;
604
let searchEndOffset = 0;
605
if (lineNumber === position.lineNumber) {
606
tokenIndex = lineTokens.findTokenIndexAtOffset(position.column - 1);
607
searchStartOffset = position.column - 1;
608
searchEndOffset = position.column - 1;
609
const tokenLanguageId = lineTokens.getLanguageId(tokenIndex);
610
if (languageId !== tokenLanguageId) {
611
languageId = tokenLanguageId;
612
modeBrackets = this.languageConfigurationService.getLanguageConfiguration(languageId).brackets;
613
bracketConfig = this.languageConfigurationService.getLanguageConfiguration(languageId).bracketsNew;
614
}
615
}
616
617
let prevSearchInToken = true;
618
for (; tokenIndex < tokenCount; tokenIndex++) {
619
const tokenLanguageId = lineTokens.getLanguageId(tokenIndex);
620
621
if (languageId !== tokenLanguageId) {
622
// language id change!
623
if (bracketConfig && modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) {
624
const r = BracketsUtils.findNextBracketInRange(modeBrackets.forwardRegex, lineNumber, lineText, searchStartOffset, searchEndOffset);
625
if (r) {
626
return this._toFoundBracket(bracketConfig, r);
627
}
628
prevSearchInToken = false;
629
}
630
languageId = tokenLanguageId;
631
modeBrackets = this.languageConfigurationService.getLanguageConfiguration(languageId).brackets;
632
bracketConfig = this.languageConfigurationService.getLanguageConfiguration(languageId).bracketsNew;
633
}
634
635
const searchInToken = (!!modeBrackets && !ignoreBracketsInToken(lineTokens.getStandardTokenType(tokenIndex)));
636
if (searchInToken) {
637
// this token should be searched
638
if (prevSearchInToken) {
639
// the previous token should be searched, simply extend searchEndOffset
640
searchEndOffset = lineTokens.getEndOffset(tokenIndex);
641
} else {
642
// the previous token should not be searched
643
searchStartOffset = lineTokens.getStartOffset(tokenIndex);
644
searchEndOffset = lineTokens.getEndOffset(tokenIndex);
645
}
646
} else {
647
// this token should not be searched
648
if (bracketConfig && modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) {
649
const r = BracketsUtils.findNextBracketInRange(modeBrackets.forwardRegex, lineNumber, lineText, searchStartOffset, searchEndOffset);
650
if (r) {
651
return this._toFoundBracket(bracketConfig, r);
652
}
653
}
654
}
655
656
prevSearchInToken = searchInToken;
657
}
658
659
if (bracketConfig && modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) {
660
const r = BracketsUtils.findNextBracketInRange(modeBrackets.forwardRegex, lineNumber, lineText, searchStartOffset, searchEndOffset);
661
if (r) {
662
return this._toFoundBracket(bracketConfig, r);
663
}
664
}
665
}
666
667
return null;
668
}
669
670
public findEnclosingBrackets(_position: IPosition, maxDuration?: number): [Range, Range] | null {
671
const position = this.textModel.validatePosition(_position);
672
673
if (this.canBuildAST) {
674
const range = Range.fromPositions(position);
675
const bracketPair =
676
this.getBracketPairsInRange(Range.fromPositions(position, position)).findLast(
677
(item) => item.closingBracketRange !== undefined && item.range.strictContainsRange(range)
678
);
679
if (bracketPair) {
680
return [bracketPair.openingBracketRange, bracketPair.closingBracketRange!];
681
}
682
return null;
683
}
684
685
const continueSearchPredicate = createTimeBasedContinueBracketSearchPredicate(maxDuration);
686
const lineCount = this.textModel.getLineCount();
687
const savedCounts = new Map<string, number[]>();
688
689
let counts: number[] = [];
690
const resetCounts = (languageId: string, modeBrackets: RichEditBrackets | null) => {
691
if (!savedCounts.has(languageId)) {
692
const tmp = [];
693
for (let i = 0, len = modeBrackets ? modeBrackets.brackets.length : 0; i < len; i++) {
694
tmp[i] = 0;
695
}
696
savedCounts.set(languageId, tmp);
697
}
698
counts = savedCounts.get(languageId)!;
699
};
700
701
let totalCallCount = 0;
702
const searchInRange = (modeBrackets: RichEditBrackets, lineNumber: number, lineText: string, searchStartOffset: number, searchEndOffset: number): [Range, Range] | null | BracketSearchCanceled => {
703
while (true) {
704
if (continueSearchPredicate && (++totalCallCount) % 100 === 0 && !continueSearchPredicate()) {
705
return BracketSearchCanceled.INSTANCE;
706
}
707
const r = BracketsUtils.findNextBracketInRange(modeBrackets.forwardRegex, lineNumber, lineText, searchStartOffset, searchEndOffset);
708
if (!r) {
709
break;
710
}
711
712
const hitText = lineText.substring(r.startColumn - 1, r.endColumn - 1).toLowerCase();
713
const bracket = modeBrackets.textIsBracket[hitText];
714
if (bracket) {
715
if (bracket.isOpen(hitText)) {
716
counts[bracket.index]++;
717
} else if (bracket.isClose(hitText)) {
718
counts[bracket.index]--;
719
}
720
721
if (counts[bracket.index] === -1) {
722
return this._matchFoundBracket(r, bracket, false, continueSearchPredicate);
723
}
724
}
725
726
searchStartOffset = r.endColumn - 1;
727
}
728
return null;
729
};
730
731
let languageId: string | null = null;
732
let modeBrackets: RichEditBrackets | null = null;
733
for (let lineNumber = position.lineNumber; lineNumber <= lineCount; lineNumber++) {
734
const lineTokens = this.textModel.tokenization.getLineTokens(lineNumber);
735
const tokenCount = lineTokens.getCount();
736
const lineText = this.textModel.getLineContent(lineNumber);
737
738
let tokenIndex = 0;
739
let searchStartOffset = 0;
740
let searchEndOffset = 0;
741
if (lineNumber === position.lineNumber) {
742
tokenIndex = lineTokens.findTokenIndexAtOffset(position.column - 1);
743
searchStartOffset = position.column - 1;
744
searchEndOffset = position.column - 1;
745
const tokenLanguageId = lineTokens.getLanguageId(tokenIndex);
746
if (languageId !== tokenLanguageId) {
747
languageId = tokenLanguageId;
748
modeBrackets = this.languageConfigurationService.getLanguageConfiguration(languageId).brackets;
749
resetCounts(languageId, modeBrackets);
750
}
751
}
752
753
let prevSearchInToken = true;
754
for (; tokenIndex < tokenCount; tokenIndex++) {
755
const tokenLanguageId = lineTokens.getLanguageId(tokenIndex);
756
757
if (languageId !== tokenLanguageId) {
758
// language id change!
759
if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) {
760
const r = searchInRange(modeBrackets, lineNumber, lineText, searchStartOffset, searchEndOffset);
761
if (r) {
762
return stripBracketSearchCanceled(r);
763
}
764
prevSearchInToken = false;
765
}
766
languageId = tokenLanguageId;
767
modeBrackets = this.languageConfigurationService.getLanguageConfiguration(languageId).brackets;
768
resetCounts(languageId, modeBrackets);
769
}
770
771
const searchInToken = (!!modeBrackets && !ignoreBracketsInToken(lineTokens.getStandardTokenType(tokenIndex)));
772
if (searchInToken) {
773
// this token should be searched
774
if (prevSearchInToken) {
775
// the previous token should be searched, simply extend searchEndOffset
776
searchEndOffset = lineTokens.getEndOffset(tokenIndex);
777
} else {
778
// the previous token should not be searched
779
searchStartOffset = lineTokens.getStartOffset(tokenIndex);
780
searchEndOffset = lineTokens.getEndOffset(tokenIndex);
781
}
782
} else {
783
// this token should not be searched
784
if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) {
785
const r = searchInRange(modeBrackets, lineNumber, lineText, searchStartOffset, searchEndOffset);
786
if (r) {
787
return stripBracketSearchCanceled(r);
788
}
789
}
790
}
791
792
prevSearchInToken = searchInToken;
793
}
794
795
if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) {
796
const r = searchInRange(modeBrackets, lineNumber, lineText, searchStartOffset, searchEndOffset);
797
if (r) {
798
return stripBracketSearchCanceled(r);
799
}
800
}
801
}
802
803
return null;
804
}
805
806
private _toFoundBracket(bracketConfig: LanguageBracketsConfiguration, r: Range): IFoundBracket | null {
807
if (!r) {
808
return null;
809
}
810
811
let text = this.textModel.getValueInRange(r);
812
text = text.toLowerCase();
813
814
const bracketInfo = bracketConfig.getBracketInfo(text);
815
if (!bracketInfo) {
816
return null;
817
}
818
819
return {
820
range: r,
821
bracketInfo
822
};
823
}
824
}
825
826
function createDisposableRef<T>(object: T, disposable?: IDisposable): IReference<T> {
827
return {
828
object,
829
dispose: () => disposable?.dispose(),
830
};
831
}
832
833
type ContinueBracketSearchPredicate = (() => boolean);
834
835
function createTimeBasedContinueBracketSearchPredicate(maxDuration: number | undefined): ContinueBracketSearchPredicate {
836
if (typeof maxDuration === 'undefined') {
837
return () => true;
838
} else {
839
const startTime = Date.now();
840
return () => {
841
return (Date.now() - startTime <= maxDuration);
842
};
843
}
844
}
845
846
class BracketSearchCanceled {
847
public static INSTANCE = new BracketSearchCanceled();
848
_searchCanceledBrand = undefined;
849
private constructor() { }
850
}
851
852
function stripBracketSearchCanceled<T>(result: T | null | BracketSearchCanceled): T | null {
853
if (result instanceof BracketSearchCanceled) {
854
return null;
855
}
856
return result;
857
}
858
859