Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/test/electron-main/treeSitterTokenizationFeature.test.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 assert from 'assert';
7
import { TestInstantiationService } from '../../../platform/instantiation/test/common/instantiationServiceMock.js';
8
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../base/test/common/utils.js';
9
import { IModelService } from '../../../editor/common/services/model.js';
10
import { Event } from '../../../base/common/event.js';
11
import { URI } from '../../../base/common/uri.js';
12
import { IFileService } from '../../../platform/files/common/files.js';
13
import { ILogService, NullLogService } from '../../../platform/log/common/log.js';
14
import { ITelemetryData, ITelemetryService, TelemetryLevel } from '../../../platform/telemetry/common/telemetry.js';
15
import { ClassifiedEvent, OmitMetadata, IGDPRProperty, StrictPropertyCheck } from '../../../platform/telemetry/common/gdprTypings.js';
16
import { IConfigurationService } from '../../../platform/configuration/common/configuration.js';
17
import { TestConfigurationService } from '../../../platform/configuration/test/common/testConfigurationService.js';
18
import { IEnvironmentService } from '../../../platform/environment/common/environment.js';
19
import { ModelService } from '../../../editor/common/services/modelService.js';
20
21
import { FileService } from '../../../platform/files/common/fileService.js';
22
import { Schemas } from '../../../base/common/network.js';
23
import { DiskFileSystemProvider } from '../../../platform/files/node/diskFileSystemProvider.js';
24
import { ILanguageService } from '../../../editor/common/languages/language.js';
25
import { LanguageService } from '../../../editor/common/services/languageService.js';
26
import { TestColorTheme, TestThemeService } from '../../../platform/theme/test/common/testThemeService.js';
27
import { IThemeService } from '../../../platform/theme/common/themeService.js';
28
import { ITextResourcePropertiesService } from '../../../editor/common/services/textResourceConfiguration.js';
29
import { TestTextResourcePropertiesService } from '../common/workbenchTestServices.js';
30
import { TestLanguageConfigurationService } from '../../../editor/test/common/modes/testLanguageConfigurationService.js';
31
import { ILanguageConfigurationService } from '../../../editor/common/languages/languageConfigurationRegistry.js';
32
import { IUndoRedoService } from '../../../platform/undoRedo/common/undoRedo.js';
33
import { UndoRedoService } from '../../../platform/undoRedo/common/undoRedoService.js';
34
import { TestDialogService } from '../../../platform/dialogs/test/common/testDialogService.js';
35
import { TestNotificationService } from '../../../platform/notification/test/common/testNotificationService.js';
36
import { DisposableStore, IDisposable } from '../../../base/common/lifecycle.js';
37
import { ProbeScope, TokenStyle } from '../../../platform/theme/common/tokenClassificationRegistry.js';
38
import { TextMateThemingRuleDefinitions } from '../../services/themes/common/colorThemeData.js';
39
import { Color } from '../../../base/common/color.js';
40
import { Range } from '../../../editor/common/core/range.js';
41
import { TokenUpdate } from '../../../editor/common/model/tokens/treeSitter/tokenStore.js';
42
import { ITreeSitterLibraryService } from '../../../editor/common/services/treeSitter/treeSitterLibraryService.js';
43
// eslint-disable-next-line local/code-layering, local/code-import-patterns
44
import { TreeSitterLibraryService } from '../../services/treeSitter/browser/treeSitterLibraryService.js';
45
import { TokenizationTextModelPart } from '../../../editor/common/model/tokens/tokenizationTextModelPart.js';
46
import { TreeSitterSyntaxTokenBackend } from '../../../editor/common/model/tokens/treeSitter/treeSitterSyntaxTokenBackend.js';
47
import { TreeParseUpdateEvent, TreeSitterTree } from '../../../editor/common/model/tokens/treeSitter/treeSitterTree.js';
48
import { ITextModel } from '../../../editor/common/model.js';
49
import { TreeSitterTokenizationImpl } from '../../../editor/common/model/tokens/treeSitter/treeSitterTokenizationImpl.js';
50
import { autorunHandleChanges, recordChanges, waitForState } from '../../../base/common/observable.js';
51
import { ITreeSitterThemeService } from '../../../editor/common/services/treeSitter/treeSitterThemeService.js';
52
// eslint-disable-next-line local/code-layering, local/code-import-patterns
53
import { TreeSitterThemeService } from '../../services/treeSitter/browser/treeSitterThemeService.js';
54
55
class MockTelemetryService implements ITelemetryService {
56
_serviceBrand: undefined;
57
telemetryLevel: TelemetryLevel = TelemetryLevel.NONE;
58
sessionId: string = '';
59
machineId: string = '';
60
sqmId: string = '';
61
devDeviceId: string = '';
62
firstSessionDate: string = '';
63
sendErrorTelemetry: boolean = false;
64
publicLog(eventName: string, data?: ITelemetryData): void {
65
}
66
publicLog2<E extends ClassifiedEvent<OmitMetadata<T>> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck<T, E>): void {
67
}
68
publicLogError(errorEventName: string, data?: ITelemetryData): void {
69
}
70
publicLogError2<E extends ClassifiedEvent<OmitMetadata<T>> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck<T, E>): void {
71
}
72
setExperimentProperty(name: string, value: string): void {
73
}
74
}
75
76
77
class TestTreeSitterColorTheme extends TestColorTheme {
78
public resolveScopes(scopes: ProbeScope[], definitions?: TextMateThemingRuleDefinitions): TokenStyle | undefined {
79
return new TokenStyle(Color.red, undefined, undefined, undefined, undefined);
80
}
81
public getTokenColorIndex(): { get: () => number } {
82
return { get: () => 10 };
83
}
84
}
85
86
suite('Tree Sitter TokenizationFeature', function () {
87
88
let instantiationService: TestInstantiationService;
89
let modelService: IModelService;
90
let fileService: IFileService;
91
let textResourcePropertiesService: ITextResourcePropertiesService;
92
let languageConfigurationService: ILanguageConfigurationService;
93
let telemetryService: ITelemetryService;
94
let logService: ILogService;
95
let configurationService: IConfigurationService;
96
let themeService: IThemeService;
97
let languageService: ILanguageService;
98
let environmentService: IEnvironmentService;
99
100
let disposables: DisposableStore;
101
102
setup(async () => {
103
disposables = new DisposableStore();
104
instantiationService = disposables.add(new TestInstantiationService());
105
106
telemetryService = new MockTelemetryService();
107
logService = new NullLogService();
108
configurationService = new TestConfigurationService({ 'editor.experimental.preferTreeSitter.typescript': true });
109
themeService = new TestThemeService(new TestTreeSitterColorTheme());
110
environmentService = {} as IEnvironmentService;
111
112
instantiationService.set(IEnvironmentService, environmentService);
113
instantiationService.set(IConfigurationService, configurationService);
114
instantiationService.set(ILogService, logService);
115
instantiationService.set(ITelemetryService, telemetryService);
116
languageService = disposables.add(instantiationService.createInstance(LanguageService));
117
instantiationService.set(ILanguageService, languageService);
118
instantiationService.set(IThemeService, themeService);
119
textResourcePropertiesService = instantiationService.createInstance(TestTextResourcePropertiesService);
120
instantiationService.set(ITextResourcePropertiesService, textResourcePropertiesService);
121
languageConfigurationService = disposables.add(instantiationService.createInstance(TestLanguageConfigurationService));
122
instantiationService.set(ILanguageConfigurationService, languageConfigurationService);
123
124
fileService = disposables.add(instantiationService.createInstance(FileService));
125
const diskFileSystemProvider = disposables.add(new DiskFileSystemProvider(logService));
126
disposables.add(fileService.registerProvider(Schemas.file, diskFileSystemProvider));
127
instantiationService.set(IFileService, fileService);
128
129
const libraryService = disposables.add(instantiationService.createInstance(TreeSitterLibraryService));
130
libraryService.isTest = true;
131
instantiationService.set(ITreeSitterLibraryService, libraryService);
132
133
instantiationService.set(ITreeSitterThemeService, instantiationService.createInstance(TreeSitterThemeService));
134
135
const dialogService = new TestDialogService();
136
const notificationService = new TestNotificationService();
137
const undoRedoService = new UndoRedoService(dialogService, notificationService);
138
instantiationService.set(IUndoRedoService, undoRedoService);
139
modelService = new ModelService(
140
configurationService,
141
textResourcePropertiesService,
142
undoRedoService,
143
instantiationService
144
);
145
instantiationService.set(IModelService, modelService);
146
});
147
148
teardown(() => {
149
disposables.dispose();
150
});
151
152
ensureNoDisposablesAreLeakedInTestSuite();
153
154
function tokensContentSize(tokens: TokenUpdate[]) {
155
return tokens[tokens.length - 1].startOffsetInclusive + tokens[tokens.length - 1].length;
156
}
157
158
let nameNumber = 1;
159
async function getModelAndPrepTree(content: string): Promise<{ model: ITextModel; treeSitterTree: TreeSitterTree; tokenizationImpl: TreeSitterTokenizationImpl }> {
160
const model = disposables.add(modelService.createModel(content, { languageId: 'typescript', onDidChange: Event.None }, URI.file(`file${nameNumber++}.ts`)));
161
const treeSitterTreeObs = disposables.add((model.tokenization as TokenizationTextModelPart).tokens.get() as TreeSitterSyntaxTokenBackend).tree;
162
const tokenizationImplObs = disposables.add((model.tokenization as TokenizationTextModelPart).tokens.get() as TreeSitterSyntaxTokenBackend).tokenizationImpl;
163
const treeSitterTree = treeSitterTreeObs.get() ?? await waitForState(treeSitterTreeObs);
164
if (!treeSitterTree.tree.get()) {
165
await waitForState(treeSitterTree.tree);
166
}
167
const tokenizationImpl = tokenizationImplObs.get() ?? await waitForState(tokenizationImplObs);
168
169
assert.ok(treeSitterTree);
170
return { model, treeSitterTree, tokenizationImpl };
171
}
172
173
function verifyTokens(tokens: TokenUpdate[] | undefined) {
174
assert.ok(tokens);
175
for (let i = 1; i < tokens.length; i++) {
176
const previousToken: TokenUpdate = tokens[i - 1];
177
const token: TokenUpdate = tokens[i];
178
assert.deepStrictEqual(previousToken.startOffsetInclusive + previousToken.length, token.startOffsetInclusive);
179
}
180
}
181
182
test('Three changes come back to back ', async () => {
183
const content = `/**
184
**/
185
class x {
186
}
187
188
189
190
191
class y {
192
}`;
193
const { model, treeSitterTree } = await getModelAndPrepTree(content);
194
195
let updateListener: IDisposable | undefined;
196
const changePromise = new Promise<TreeParseUpdateEvent | undefined>(resolve => {
197
updateListener = autorunHandleChanges({
198
owner: this,
199
changeTracker: recordChanges({ tree: treeSitterTree.tree }),
200
}, (reader, ctx) => {
201
const changeEvent = ctx.changes.at(0)?.change;
202
if (changeEvent) {
203
resolve(changeEvent);
204
}
205
});
206
});
207
208
const edit1 = new Promise<void>(resolve => {
209
model.applyEdits([{ range: new Range(7, 1, 8, 1), text: '' }]);
210
resolve();
211
});
212
const edit2 = new Promise<void>(resolve => {
213
model.applyEdits([{ range: new Range(6, 1, 7, 1), text: '' }]);
214
resolve();
215
});
216
const edit3 = new Promise<void>(resolve => {
217
model.applyEdits([{ range: new Range(5, 1, 6, 1), text: '' }]);
218
resolve();
219
});
220
const edits = Promise.all([edit1, edit2, edit3]);
221
const change = await changePromise;
222
await edits;
223
assert.ok(change);
224
225
assert.strictEqual(change.versionId, 4);
226
assert.strictEqual(change.ranges[0].newRangeStartOffset, 0);
227
assert.strictEqual(change.ranges[0].newRangeEndOffset, 32);
228
assert.strictEqual(change.ranges[0].newRange.startLineNumber, 1);
229
assert.strictEqual(change.ranges[0].newRange.endLineNumber, 7);
230
231
updateListener?.dispose();
232
modelService.destroyModel(model.uri);
233
});
234
235
test('File single line file', async () => {
236
const content = `console.log('x');`;
237
const { model, tokenizationImpl } = await getModelAndPrepTree(content);
238
const tokens = tokenizationImpl.getTokensInRange(new Range(1, 1, 1, 18), 0, 17);
239
verifyTokens(tokens);
240
assert.deepStrictEqual(tokens?.length, 9);
241
assert.deepStrictEqual(tokensContentSize(tokens), content.length);
242
modelService.destroyModel(model.uri);
243
});
244
245
test('File with new lines at beginning and end', async () => {
246
const content = `
247
console.log('x');
248
`;
249
const { model, tokenizationImpl } = await getModelAndPrepTree(content);
250
const tokens = tokenizationImpl.getTokensInRange(new Range(1, 1, 3, 1), 0, 19);
251
verifyTokens(tokens);
252
assert.deepStrictEqual(tokens?.length, 11);
253
assert.deepStrictEqual(tokensContentSize(tokens), content.length);
254
modelService.destroyModel(model.uri);
255
});
256
257
test('File with new lines at beginning and end \\r\\n', async () => {
258
const content = '\r\nconsole.log(\'x\');\r\n';
259
const { model, tokenizationImpl } = await getModelAndPrepTree(content);
260
const tokens = tokenizationImpl.getTokensInRange(new Range(1, 1, 3, 1), 0, 21);
261
verifyTokens(tokens);
262
assert.deepStrictEqual(tokens?.length, 11);
263
assert.deepStrictEqual(tokensContentSize(tokens), content.length);
264
modelService.destroyModel(model.uri);
265
});
266
267
test('File with empty lines in the middle', async () => {
268
const content = `
269
console.log('x');
270
271
console.log('7');
272
`;
273
const { model, tokenizationImpl } = await getModelAndPrepTree(content);
274
const tokens = tokenizationImpl.getTokensInRange(new Range(1, 1, 5, 1), 0, 38);
275
verifyTokens(tokens);
276
assert.deepStrictEqual(tokens?.length, 21);
277
assert.deepStrictEqual(tokensContentSize(tokens), content.length);
278
modelService.destroyModel(model.uri);
279
});
280
281
test('File with empty lines in the middle \\r\\n', async () => {
282
const content = '\r\nconsole.log(\'x\');\r\n\r\nconsole.log(\'7\');\r\n';
283
const { model, tokenizationImpl } = await getModelAndPrepTree(content);
284
const tokens = tokenizationImpl.getTokensInRange(new Range(1, 1, 5, 1), 0, 42);
285
verifyTokens(tokens);
286
assert.deepStrictEqual(tokens?.length, 21);
287
assert.deepStrictEqual(tokensContentSize(tokens), content.length);
288
modelService.destroyModel(model.uri);
289
});
290
291
test('File with non-empty lines that match no scopes', async () => {
292
const content = `console.log('x');
293
;
294
{
295
}
296
`;
297
const { model, tokenizationImpl } = await getModelAndPrepTree(content);
298
const tokens = tokenizationImpl.getTokensInRange(new Range(1, 1, 5, 1), 0, 24);
299
verifyTokens(tokens);
300
assert.deepStrictEqual(tokens?.length, 16);
301
assert.deepStrictEqual(tokensContentSize(tokens), content.length);
302
modelService.destroyModel(model.uri);
303
});
304
305
test('File with non-empty lines that match no scopes \\r\\n', async () => {
306
const content = 'console.log(\'x\');\r\n;\r\n{\r\n}\r\n';
307
const { model, tokenizationImpl } = await getModelAndPrepTree(content);
308
const tokens = tokenizationImpl.getTokensInRange(new Range(1, 1, 5, 1), 0, 28);
309
verifyTokens(tokens);
310
assert.deepStrictEqual(tokens?.length, 16);
311
assert.deepStrictEqual(tokensContentSize(tokens), content.length);
312
modelService.destroyModel(model.uri);
313
});
314
315
test('File with tree-sitter token that spans multiple lines', async () => {
316
const content = `/**
317
**/
318
319
console.log('x');
320
321
`;
322
const { model, tokenizationImpl } = await getModelAndPrepTree(content);
323
const tokens = tokenizationImpl.getTokensInRange(new Range(1, 1, 6, 1), 0, 28);
324
verifyTokens(tokens);
325
assert.deepStrictEqual(tokens?.length, 12);
326
assert.deepStrictEqual(tokensContentSize(tokens), content.length);
327
modelService.destroyModel(model.uri);
328
});
329
330
test('File with tree-sitter token that spans multiple lines \\r\\n', async () => {
331
const content = '/**\r\n**/\r\n\r\nconsole.log(\'x\');\r\n\r\n';
332
const { model, tokenizationImpl } = await getModelAndPrepTree(content);
333
const tokens = tokenizationImpl.getTokensInRange(new Range(1, 1, 6, 1), 0, 33);
334
verifyTokens(tokens);
335
assert.deepStrictEqual(tokens?.length, 12);
336
assert.deepStrictEqual(tokensContentSize(tokens), content.length);
337
modelService.destroyModel(model.uri);
338
});
339
340
test('File with tabs', async () => {
341
const content = `function x() {
342
return true;
343
}
344
345
class Y {
346
private z = false;
347
}`;
348
const { model, tokenizationImpl } = await getModelAndPrepTree(content);
349
const tokens = tokenizationImpl.getTokensInRange(new Range(1, 1, 7, 1), 0, 63);
350
verifyTokens(tokens);
351
assert.deepStrictEqual(tokens?.length, 30);
352
assert.deepStrictEqual(tokensContentSize(tokens), content.length);
353
modelService.destroyModel(model.uri);
354
});
355
356
test('File with tabs \\r\\n', async () => {
357
const content = 'function x() {\r\n\treturn true;\r\n}\r\n\r\nclass Y {\r\n\tprivate z = false;\r\n}';
358
const { model, tokenizationImpl } = await getModelAndPrepTree(content);
359
const tokens = tokenizationImpl.getTokensInRange(new Range(1, 1, 7, 1), 0, 69);
360
verifyTokens(tokens);
361
assert.deepStrictEqual(tokens?.length, 30);
362
assert.deepStrictEqual(tokensContentSize(tokens), content.length);
363
modelService.destroyModel(model.uri);
364
});
365
366
test('Template string', async () => {
367
const content = '`t ${6}`';
368
const { model, tokenizationImpl } = await getModelAndPrepTree(content);
369
const tokens = tokenizationImpl.getTokensInRange(new Range(1, 1, 1, 8), 0, 8);
370
verifyTokens(tokens);
371
assert.deepStrictEqual(tokens?.length, 6);
372
assert.deepStrictEqual(tokensContentSize(tokens), content.length);
373
modelService.destroyModel(model.uri);
374
});
375
376
test('Many nested scopes', async () => {
377
const content = `y = new x(ttt({
378
message: '{0} i\\n\\n [commandName]({1}).',
379
args: ['Test', \`command:\${openSettingsCommand}?\${encodeURIComponent('["SettingName"]')}\`],
380
// To make sure the translators don't break the link
381
comment: ["{Locked=']({'}"]
382
}));`;
383
const { model, tokenizationImpl } = await getModelAndPrepTree(content);
384
const tokens = tokenizationImpl.getTokensInRange(new Range(1, 1, 6, 5), 0, 238);
385
verifyTokens(tokens);
386
assert.deepStrictEqual(tokens?.length, 65);
387
assert.deepStrictEqual(tokensContentSize(tokens), content.length);
388
modelService.destroyModel(model.uri);
389
});
390
391
});
392
393