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