Skip to content

Commit edff076

Browse files
authored
Hookup semantic tokens range refresh notification to the viewport contribution (microsoft#271419)
* Hookup the semantic tokens range refresh notification to the viewport contribution * cleanup event subscription and dispose
1 parent f958c6f commit edff076

File tree

2 files changed

+158
-1
lines changed

2 files changed

+158
-1
lines changed

src/vs/editor/contrib/semanticTokens/browser/viewportSemanticTokens.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { CancelablePromise, createCancelablePromise, RunOnceScheduler } from '../../../../base/common/async.js';
7-
import { Disposable } from '../../../../base/common/lifecycle.js';
7+
import { Disposable, dispose, IDisposable } from '../../../../base/common/lifecycle.js';
88
import { ICodeEditor } from '../../../browser/editorBrowser.js';
99
import { EditorContributionInstantiation, registerEditorContribution } from '../../../browser/editorExtensions.js';
1010
import { Range } from '../../../common/core/range.js';
@@ -35,6 +35,7 @@ export class ViewportSemanticTokensContribution extends Disposable implements IE
3535
private readonly _debounceInformation: IFeatureDebounceInformation;
3636
private readonly _tokenizeViewport: RunOnceScheduler;
3737
private _outstandingRequests: CancelablePromise<any>[];
38+
private _rangeProvidersChangeListeners: IDisposable[];
3839

3940
constructor(
4041
editor: ICodeEditor,
@@ -50,23 +51,44 @@ export class ViewportSemanticTokensContribution extends Disposable implements IE
5051
this._debounceInformation = languageFeatureDebounceService.for(this._provider, 'DocumentRangeSemanticTokens', { min: 100, max: 500 });
5152
this._tokenizeViewport = this._register(new RunOnceScheduler(() => this._tokenizeViewportNow(), 100));
5253
this._outstandingRequests = [];
54+
this._rangeProvidersChangeListeners = [];
5355
const scheduleTokenizeViewport = () => {
5456
if (this._editor.hasModel()) {
5557
this._tokenizeViewport.schedule(this._debounceInformation.get(this._editor.getModel()));
5658
}
5759
};
60+
const bindRangeProvidersChangeListeners = () => {
61+
this._cleanupProviderListeners();
62+
if (this._editor.hasModel()) {
63+
const model = this._editor.getModel();
64+
for (const provider of this._provider.all(model)) {
65+
const disposable = provider.onDidChange?.(() => {
66+
this._cancelAll();
67+
scheduleTokenizeViewport();
68+
});
69+
if (disposable) {
70+
this._rangeProvidersChangeListeners.push(disposable);
71+
}
72+
}
73+
}
74+
};
75+
5876
this._register(this._editor.onDidScrollChange(() => {
5977
scheduleTokenizeViewport();
6078
}));
6179
this._register(this._editor.onDidChangeModel(() => {
80+
bindRangeProvidersChangeListeners();
6281
this._cancelAll();
6382
scheduleTokenizeViewport();
6483
}));
6584
this._register(this._editor.onDidChangeModelContent((e) => {
6685
this._cancelAll();
6786
scheduleTokenizeViewport();
6887
}));
88+
89+
bindRangeProvidersChangeListeners();
6990
this._register(this._provider.onDidChange(() => {
91+
bindRangeProvidersChangeListeners();
7092
this._cancelAll();
7193
scheduleTokenizeViewport();
7294
}));
@@ -83,6 +105,16 @@ export class ViewportSemanticTokensContribution extends Disposable implements IE
83105
scheduleTokenizeViewport();
84106
}
85107

108+
public override dispose(): void {
109+
this._cleanupProviderListeners();
110+
super.dispose();
111+
}
112+
113+
private _cleanupProviderListeners(): void {
114+
dispose(this._rangeProvidersChangeListeners);
115+
this._rangeProvidersChangeListeners = [];
116+
}
117+
86118
private _cancelAll(): void {
87119
for (const request of this._outstandingRequests) {
88120
request.cancel();
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
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 { Barrier, timeout } from '../../../../../base/common/async.js';
8+
import { CancellationToken } from '../../../../../base/common/cancellation.js';
9+
import { Emitter } from '../../../../../base/common/event.js';
10+
import { DisposableStore } from '../../../../../base/common/lifecycle.js';
11+
import { mock } from '../../../../../base/test/common/mock.js';
12+
import { runWithFakedTimers } from '../../../../../base/test/common/timeTravelScheduler.js';
13+
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
14+
import { Range } from '../../../../common/core/range.js';
15+
import { DocumentRangeSemanticTokensProvider, SemanticTokens, SemanticTokensLegend } from '../../../../common/languages.js';
16+
import { ILanguageService } from '../../../../common/languages/language.js';
17+
import { ITextModel } from '../../../../common/model.js';
18+
import { ILanguageFeatureDebounceService, LanguageFeatureDebounceService } from '../../../../common/services/languageFeatureDebounce.js';
19+
import { ILanguageFeaturesService } from '../../../../common/services/languageFeatures.js';
20+
import { LanguageFeaturesService } from '../../../../common/services/languageFeaturesService.js';
21+
import { LanguageService } from '../../../../common/services/languageService.js';
22+
import { ISemanticTokensStylingService } from '../../../../common/services/semanticTokensStyling.js';
23+
import { SemanticTokensStylingService } from '../../../../common/services/semanticTokensStylingService.js';
24+
import { ViewportSemanticTokensContribution } from '../../browser/viewportSemanticTokens.js';
25+
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
26+
import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js';
27+
import { IEnvironmentService } from '../../../../../platform/environment/common/environment.js';
28+
import { NullLogService } from '../../../../../platform/log/common/log.js';
29+
import { ColorScheme } from '../../../../../platform/theme/common/theme.js';
30+
import { IThemeService } from '../../../../../platform/theme/common/themeService.js';
31+
import { TestColorTheme, TestThemeService } from '../../../../../platform/theme/test/common/testThemeService.js';
32+
import { createTextModel } from '../../../../test/common/testTextModel.js';
33+
import { createTestCodeEditor } from '../../../../test/browser/testCodeEditor.js';
34+
import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js';
35+
import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
36+
37+
suite('ViewportSemanticTokens', () => {
38+
39+
const disposables = new DisposableStore();
40+
let languageService: ILanguageService;
41+
let languageFeaturesService: ILanguageFeaturesService;
42+
let serviceCollection: ServiceCollection;
43+
44+
setup(() => {
45+
const configService = new TestConfigurationService({ editor: { semanticHighlighting: true } });
46+
const themeService = new TestThemeService();
47+
themeService.setTheme(new TestColorTheme({}, ColorScheme.DARK, true));
48+
languageFeaturesService = new LanguageFeaturesService();
49+
languageService = disposables.add(new LanguageService(false));
50+
51+
const logService = new NullLogService();
52+
const semanticTokensStylingService = new SemanticTokensStylingService(themeService, logService, languageService);
53+
const envService = new class extends mock<IEnvironmentService>() {
54+
override isBuilt: boolean = true;
55+
override isExtensionDevelopment: boolean = false;
56+
};
57+
const languageFeatureDebounceService = new LanguageFeatureDebounceService(logService, envService);
58+
59+
serviceCollection = new ServiceCollection(
60+
[ILanguageFeaturesService, languageFeaturesService],
61+
[ILanguageFeatureDebounceService, languageFeatureDebounceService],
62+
[ISemanticTokensStylingService, semanticTokensStylingService],
63+
[IThemeService, themeService],
64+
[IConfigurationService, configService]
65+
);
66+
});
67+
68+
teardown(() => {
69+
disposables.clear();
70+
});
71+
72+
ensureNoDisposablesAreLeakedInTestSuite();
73+
74+
test('DocumentRangeSemanticTokens provider onDidChange event should trigger refresh', async () => {
75+
await runWithFakedTimers({}, async () => {
76+
77+
disposables.add(languageService.registerLanguage({ id: 'testMode' }));
78+
79+
const inFirstCall = new Barrier();
80+
const inRefreshCall = new Barrier();
81+
82+
const emitter = new Emitter<void>();
83+
let requestCount = 0;
84+
disposables.add(languageFeaturesService.documentRangeSemanticTokensProvider.register('testMode', new class implements DocumentRangeSemanticTokensProvider {
85+
onDidChange = emitter.event;
86+
getLegend(): SemanticTokensLegend {
87+
return { tokenTypes: ['class'], tokenModifiers: [] };
88+
}
89+
async provideDocumentRangeSemanticTokens(model: ITextModel, range: Range, token: CancellationToken): Promise<SemanticTokens | null> {
90+
requestCount++;
91+
if (requestCount === 1) {
92+
inFirstCall.open();
93+
} else if (requestCount === 2) {
94+
inRefreshCall.open();
95+
}
96+
return {
97+
data: new Uint32Array([0, 1, 1, 1, 1])
98+
};
99+
}
100+
}));
101+
102+
const textModel = disposables.add(createTextModel('Hello world', 'testMode'));
103+
const editor = disposables.add(createTestCodeEditor(textModel, { serviceCollection }));
104+
const instantiationService = new TestInstantiationService(serviceCollection);
105+
disposables.add(instantiationService.createInstance(ViewportSemanticTokensContribution, editor));
106+
107+
textModel.onBeforeAttached();
108+
109+
await inFirstCall.wait();
110+
111+
assert.strictEqual(requestCount, 1, 'Initial request should have been made');
112+
113+
// Make sure no other requests are made for a little while
114+
await timeout(1000);
115+
assert.strictEqual(requestCount, 1, 'No additional requests should have been made');
116+
117+
// Fire the provider's onDidChange event
118+
emitter.fire();
119+
120+
await inRefreshCall.wait();
121+
122+
assert.strictEqual(requestCount, 2, 'Provider onDidChange should trigger a refresh of viewport semantic tokens');
123+
});
124+
});
125+
});

0 commit comments

Comments
 (0)