Path: blob/main/src/vs/workbench/test/electron-browser/treeSitterTokenizationFeature.test.ts
4781 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import assert from 'assert';6import { TestInstantiationService } from '../../../platform/instantiation/test/common/instantiationServiceMock.js';7import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../base/test/common/utils.js';8import { IModelService } from '../../../editor/common/services/model.js';9import { Event } from '../../../base/common/event.js';10import { URI } from '../../../base/common/uri.js';11import { IFileService } from '../../../platform/files/common/files.js';12import { ILogService, NullLogService } from '../../../platform/log/common/log.js';13import { ITelemetryData, ITelemetryService, TelemetryLevel } from '../../../platform/telemetry/common/telemetry.js';14import { ClassifiedEvent, OmitMetadata, IGDPRProperty, StrictPropertyCheck } from '../../../platform/telemetry/common/gdprTypings.js';15import { IConfigurationService } from '../../../platform/configuration/common/configuration.js';16import { TestConfigurationService } from '../../../platform/configuration/test/common/testConfigurationService.js';17import { IEnvironmentService } from '../../../platform/environment/common/environment.js';18import { ModelService } from '../../../editor/common/services/modelService.js';1920import { FileService } from '../../../platform/files/common/fileService.js';21import { Schemas } from '../../../base/common/network.js';22import { TestIPCFileSystemProvider } from './workbenchTestServices.js';23import { ILanguageService } from '../../../editor/common/languages/language.js';24import { LanguageService } from '../../../editor/common/services/languageService.js';25import { TestColorTheme, TestThemeService } from '../../../platform/theme/test/common/testThemeService.js';26import { IThemeService } from '../../../platform/theme/common/themeService.js';27import { ITextResourcePropertiesService } from '../../../editor/common/services/textResourceConfiguration.js';28import { TestTextResourcePropertiesService } from '../common/workbenchTestServices.js';29import { TestLanguageConfigurationService } from '../../../editor/test/common/modes/testLanguageConfigurationService.js';30import { ILanguageConfigurationService } from '../../../editor/common/languages/languageConfigurationRegistry.js';31import { IUndoRedoService } from '../../../platform/undoRedo/common/undoRedo.js';32import { UndoRedoService } from '../../../platform/undoRedo/common/undoRedoService.js';33import { TestDialogService } from '../../../platform/dialogs/test/common/testDialogService.js';34import { TestNotificationService } from '../../../platform/notification/test/common/testNotificationService.js';35import { DisposableStore, IDisposable } from '../../../base/common/lifecycle.js';36import { ProbeScope, TokenStyle } from '../../../platform/theme/common/tokenClassificationRegistry.js';37import { TextMateThemingRuleDefinitions } from '../../services/themes/common/colorThemeData.js';38import { Color } from '../../../base/common/color.js';39import { Range } from '../../../editor/common/core/range.js';40import { TokenUpdate } from '../../../editor/common/model/tokens/treeSitter/tokenStore.js';41import { ITreeSitterLibraryService } from '../../../editor/common/services/treeSitter/treeSitterLibraryService.js';42import { TreeSitterLibraryService } from '../../services/treeSitter/browser/treeSitterLibraryService.js';43import { TokenizationTextModelPart } from '../../../editor/common/model/tokens/tokenizationTextModelPart.js';44import { TreeSitterSyntaxTokenBackend } from '../../../editor/common/model/tokens/treeSitter/treeSitterSyntaxTokenBackend.js';45import { TreeParseUpdateEvent, TreeSitterTree } from '../../../editor/common/model/tokens/treeSitter/treeSitterTree.js';46import { ITextModel } from '../../../editor/common/model.js';47import { TreeSitterTokenizationImpl } from '../../../editor/common/model/tokens/treeSitter/treeSitterTokenizationImpl.js';48import { autorunHandleChanges, recordChanges, waitForState } from '../../../base/common/observable.js';49import { ITreeSitterThemeService } from '../../../editor/common/services/treeSitter/treeSitterThemeService.js';50import { TreeSitterThemeService } from '../../services/treeSitter/browser/treeSitterThemeService.js';5152class MockTelemetryService implements ITelemetryService {53_serviceBrand: undefined;54telemetryLevel: TelemetryLevel = TelemetryLevel.NONE;55sessionId: string = '';56machineId: string = '';57sqmId: string = '';58devDeviceId: string = '';59firstSessionDate: string = '';60sendErrorTelemetry: boolean = false;61publicLog(eventName: string, data?: ITelemetryData): void {62}63publicLog2<E extends ClassifiedEvent<OmitMetadata<T>> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck<T, E>): void {64}65publicLogError(errorEventName: string, data?: ITelemetryData): void {66}67publicLogError2<E extends ClassifiedEvent<OmitMetadata<T>> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck<T, E>): void {68}69setExperimentProperty(name: string, value: string): void {70}71}727374class TestTreeSitterColorTheme extends TestColorTheme {75public resolveScopes(scopes: ProbeScope[], definitions?: TextMateThemingRuleDefinitions): TokenStyle | undefined {76return new TokenStyle(Color.red, undefined, undefined, undefined, undefined);77}78public getTokenColorIndex(): { get: () => number } {79return { get: () => 10 };80}81}8283suite('Tree Sitter TokenizationFeature', function () {8485let instantiationService: TestInstantiationService;86let modelService: IModelService;87let fileService: IFileService;88let textResourcePropertiesService: ITextResourcePropertiesService;89let languageConfigurationService: ILanguageConfigurationService;90let telemetryService: ITelemetryService;91let logService: ILogService;92let configurationService: IConfigurationService;93let themeService: IThemeService;94let languageService: ILanguageService;95let environmentService: IEnvironmentService;9697let disposables: DisposableStore;9899setup(async () => {100disposables = new DisposableStore();101instantiationService = disposables.add(new TestInstantiationService());102103telemetryService = new MockTelemetryService();104logService = new NullLogService();105configurationService = new TestConfigurationService({ 'editor.experimental.preferTreeSitter.typescript': true });106themeService = new TestThemeService(new TestTreeSitterColorTheme());107environmentService = {} as IEnvironmentService;108109instantiationService.set(IEnvironmentService, environmentService);110instantiationService.set(IConfigurationService, configurationService);111instantiationService.set(ILogService, logService);112instantiationService.set(ITelemetryService, telemetryService);113languageService = disposables.add(instantiationService.createInstance(LanguageService));114instantiationService.set(ILanguageService, languageService);115instantiationService.set(IThemeService, themeService);116textResourcePropertiesService = instantiationService.createInstance(TestTextResourcePropertiesService);117instantiationService.set(ITextResourcePropertiesService, textResourcePropertiesService);118languageConfigurationService = disposables.add(instantiationService.createInstance(TestLanguageConfigurationService));119instantiationService.set(ILanguageConfigurationService, languageConfigurationService);120121fileService = disposables.add(instantiationService.createInstance(FileService));122const fileSystemProvider = new TestIPCFileSystemProvider();123disposables.add(fileService.registerProvider(Schemas.file, fileSystemProvider));124instantiationService.set(IFileService, fileService);125126const libraryService = disposables.add(instantiationService.createInstance(TreeSitterLibraryService));127libraryService.isTest = true;128instantiationService.set(ITreeSitterLibraryService, libraryService);129130instantiationService.set(ITreeSitterThemeService, instantiationService.createInstance(TreeSitterThemeService));131132const dialogService = new TestDialogService();133const notificationService = new TestNotificationService();134const undoRedoService = new UndoRedoService(dialogService, notificationService);135instantiationService.set(IUndoRedoService, undoRedoService);136modelService = new ModelService(137configurationService,138textResourcePropertiesService,139undoRedoService,140instantiationService141);142instantiationService.set(IModelService, modelService);143});144145teardown(() => {146disposables.dispose();147});148149ensureNoDisposablesAreLeakedInTestSuite();150151function tokensContentSize(tokens: TokenUpdate[]) {152return tokens[tokens.length - 1].startOffsetInclusive + tokens[tokens.length - 1].length;153}154155let nameNumber = 1;156async function getModelAndPrepTree(content: string): Promise<{ model: ITextModel; treeSitterTree: TreeSitterTree; tokenizationImpl: TreeSitterTokenizationImpl }> {157const model = disposables.add(modelService.createModel(content, { languageId: 'typescript', onDidChange: Event.None }, URI.file(`file${nameNumber++}.ts`)));158const treeSitterTreeObs = disposables.add((model.tokenization as TokenizationTextModelPart).tokens.get() as TreeSitterSyntaxTokenBackend).tree;159const tokenizationImplObs = disposables.add((model.tokenization as TokenizationTextModelPart).tokens.get() as TreeSitterSyntaxTokenBackend).tokenizationImpl;160const treeSitterTree = treeSitterTreeObs.get() ?? await waitForState(treeSitterTreeObs);161if (!treeSitterTree.tree.get()) {162await waitForState(treeSitterTree.tree);163}164const tokenizationImpl = tokenizationImplObs.get() ?? await waitForState(tokenizationImplObs);165166assert.ok(treeSitterTree);167return { model, treeSitterTree, tokenizationImpl };168}169170function verifyTokens(tokens: TokenUpdate[] | undefined) {171assert.ok(tokens);172for (let i = 1; i < tokens.length; i++) {173const previousToken: TokenUpdate = tokens[i - 1];174const token: TokenUpdate = tokens[i];175assert.deepStrictEqual(previousToken.startOffsetInclusive + previousToken.length, token.startOffsetInclusive);176}177}178179test('Three changes come back to back ', async () => {180const content = `/**181**/182class x {183}184185186187188class y {189}`;190const { model, treeSitterTree } = await getModelAndPrepTree(content);191192let updateListener: IDisposable | undefined;193const changePromise = new Promise<TreeParseUpdateEvent | undefined>(resolve => {194updateListener = autorunHandleChanges({195owner: this,196changeTracker: recordChanges({ tree: treeSitterTree.tree }),197}, (reader, ctx) => {198const changeEvent = ctx.changes.at(0)?.change;199if (changeEvent) {200resolve(changeEvent);201}202});203});204205const edit1 = new Promise<void>(resolve => {206model.applyEdits([{ range: new Range(7, 1, 8, 1), text: '' }]);207resolve();208});209const edit2 = new Promise<void>(resolve => {210model.applyEdits([{ range: new Range(6, 1, 7, 1), text: '' }]);211resolve();212});213const edit3 = new Promise<void>(resolve => {214model.applyEdits([{ range: new Range(5, 1, 6, 1), text: '' }]);215resolve();216});217const edits = Promise.all([edit1, edit2, edit3]);218const change = await changePromise;219await edits;220assert.ok(change);221222assert.strictEqual(change.versionId, 4);223assert.strictEqual(change.ranges[0].newRangeStartOffset, 0);224assert.strictEqual(change.ranges[0].newRangeEndOffset, 32);225assert.strictEqual(change.ranges[0].newRange.startLineNumber, 1);226assert.strictEqual(change.ranges[0].newRange.endLineNumber, 7);227228updateListener?.dispose();229modelService.destroyModel(model.uri);230});231232test('File single line file', async () => {233const content = `console.log('x');`;234const { model, tokenizationImpl } = await getModelAndPrepTree(content);235const tokens = tokenizationImpl.getTokensInRange(new Range(1, 1, 1, 18), 0, 17);236verifyTokens(tokens);237assert.deepStrictEqual(tokens?.length, 9);238assert.deepStrictEqual(tokensContentSize(tokens), content.length);239modelService.destroyModel(model.uri);240});241242test('File with new lines at beginning and end', async () => {243const content = `244console.log('x');245`;246const { model, tokenizationImpl } = await getModelAndPrepTree(content);247const tokens = tokenizationImpl.getTokensInRange(new Range(1, 1, 3, 1), 0, 19);248verifyTokens(tokens);249assert.deepStrictEqual(tokens?.length, 11);250assert.deepStrictEqual(tokensContentSize(tokens), content.length);251modelService.destroyModel(model.uri);252});253254test('File with new lines at beginning and end \\r\\n', async () => {255const content = '\r\nconsole.log(\'x\');\r\n';256const { model, tokenizationImpl } = await getModelAndPrepTree(content);257const tokens = tokenizationImpl.getTokensInRange(new Range(1, 1, 3, 1), 0, 21);258verifyTokens(tokens);259assert.deepStrictEqual(tokens?.length, 11);260assert.deepStrictEqual(tokensContentSize(tokens), content.length);261modelService.destroyModel(model.uri);262});263264test('File with empty lines in the middle', async () => {265const content = `266console.log('x');267268console.log('7');269`;270const { model, tokenizationImpl } = await getModelAndPrepTree(content);271const tokens = tokenizationImpl.getTokensInRange(new Range(1, 1, 5, 1), 0, 38);272verifyTokens(tokens);273assert.deepStrictEqual(tokens?.length, 21);274assert.deepStrictEqual(tokensContentSize(tokens), content.length);275modelService.destroyModel(model.uri);276});277278test('File with empty lines in the middle \\r\\n', async () => {279const content = '\r\nconsole.log(\'x\');\r\n\r\nconsole.log(\'7\');\r\n';280const { model, tokenizationImpl } = await getModelAndPrepTree(content);281const tokens = tokenizationImpl.getTokensInRange(new Range(1, 1, 5, 1), 0, 42);282verifyTokens(tokens);283assert.deepStrictEqual(tokens?.length, 21);284assert.deepStrictEqual(tokensContentSize(tokens), content.length);285modelService.destroyModel(model.uri);286});287288test('File with non-empty lines that match no scopes', async () => {289const content = `console.log('x');290;291{292}293`;294const { model, tokenizationImpl } = await getModelAndPrepTree(content);295const tokens = tokenizationImpl.getTokensInRange(new Range(1, 1, 5, 1), 0, 24);296verifyTokens(tokens);297assert.deepStrictEqual(tokens?.length, 16);298assert.deepStrictEqual(tokensContentSize(tokens), content.length);299modelService.destroyModel(model.uri);300});301302test('File with non-empty lines that match no scopes \\r\\n', async () => {303const content = 'console.log(\'x\');\r\n;\r\n{\r\n}\r\n';304const { model, tokenizationImpl } = await getModelAndPrepTree(content);305const tokens = tokenizationImpl.getTokensInRange(new Range(1, 1, 5, 1), 0, 28);306verifyTokens(tokens);307assert.deepStrictEqual(tokens?.length, 16);308assert.deepStrictEqual(tokensContentSize(tokens), content.length);309modelService.destroyModel(model.uri);310});311312test('File with tree-sitter token that spans multiple lines', async () => {313const content = `/**314**/315316console.log('x');317318`;319const { model, tokenizationImpl } = await getModelAndPrepTree(content);320const tokens = tokenizationImpl.getTokensInRange(new Range(1, 1, 6, 1), 0, 28);321verifyTokens(tokens);322assert.deepStrictEqual(tokens?.length, 12);323assert.deepStrictEqual(tokensContentSize(tokens), content.length);324modelService.destroyModel(model.uri);325});326327test('File with tree-sitter token that spans multiple lines \\r\\n', async () => {328const content = '/**\r\n**/\r\n\r\nconsole.log(\'x\');\r\n\r\n';329const { model, tokenizationImpl } = await getModelAndPrepTree(content);330const tokens = tokenizationImpl.getTokensInRange(new Range(1, 1, 6, 1), 0, 33);331verifyTokens(tokens);332assert.deepStrictEqual(tokens?.length, 12);333assert.deepStrictEqual(tokensContentSize(tokens), content.length);334modelService.destroyModel(model.uri);335});336337test('File with tabs', async () => {338const content = `function x() {339return true;340}341342class Y {343private z = false;344}`;345const { model, tokenizationImpl } = await getModelAndPrepTree(content);346const tokens = tokenizationImpl.getTokensInRange(new Range(1, 1, 7, 1), 0, 63);347verifyTokens(tokens);348assert.deepStrictEqual(tokens?.length, 30);349assert.deepStrictEqual(tokensContentSize(tokens), content.length);350modelService.destroyModel(model.uri);351});352353test('File with tabs \\r\\n', async () => {354const content = 'function x() {\r\n\treturn true;\r\n}\r\n\r\nclass Y {\r\n\tprivate z = false;\r\n}';355const { model, tokenizationImpl } = await getModelAndPrepTree(content);356const tokens = tokenizationImpl.getTokensInRange(new Range(1, 1, 7, 1), 0, 69);357verifyTokens(tokens);358assert.deepStrictEqual(tokens?.length, 30);359assert.deepStrictEqual(tokensContentSize(tokens), content.length);360modelService.destroyModel(model.uri);361});362363test('Template string', async () => {364const content = '`t ${6}`';365const { model, tokenizationImpl } = await getModelAndPrepTree(content);366const tokens = tokenizationImpl.getTokensInRange(new Range(1, 1, 1, 8), 0, 8);367verifyTokens(tokens);368assert.deepStrictEqual(tokens?.length, 6);369assert.deepStrictEqual(tokensContentSize(tokens), content.length);370modelService.destroyModel(model.uri);371});372373test('Many nested scopes', async () => {374const content = `y = new x(ttt({375message: '{0} i\\n\\n [commandName]({1}).',376args: ['Test', \`command:\${openSettingsCommand}?\${encodeURIComponent('["SettingName"]')}\`],377// To make sure the translators don't break the link378comment: ["{Locked=']({'}"]379}));`;380const { model, tokenizationImpl } = await getModelAndPrepTree(content);381const tokens = tokenizationImpl.getTokensInRange(new Range(1, 1, 6, 5), 0, 238);382verifyTokens(tokens);383assert.deepStrictEqual(tokens?.length, 65);384assert.deepStrictEqual(tokensContentSize(tokens), content.length);385modelService.destroyModel(model.uri);386});387388});389390391