Path: blob/main/src/vs/editor/contrib/semanticTokens/test/browser/documentSemanticTokens.test.ts
4780 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 { Barrier, timeout } from '../../../../../base/common/async.js';7import { CancellationToken } from '../../../../../base/common/cancellation.js';8import { Emitter, Event } from '../../../../../base/common/event.js';9import { DisposableStore } from '../../../../../base/common/lifecycle.js';10import { mock } from '../../../../../base/test/common/mock.js';11import { runWithFakedTimers } from '../../../../../base/test/common/timeTravelScheduler.js';12import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';13import { Range } from '../../../../common/core/range.js';14import { DocumentSemanticTokensProvider, SemanticTokens, SemanticTokensEdits, SemanticTokensLegend } from '../../../../common/languages.js';15import { ILanguageService } from '../../../../common/languages/language.js';16import { ILanguageConfigurationService } from '../../../../common/languages/languageConfigurationRegistry.js';17import { ITextModel } from '../../../../common/model.js';18import { LanguageFeatureDebounceService } from '../../../../common/services/languageFeatureDebounce.js';19import { ILanguageFeaturesService } from '../../../../common/services/languageFeatures.js';20import { LanguageFeaturesService } from '../../../../common/services/languageFeaturesService.js';21import { LanguageService } from '../../../../common/services/languageService.js';22import { IModelService } from '../../../../common/services/model.js';23import { ModelService } from '../../../../common/services/modelService.js';24import { SemanticTokensStylingService } from '../../../../common/services/semanticTokensStylingService.js';25import { DocumentSemanticTokensFeature } from '../../browser/documentSemanticTokens.js';26import { getDocumentSemanticTokens, isSemanticTokens } from '../../common/getSemanticTokens.js';27import { TestLanguageConfigurationService } from '../../../../test/common/modes/testLanguageConfigurationService.js';28import { TestTextResourcePropertiesService } from '../../../../test/common/services/testTextResourcePropertiesService.js';29import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js';30import { TestDialogService } from '../../../../../platform/dialogs/test/common/testDialogService.js';31import { IEnvironmentService } from '../../../../../platform/environment/common/environment.js';32import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';33import { NullLogService } from '../../../../../platform/log/common/log.js';34import { TestNotificationService } from '../../../../../platform/notification/test/common/testNotificationService.js';35import { ColorScheme } from '../../../../../platform/theme/common/theme.js';36import { TestColorTheme, TestThemeService } from '../../../../../platform/theme/test/common/testThemeService.js';37import { UndoRedoService } from '../../../../../platform/undoRedo/common/undoRedoService.js';38import { ITreeSitterLibraryService } from '../../../../common/services/treeSitter/treeSitterLibraryService.js';39import { TestTreeSitterLibraryService } from '../../../../test/common/services/testTreeSitterLibraryService.js';4041suite('ModelSemanticColoring', () => {4243const disposables = new DisposableStore();44let modelService: IModelService;45let languageService: ILanguageService;46let languageFeaturesService: ILanguageFeaturesService;4748setup(() => {49const configService = new TestConfigurationService({ editor: { semanticHighlighting: true } });50const themeService = new TestThemeService();51themeService.setTheme(new TestColorTheme({}, ColorScheme.DARK, true));52const logService = new NullLogService();53languageFeaturesService = new LanguageFeaturesService();54languageService = disposables.add(new LanguageService(false));55const semanticTokensStylingService = disposables.add(new SemanticTokensStylingService(themeService, logService, languageService));56const instantiationService = new TestInstantiationService();57instantiationService.set(ILanguageService, languageService);58instantiationService.set(ILanguageConfigurationService, new TestLanguageConfigurationService());59instantiationService.set(ITreeSitterLibraryService, new TestTreeSitterLibraryService());60modelService = disposables.add(new ModelService(61configService,62new TestTextResourcePropertiesService(configService),63new UndoRedoService(new TestDialogService(), new TestNotificationService()),64instantiationService65));66const envService = new class extends mock<IEnvironmentService>() {67override isBuilt: boolean = true;68override isExtensionDevelopment: boolean = false;69};70disposables.add(new DocumentSemanticTokensFeature(semanticTokensStylingService, modelService, themeService, configService, new LanguageFeatureDebounceService(logService, envService), languageFeaturesService));71});7273teardown(() => {74disposables.clear();75});7677ensureNoDisposablesAreLeakedInTestSuite();7879test('DocumentSemanticTokens should be fetched when the result is empty if there are pending changes', async () => {80await runWithFakedTimers({}, async () => {8182disposables.add(languageService.registerLanguage({ id: 'testMode' }));8384const inFirstCall = new Barrier();85const delayFirstResult = new Barrier();86const secondResultProvided = new Barrier();87let callCount = 0;8889disposables.add(languageFeaturesService.documentSemanticTokensProvider.register('testMode', new class implements DocumentSemanticTokensProvider {90getLegend(): SemanticTokensLegend {91return { tokenTypes: ['class'], tokenModifiers: [] };92}93async provideDocumentSemanticTokens(model: ITextModel, lastResultId: string | null, token: CancellationToken): Promise<SemanticTokens | SemanticTokensEdits | null> {94callCount++;95if (callCount === 1) {96assert.ok('called once');97inFirstCall.open();98await delayFirstResult.wait();99await timeout(0); // wait for the simple scheduler to fire to check that we do actually get rescheduled100return null;101}102if (callCount === 2) {103assert.ok('called twice');104secondResultProvided.open();105return null;106}107assert.fail('Unexpected call');108}109releaseDocumentSemanticTokens(resultId: string | undefined): void {110}111}));112113const textModel = disposables.add(modelService.createModel('Hello world', languageService.createById('testMode')));114// pretend the text model is attached to an editor (so that semantic tokens are computed)115textModel.onBeforeAttached();116117// wait for the provider to be called118await inFirstCall.wait();119120// the provider is now in the provide call121// change the text buffer while the provider is running122textModel.applyEdits([{ range: new Range(1, 1, 1, 1), text: 'x' }]);123124// let the provider finish its first result125delayFirstResult.open();126127// we need to check that the provider is called again, even if it returns null128await secondResultProvided.wait();129130// assert that it got called twice131assert.strictEqual(callCount, 2);132});133});134135test('issue #149412: VS Code hangs when bad semantic token data is received', async () => {136await runWithFakedTimers({}, async () => {137138disposables.add(languageService.registerLanguage({ id: 'testMode' }));139140let lastResult: SemanticTokens | SemanticTokensEdits | null = null;141142disposables.add(languageFeaturesService.documentSemanticTokensProvider.register('testMode', new class implements DocumentSemanticTokensProvider {143getLegend(): SemanticTokensLegend {144return { tokenTypes: ['class'], tokenModifiers: [] };145}146async provideDocumentSemanticTokens(model: ITextModel, lastResultId: string | null, token: CancellationToken): Promise<SemanticTokens | SemanticTokensEdits | null> {147if (!lastResultId) {148// this is the first call149lastResult = {150resultId: '1',151data: new Uint32Array([4294967293, 0, 7, 16, 0, 1, 4, 3, 11, 1])152};153} else {154// this is the second call155lastResult = {156resultId: '2',157edits: [{158start: 4294967276,159deleteCount: 0,160data: new Uint32Array([2, 0, 3, 11, 0])161}]162};163}164return lastResult;165}166releaseDocumentSemanticTokens(resultId: string | undefined): void {167}168}));169170const textModel = disposables.add(modelService.createModel('', languageService.createById('testMode')));171// pretend the text model is attached to an editor (so that semantic tokens are computed)172textModel.onBeforeAttached();173174// wait for the semantic tokens to be fetched175await Event.toPromise(textModel.onDidChangeTokens);176assert.strictEqual(lastResult!.resultId, '1');177178// edit the text179textModel.applyEdits([{ range: new Range(1, 1, 1, 1), text: 'foo' }]);180181// wait for the semantic tokens to be fetched again182await Event.toPromise(textModel.onDidChangeTokens);183assert.strictEqual(lastResult!.resultId, '2');184});185});186187test('issue #161573: onDidChangeSemanticTokens doesn\'t consistently trigger provideDocumentSemanticTokens', async () => {188await runWithFakedTimers({}, async () => {189190disposables.add(languageService.registerLanguage({ id: 'testMode' }));191192const emitter = new Emitter<void>();193let requestCount = 0;194disposables.add(languageFeaturesService.documentSemanticTokensProvider.register('testMode', new class implements DocumentSemanticTokensProvider {195onDidChange = emitter.event;196getLegend(): SemanticTokensLegend {197return { tokenTypes: ['class'], tokenModifiers: [] };198}199async provideDocumentSemanticTokens(model: ITextModel, lastResultId: string | null, token: CancellationToken): Promise<SemanticTokens | SemanticTokensEdits | null> {200requestCount++;201if (requestCount === 1) {202await timeout(1000);203// send a change event204emitter.fire();205await timeout(1000);206return null;207}208return null;209}210releaseDocumentSemanticTokens(resultId: string | undefined): void {211}212}));213214const textModel = disposables.add(modelService.createModel('', languageService.createById('testMode')));215// pretend the text model is attached to an editor (so that semantic tokens are computed)216textModel.onBeforeAttached();217218await timeout(5000);219assert.deepStrictEqual(requestCount, 2);220});221});222223test('DocumentSemanticTokens should be pick the token provider with actual items', async () => {224await runWithFakedTimers({}, async () => {225226let callCount = 0;227disposables.add(languageService.registerLanguage({ id: 'testMode2' }));228disposables.add(languageFeaturesService.documentSemanticTokensProvider.register('testMode2', new class implements DocumentSemanticTokensProvider {229getLegend(): SemanticTokensLegend {230return { tokenTypes: ['class1'], tokenModifiers: [] };231}232async provideDocumentSemanticTokens(model: ITextModel, lastResultId: string | null, token: CancellationToken): Promise<SemanticTokens | SemanticTokensEdits | null> {233callCount++;234// For a secondary request return a different value235if (lastResultId) {236return {237data: new Uint32Array([2, 1, 1, 1, 1, 0, 2, 1, 1, 1])238};239}240return {241resultId: '1',242data: new Uint32Array([0, 1, 1, 1, 1, 0, 2, 1, 1, 1])243};244}245releaseDocumentSemanticTokens(resultId: string | undefined): void {246}247}));248disposables.add(languageFeaturesService.documentSemanticTokensProvider.register('testMode2', new class implements DocumentSemanticTokensProvider {249getLegend(): SemanticTokensLegend {250return { tokenTypes: ['class2'], tokenModifiers: [] };251}252async provideDocumentSemanticTokens(model: ITextModel, lastResultId: string | null, token: CancellationToken): Promise<SemanticTokens | SemanticTokensEdits | null> {253callCount++;254return null;255}256releaseDocumentSemanticTokens(resultId: string | undefined): void {257}258}));259260function toArr(arr: Uint32Array): number[] {261const result: number[] = [];262for (let i = 0; i < arr.length; i++) {263result[i] = arr[i];264}265return result;266}267268const textModel = modelService.createModel('Hello world 2', languageService.createById('testMode2'));269try {270let result = await getDocumentSemanticTokens(languageFeaturesService.documentSemanticTokensProvider, textModel, null, null, CancellationToken.None);271assert.ok(result, `We should have tokens (1)`);272assert.ok(result.tokens, `Tokens are found from multiple providers (1)`);273assert.ok(isSemanticTokens(result.tokens), `Tokens are full (1)`);274assert.ok(result.tokens.resultId, `Token result id found from multiple providers (1)`);275assert.deepStrictEqual(toArr(result.tokens.data), [0, 1, 1, 1, 1, 0, 2, 1, 1, 1], `Token data returned for multiple providers (1)`);276assert.deepStrictEqual(callCount, 2, `Called both token providers (1)`);277assert.deepStrictEqual(result.provider.getLegend(), { tokenTypes: ['class1'], tokenModifiers: [] }, `Legend matches the tokens (1)`);278279// Make a second request. Make sure we get the secondary value280result = await getDocumentSemanticTokens(languageFeaturesService.documentSemanticTokensProvider, textModel, result.provider, result.tokens.resultId, CancellationToken.None);281assert.ok(result, `We should have tokens (2)`);282assert.ok(result.tokens, `Tokens are found from multiple providers (2)`);283assert.ok(isSemanticTokens(result.tokens), `Tokens are full (2)`);284assert.ok(!result.tokens.resultId, `Token result id found from multiple providers (2)`);285assert.deepStrictEqual(toArr(result.tokens.data), [2, 1, 1, 1, 1, 0, 2, 1, 1, 1], `Token data returned for multiple providers (2)`);286assert.deepStrictEqual(callCount, 4, `Called both token providers (2)`);287assert.deepStrictEqual(result.provider.getLegend(), { tokenTypes: ['class1'], tokenModifiers: [] }, `Legend matches the tokens (2)`);288} finally {289disposables.clear();290291// Wait for scheduler to finish292await timeout(0);293294// Now dispose the text model295textModel.dispose();296}297});298});299});300301302