Path: blob/main/src/vs/workbench/test/electron-main/treeSitterTokenizationFeature.test.ts
3296 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 { DiskFileSystemProvider } from '../../../platform/files/node/diskFileSystemProvider.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';42// eslint-disable-next-line local/code-layering, local/code-import-patterns43import { TreeSitterLibraryService } from '../../services/treeSitter/browser/treeSitterLibraryService.js';44import { TokenizationTextModelPart } from '../../../editor/common/model/tokens/tokenizationTextModelPart.js';45import { TreeSitterSyntaxTokenBackend } from '../../../editor/common/model/tokens/treeSitter/treeSitterSyntaxTokenBackend.js';46import { TreeParseUpdateEvent, TreeSitterTree } from '../../../editor/common/model/tokens/treeSitter/treeSitterTree.js';47import { ITextModel } from '../../../editor/common/model.js';48import { TreeSitterTokenizationImpl } from '../../../editor/common/model/tokens/treeSitter/treeSitterTokenizationImpl.js';49import { autorunHandleChanges, recordChanges, waitForState } from '../../../base/common/observable.js';50import { ITreeSitterThemeService } from '../../../editor/common/services/treeSitter/treeSitterThemeService.js';51// eslint-disable-next-line local/code-layering, local/code-import-patterns52import { TreeSitterThemeService } from '../../services/treeSitter/browser/treeSitterThemeService.js';5354class MockTelemetryService implements ITelemetryService {55_serviceBrand: undefined;56telemetryLevel: TelemetryLevel = TelemetryLevel.NONE;57sessionId: string = '';58machineId: string = '';59sqmId: string = '';60devDeviceId: string = '';61firstSessionDate: string = '';62sendErrorTelemetry: boolean = false;63publicLog(eventName: string, data?: ITelemetryData): void {64}65publicLog2<E extends ClassifiedEvent<OmitMetadata<T>> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck<T, E>): void {66}67publicLogError(errorEventName: string, data?: ITelemetryData): void {68}69publicLogError2<E extends ClassifiedEvent<OmitMetadata<T>> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck<T, E>): void {70}71setExperimentProperty(name: string, value: string): void {72}73}747576class TestTreeSitterColorTheme extends TestColorTheme {77public resolveScopes(scopes: ProbeScope[], definitions?: TextMateThemingRuleDefinitions): TokenStyle | undefined {78return new TokenStyle(Color.red, undefined, undefined, undefined, undefined);79}80public getTokenColorIndex(): { get: () => number } {81return { get: () => 10 };82}83}8485suite('Tree Sitter TokenizationFeature', function () {8687let instantiationService: TestInstantiationService;88let modelService: IModelService;89let fileService: IFileService;90let textResourcePropertiesService: ITextResourcePropertiesService;91let languageConfigurationService: ILanguageConfigurationService;92let telemetryService: ITelemetryService;93let logService: ILogService;94let configurationService: IConfigurationService;95let themeService: IThemeService;96let languageService: ILanguageService;97let environmentService: IEnvironmentService;9899let disposables: DisposableStore;100101setup(async () => {102disposables = new DisposableStore();103instantiationService = disposables.add(new TestInstantiationService());104105telemetryService = new MockTelemetryService();106logService = new NullLogService();107configurationService = new TestConfigurationService({ 'editor.experimental.preferTreeSitter.typescript': true });108themeService = new TestThemeService(new TestTreeSitterColorTheme());109environmentService = {} as IEnvironmentService;110111instantiationService.set(IEnvironmentService, environmentService);112instantiationService.set(IConfigurationService, configurationService);113instantiationService.set(ILogService, logService);114instantiationService.set(ITelemetryService, telemetryService);115languageService = disposables.add(instantiationService.createInstance(LanguageService));116instantiationService.set(ILanguageService, languageService);117instantiationService.set(IThemeService, themeService);118textResourcePropertiesService = instantiationService.createInstance(TestTextResourcePropertiesService);119instantiationService.set(ITextResourcePropertiesService, textResourcePropertiesService);120languageConfigurationService = disposables.add(instantiationService.createInstance(TestLanguageConfigurationService));121instantiationService.set(ILanguageConfigurationService, languageConfigurationService);122123fileService = disposables.add(instantiationService.createInstance(FileService));124const diskFileSystemProvider = disposables.add(new DiskFileSystemProvider(logService));125disposables.add(fileService.registerProvider(Schemas.file, diskFileSystemProvider));126instantiationService.set(IFileService, fileService);127128const libraryService = disposables.add(instantiationService.createInstance(TreeSitterLibraryService));129libraryService.isTest = true;130instantiationService.set(ITreeSitterLibraryService, libraryService);131132instantiationService.set(ITreeSitterThemeService, instantiationService.createInstance(TreeSitterThemeService));133134const dialogService = new TestDialogService();135const notificationService = new TestNotificationService();136const undoRedoService = new UndoRedoService(dialogService, notificationService);137instantiationService.set(IUndoRedoService, undoRedoService);138modelService = new ModelService(139configurationService,140textResourcePropertiesService,141undoRedoService,142instantiationService143);144instantiationService.set(IModelService, modelService);145});146147teardown(() => {148disposables.dispose();149});150151ensureNoDisposablesAreLeakedInTestSuite();152153function tokensContentSize(tokens: TokenUpdate[]) {154return tokens[tokens.length - 1].startOffsetInclusive + tokens[tokens.length - 1].length;155}156157let nameNumber = 1;158async function getModelAndPrepTree(content: string): Promise<{ model: ITextModel; treeSitterTree: TreeSitterTree; tokenizationImpl: TreeSitterTokenizationImpl }> {159const model = disposables.add(modelService.createModel(content, { languageId: 'typescript', onDidChange: Event.None }, URI.file(`file${nameNumber++}.ts`)));160const treeSitterTreeObs = disposables.add((model.tokenization as TokenizationTextModelPart).tokens.get() as TreeSitterSyntaxTokenBackend).tree;161const tokenizationImplObs = disposables.add((model.tokenization as TokenizationTextModelPart).tokens.get() as TreeSitterSyntaxTokenBackend).tokenizationImpl;162const treeSitterTree = treeSitterTreeObs.get() ?? await waitForState(treeSitterTreeObs);163if (!treeSitterTree.tree.get()) {164await waitForState(treeSitterTree.tree);165}166const tokenizationImpl = tokenizationImplObs.get() ?? await waitForState(tokenizationImplObs);167168assert.ok(treeSitterTree);169return { model, treeSitterTree, tokenizationImpl };170}171172function verifyTokens(tokens: TokenUpdate[] | undefined) {173assert.ok(tokens);174for (let i = 1; i < tokens.length; i++) {175const previousToken: TokenUpdate = tokens[i - 1];176const token: TokenUpdate = tokens[i];177assert.deepStrictEqual(previousToken.startOffsetInclusive + previousToken.length, token.startOffsetInclusive);178}179}180181test('Three changes come back to back ', async () => {182const content = `/**183**/184class x {185}186187188189190class y {191}`;192const { model, treeSitterTree } = await getModelAndPrepTree(content);193194let updateListener: IDisposable | undefined;195const changePromise = new Promise<TreeParseUpdateEvent | undefined>(resolve => {196updateListener = autorunHandleChanges({197owner: this,198changeTracker: recordChanges({ tree: treeSitterTree.tree }),199}, (reader, ctx) => {200const changeEvent = ctx.changes.at(0)?.change;201if (changeEvent) {202resolve(changeEvent);203}204});205});206207const edit1 = new Promise<void>(resolve => {208model.applyEdits([{ range: new Range(7, 1, 8, 1), text: '' }]);209resolve();210});211const edit2 = new Promise<void>(resolve => {212model.applyEdits([{ range: new Range(6, 1, 7, 1), text: '' }]);213resolve();214});215const edit3 = new Promise<void>(resolve => {216model.applyEdits([{ range: new Range(5, 1, 6, 1), text: '' }]);217resolve();218});219const edits = Promise.all([edit1, edit2, edit3]);220const change = await changePromise;221await edits;222assert.ok(change);223224assert.strictEqual(change.versionId, 4);225assert.strictEqual(change.ranges[0].newRangeStartOffset, 0);226assert.strictEqual(change.ranges[0].newRangeEndOffset, 32);227assert.strictEqual(change.ranges[0].newRange.startLineNumber, 1);228assert.strictEqual(change.ranges[0].newRange.endLineNumber, 7);229230updateListener?.dispose();231modelService.destroyModel(model.uri);232});233234test('File single line file', async () => {235const content = `console.log('x');`;236const { model, tokenizationImpl } = await getModelAndPrepTree(content);237const tokens = tokenizationImpl.getTokensInRange(new Range(1, 1, 1, 18), 0, 17);238verifyTokens(tokens);239assert.deepStrictEqual(tokens?.length, 9);240assert.deepStrictEqual(tokensContentSize(tokens), content.length);241modelService.destroyModel(model.uri);242});243244test('File with new lines at beginning and end', async () => {245const content = `246console.log('x');247`;248const { model, tokenizationImpl } = await getModelAndPrepTree(content);249const tokens = tokenizationImpl.getTokensInRange(new Range(1, 1, 3, 1), 0, 19);250verifyTokens(tokens);251assert.deepStrictEqual(tokens?.length, 11);252assert.deepStrictEqual(tokensContentSize(tokens), content.length);253modelService.destroyModel(model.uri);254});255256test('File with new lines at beginning and end \\r\\n', async () => {257const content = '\r\nconsole.log(\'x\');\r\n';258const { model, tokenizationImpl } = await getModelAndPrepTree(content);259const tokens = tokenizationImpl.getTokensInRange(new Range(1, 1, 3, 1), 0, 21);260verifyTokens(tokens);261assert.deepStrictEqual(tokens?.length, 11);262assert.deepStrictEqual(tokensContentSize(tokens), content.length);263modelService.destroyModel(model.uri);264});265266test('File with empty lines in the middle', async () => {267const content = `268console.log('x');269270console.log('7');271`;272const { model, tokenizationImpl } = await getModelAndPrepTree(content);273const tokens = tokenizationImpl.getTokensInRange(new Range(1, 1, 5, 1), 0, 38);274verifyTokens(tokens);275assert.deepStrictEqual(tokens?.length, 21);276assert.deepStrictEqual(tokensContentSize(tokens), content.length);277modelService.destroyModel(model.uri);278});279280test('File with empty lines in the middle \\r\\n', async () => {281const content = '\r\nconsole.log(\'x\');\r\n\r\nconsole.log(\'7\');\r\n';282const { model, tokenizationImpl } = await getModelAndPrepTree(content);283const tokens = tokenizationImpl.getTokensInRange(new Range(1, 1, 5, 1), 0, 42);284verifyTokens(tokens);285assert.deepStrictEqual(tokens?.length, 21);286assert.deepStrictEqual(tokensContentSize(tokens), content.length);287modelService.destroyModel(model.uri);288});289290test('File with non-empty lines that match no scopes', async () => {291const content = `console.log('x');292;293{294}295`;296const { model, tokenizationImpl } = await getModelAndPrepTree(content);297const tokens = tokenizationImpl.getTokensInRange(new Range(1, 1, 5, 1), 0, 24);298verifyTokens(tokens);299assert.deepStrictEqual(tokens?.length, 16);300assert.deepStrictEqual(tokensContentSize(tokens), content.length);301modelService.destroyModel(model.uri);302});303304test('File with non-empty lines that match no scopes \\r\\n', async () => {305const content = 'console.log(\'x\');\r\n;\r\n{\r\n}\r\n';306const { model, tokenizationImpl } = await getModelAndPrepTree(content);307const tokens = tokenizationImpl.getTokensInRange(new Range(1, 1, 5, 1), 0, 28);308verifyTokens(tokens);309assert.deepStrictEqual(tokens?.length, 16);310assert.deepStrictEqual(tokensContentSize(tokens), content.length);311modelService.destroyModel(model.uri);312});313314test('File with tree-sitter token that spans multiple lines', async () => {315const content = `/**316**/317318console.log('x');319320`;321const { model, tokenizationImpl } = await getModelAndPrepTree(content);322const tokens = tokenizationImpl.getTokensInRange(new Range(1, 1, 6, 1), 0, 28);323verifyTokens(tokens);324assert.deepStrictEqual(tokens?.length, 12);325assert.deepStrictEqual(tokensContentSize(tokens), content.length);326modelService.destroyModel(model.uri);327});328329test('File with tree-sitter token that spans multiple lines \\r\\n', async () => {330const content = '/**\r\n**/\r\n\r\nconsole.log(\'x\');\r\n\r\n';331const { model, tokenizationImpl } = await getModelAndPrepTree(content);332const tokens = tokenizationImpl.getTokensInRange(new Range(1, 1, 6, 1), 0, 33);333verifyTokens(tokens);334assert.deepStrictEqual(tokens?.length, 12);335assert.deepStrictEqual(tokensContentSize(tokens), content.length);336modelService.destroyModel(model.uri);337});338339test('File with tabs', async () => {340const content = `function x() {341return true;342}343344class Y {345private z = false;346}`;347const { model, tokenizationImpl } = await getModelAndPrepTree(content);348const tokens = tokenizationImpl.getTokensInRange(new Range(1, 1, 7, 1), 0, 63);349verifyTokens(tokens);350assert.deepStrictEqual(tokens?.length, 30);351assert.deepStrictEqual(tokensContentSize(tokens), content.length);352modelService.destroyModel(model.uri);353});354355test('File with tabs \\r\\n', async () => {356const content = 'function x() {\r\n\treturn true;\r\n}\r\n\r\nclass Y {\r\n\tprivate z = false;\r\n}';357const { model, tokenizationImpl } = await getModelAndPrepTree(content);358const tokens = tokenizationImpl.getTokensInRange(new Range(1, 1, 7, 1), 0, 69);359verifyTokens(tokens);360assert.deepStrictEqual(tokens?.length, 30);361assert.deepStrictEqual(tokensContentSize(tokens), content.length);362modelService.destroyModel(model.uri);363});364365test('Template string', async () => {366const content = '`t ${6}`';367const { model, tokenizationImpl } = await getModelAndPrepTree(content);368const tokens = tokenizationImpl.getTokensInRange(new Range(1, 1, 1, 8), 0, 8);369verifyTokens(tokens);370assert.deepStrictEqual(tokens?.length, 6);371assert.deepStrictEqual(tokensContentSize(tokens), content.length);372modelService.destroyModel(model.uri);373});374375test('Many nested scopes', async () => {376const content = `y = new x(ttt({377message: '{0} i\\n\\n [commandName]({1}).',378args: ['Test', \`command:\${openSettingsCommand}?\${encodeURIComponent('["SettingName"]')}\`],379// To make sure the translators don't break the link380comment: ["{Locked=']({'}"]381}));`;382const { model, tokenizationImpl } = await getModelAndPrepTree(content);383const tokens = tokenizationImpl.getTokensInRange(new Range(1, 1, 6, 5), 0, 238);384verifyTokens(tokens);385assert.deepStrictEqual(tokens?.length, 65);386assert.deepStrictEqual(tokensContentSize(tokens), content.length);387modelService.destroyModel(model.uri);388});389390});391392393