Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/semanticTokens/browser/documentSemanticTokens.ts
3296 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 { RunOnceScheduler } from '../../../../base/common/async.js';
7
import { CancellationTokenSource } from '../../../../base/common/cancellation.js';
8
import * as errors from '../../../../base/common/errors.js';
9
import { Disposable, IDisposable, dispose } from '../../../../base/common/lifecycle.js';
10
import { ResourceMap } from '../../../../base/common/map.js';
11
import { StopWatch } from '../../../../base/common/stopwatch.js';
12
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
13
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
14
import { registerEditorFeature } from '../../../common/editorFeatures.js';
15
import { LanguageFeatureRegistry } from '../../../common/languageFeatureRegistry.js';
16
import { DocumentSemanticTokensProvider, SemanticTokens, SemanticTokensEdits } from '../../../common/languages.js';
17
import { ITextModel } from '../../../common/model.js';
18
import { IFeatureDebounceInformation, ILanguageFeatureDebounceService } from '../../../common/services/languageFeatureDebounce.js';
19
import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js';
20
import { IModelService } from '../../../common/services/model.js';
21
import { SemanticTokensProviderStyling, toMultilineTokens2 } from '../../../common/services/semanticTokensProviderStyling.js';
22
import { ISemanticTokensStylingService } from '../../../common/services/semanticTokensStyling.js';
23
import { IModelContentChangedEvent } from '../../../common/textModelEvents.js';
24
import { getDocumentSemanticTokens, hasDocumentSemanticTokensProvider, isSemanticTokens, isSemanticTokensEdits } from '../common/getSemanticTokens.js';
25
import { SEMANTIC_HIGHLIGHTING_SETTING_ID, isSemanticColoringEnabled } from '../common/semanticTokensConfig.js';
26
27
export class DocumentSemanticTokensFeature extends Disposable {
28
29
private readonly _watchers = new ResourceMap<ModelSemanticColoring>();
30
31
constructor(
32
@ISemanticTokensStylingService semanticTokensStylingService: ISemanticTokensStylingService,
33
@IModelService modelService: IModelService,
34
@IThemeService themeService: IThemeService,
35
@IConfigurationService configurationService: IConfigurationService,
36
@ILanguageFeatureDebounceService languageFeatureDebounceService: ILanguageFeatureDebounceService,
37
@ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService,
38
) {
39
super();
40
41
const register = (model: ITextModel) => {
42
this._watchers.get(model.uri)?.dispose();
43
this._watchers.set(model.uri, new ModelSemanticColoring(model, semanticTokensStylingService, themeService, languageFeatureDebounceService, languageFeaturesService));
44
};
45
const deregister = (model: ITextModel, modelSemanticColoring: ModelSemanticColoring) => {
46
modelSemanticColoring.dispose();
47
this._watchers.delete(model.uri);
48
};
49
const handleSettingOrThemeChange = () => {
50
for (const model of modelService.getModels()) {
51
const curr = this._watchers.get(model.uri);
52
if (isSemanticColoringEnabled(model, themeService, configurationService)) {
53
if (!curr) {
54
register(model);
55
}
56
} else {
57
if (curr) {
58
deregister(model, curr);
59
}
60
}
61
}
62
};
63
modelService.getModels().forEach(model => {
64
if (isSemanticColoringEnabled(model, themeService, configurationService)) {
65
register(model);
66
}
67
});
68
this._register(modelService.onModelAdded((model) => {
69
if (isSemanticColoringEnabled(model, themeService, configurationService)) {
70
register(model);
71
}
72
}));
73
this._register(modelService.onModelRemoved((model) => {
74
const curr = this._watchers.get(model.uri);
75
if (curr) {
76
deregister(model, curr);
77
}
78
}));
79
this._register(configurationService.onDidChangeConfiguration(e => {
80
if (e.affectsConfiguration(SEMANTIC_HIGHLIGHTING_SETTING_ID)) {
81
handleSettingOrThemeChange();
82
}
83
}));
84
this._register(themeService.onDidColorThemeChange(handleSettingOrThemeChange));
85
}
86
87
override dispose(): void {
88
dispose(this._watchers.values());
89
this._watchers.clear();
90
91
super.dispose();
92
}
93
}
94
95
class ModelSemanticColoring extends Disposable {
96
97
public static REQUEST_MIN_DELAY = 300;
98
public static REQUEST_MAX_DELAY = 2000;
99
100
private _isDisposed: boolean;
101
private readonly _model: ITextModel;
102
private readonly _provider: LanguageFeatureRegistry<DocumentSemanticTokensProvider>;
103
private readonly _debounceInformation: IFeatureDebounceInformation;
104
private readonly _fetchDocumentSemanticTokens: RunOnceScheduler;
105
private _currentDocumentResponse: SemanticTokensResponse | null;
106
private _currentDocumentRequestCancellationTokenSource: CancellationTokenSource | null;
107
private _documentProvidersChangeListeners: IDisposable[];
108
private _providersChangedDuringRequest: boolean;
109
110
constructor(
111
model: ITextModel,
112
@ISemanticTokensStylingService private readonly _semanticTokensStylingService: ISemanticTokensStylingService,
113
@IThemeService themeService: IThemeService,
114
@ILanguageFeatureDebounceService languageFeatureDebounceService: ILanguageFeatureDebounceService,
115
@ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService,
116
) {
117
super();
118
119
this._isDisposed = false;
120
this._model = model;
121
this._provider = languageFeaturesService.documentSemanticTokensProvider;
122
this._debounceInformation = languageFeatureDebounceService.for(this._provider, 'DocumentSemanticTokens', { min: ModelSemanticColoring.REQUEST_MIN_DELAY, max: ModelSemanticColoring.REQUEST_MAX_DELAY });
123
this._fetchDocumentSemanticTokens = this._register(new RunOnceScheduler(() => this._fetchDocumentSemanticTokensNow(), ModelSemanticColoring.REQUEST_MIN_DELAY));
124
this._currentDocumentResponse = null;
125
this._currentDocumentRequestCancellationTokenSource = null;
126
this._documentProvidersChangeListeners = [];
127
this._providersChangedDuringRequest = false;
128
129
this._register(this._model.onDidChangeContent(() => {
130
if (!this._fetchDocumentSemanticTokens.isScheduled()) {
131
this._fetchDocumentSemanticTokens.schedule(this._debounceInformation.get(this._model));
132
}
133
}));
134
this._register(this._model.onDidChangeAttached(() => {
135
if (!this._fetchDocumentSemanticTokens.isScheduled()) {
136
this._fetchDocumentSemanticTokens.schedule(this._debounceInformation.get(this._model));
137
}
138
}));
139
this._register(this._model.onDidChangeLanguage(() => {
140
// clear any outstanding state
141
if (this._currentDocumentResponse) {
142
this._currentDocumentResponse.dispose();
143
this._currentDocumentResponse = null;
144
}
145
if (this._currentDocumentRequestCancellationTokenSource) {
146
this._currentDocumentRequestCancellationTokenSource.cancel();
147
this._currentDocumentRequestCancellationTokenSource = null;
148
}
149
this._setDocumentSemanticTokens(null, null, null, []);
150
this._fetchDocumentSemanticTokens.schedule(0);
151
}));
152
153
const bindDocumentChangeListeners = () => {
154
dispose(this._documentProvidersChangeListeners);
155
this._documentProvidersChangeListeners = [];
156
for (const provider of this._provider.all(model)) {
157
if (typeof provider.onDidChange === 'function') {
158
this._documentProvidersChangeListeners.push(provider.onDidChange(() => {
159
if (this._currentDocumentRequestCancellationTokenSource) {
160
// there is already a request running,
161
this._providersChangedDuringRequest = true;
162
return;
163
}
164
this._fetchDocumentSemanticTokens.schedule(0);
165
}));
166
}
167
}
168
};
169
bindDocumentChangeListeners();
170
this._register(this._provider.onDidChange(() => {
171
bindDocumentChangeListeners();
172
this._fetchDocumentSemanticTokens.schedule(this._debounceInformation.get(this._model));
173
}));
174
175
this._register(themeService.onDidColorThemeChange(_ => {
176
// clear out existing tokens
177
this._setDocumentSemanticTokens(null, null, null, []);
178
this._fetchDocumentSemanticTokens.schedule(this._debounceInformation.get(this._model));
179
}));
180
181
this._fetchDocumentSemanticTokens.schedule(0);
182
}
183
184
public override dispose(): void {
185
if (this._currentDocumentResponse) {
186
this._currentDocumentResponse.dispose();
187
this._currentDocumentResponse = null;
188
}
189
if (this._currentDocumentRequestCancellationTokenSource) {
190
this._currentDocumentRequestCancellationTokenSource.cancel();
191
this._currentDocumentRequestCancellationTokenSource = null;
192
}
193
dispose(this._documentProvidersChangeListeners);
194
this._documentProvidersChangeListeners = [];
195
this._setDocumentSemanticTokens(null, null, null, []);
196
this._isDisposed = true;
197
198
super.dispose();
199
}
200
201
private _fetchDocumentSemanticTokensNow(): void {
202
if (this._currentDocumentRequestCancellationTokenSource) {
203
// there is already a request running, let it finish...
204
return;
205
}
206
207
if (!hasDocumentSemanticTokensProvider(this._provider, this._model)) {
208
// there is no provider
209
if (this._currentDocumentResponse) {
210
// there are semantic tokens set
211
this._model.tokenization.setSemanticTokens(null, false);
212
}
213
return;
214
}
215
216
if (!this._model.isAttachedToEditor()) {
217
// this document is not visible, there is no need to fetch semantic tokens for it
218
return;
219
}
220
221
const cancellationTokenSource = new CancellationTokenSource();
222
const lastProvider = this._currentDocumentResponse ? this._currentDocumentResponse.provider : null;
223
const lastResultId = this._currentDocumentResponse ? this._currentDocumentResponse.resultId || null : null;
224
const request = getDocumentSemanticTokens(this._provider, this._model, lastProvider, lastResultId, cancellationTokenSource.token);
225
this._currentDocumentRequestCancellationTokenSource = cancellationTokenSource;
226
this._providersChangedDuringRequest = false;
227
228
const pendingChanges: IModelContentChangedEvent[] = [];
229
const contentChangeListener = this._model.onDidChangeContent((e) => {
230
pendingChanges.push(e);
231
});
232
233
const sw = new StopWatch(false);
234
request.then((res) => {
235
this._debounceInformation.update(this._model, sw.elapsed());
236
this._currentDocumentRequestCancellationTokenSource = null;
237
contentChangeListener.dispose();
238
239
if (!res) {
240
this._setDocumentSemanticTokens(null, null, null, pendingChanges);
241
} else {
242
const { provider, tokens } = res;
243
const styling = this._semanticTokensStylingService.getStyling(provider);
244
this._setDocumentSemanticTokens(provider, tokens || null, styling, pendingChanges);
245
}
246
}, (err) => {
247
const isExpectedError = err && (errors.isCancellationError(err) || (typeof err.message === 'string' && err.message.indexOf('busy') !== -1));
248
if (!isExpectedError) {
249
errors.onUnexpectedError(err);
250
}
251
252
// Semantic tokens eats up all errors and considers errors to mean that the result is temporarily not available
253
// The API does not have a special error kind to express this...
254
this._currentDocumentRequestCancellationTokenSource = null;
255
contentChangeListener.dispose();
256
257
if (pendingChanges.length > 0 || this._providersChangedDuringRequest) {
258
// More changes occurred while the request was running
259
if (!this._fetchDocumentSemanticTokens.isScheduled()) {
260
this._fetchDocumentSemanticTokens.schedule(this._debounceInformation.get(this._model));
261
}
262
}
263
});
264
}
265
266
private static _copy(src: Uint32Array, srcOffset: number, dest: Uint32Array, destOffset: number, length: number): void {
267
// protect against overflows
268
length = Math.min(length, dest.length - destOffset, src.length - srcOffset);
269
for (let i = 0; i < length; i++) {
270
dest[destOffset + i] = src[srcOffset + i];
271
}
272
}
273
274
private _setDocumentSemanticTokens(provider: DocumentSemanticTokensProvider | null, tokens: SemanticTokens | SemanticTokensEdits | null, styling: SemanticTokensProviderStyling | null, pendingChanges: IModelContentChangedEvent[]): void {
275
const currentResponse = this._currentDocumentResponse;
276
const rescheduleIfNeeded = () => {
277
if ((pendingChanges.length > 0 || this._providersChangedDuringRequest) && !this._fetchDocumentSemanticTokens.isScheduled()) {
278
this._fetchDocumentSemanticTokens.schedule(this._debounceInformation.get(this._model));
279
}
280
};
281
282
if (this._currentDocumentResponse) {
283
this._currentDocumentResponse.dispose();
284
this._currentDocumentResponse = null;
285
}
286
if (this._isDisposed) {
287
// disposed!
288
if (provider && tokens) {
289
provider.releaseDocumentSemanticTokens(tokens.resultId);
290
}
291
return;
292
}
293
if (!provider || !styling) {
294
this._model.tokenization.setSemanticTokens(null, false);
295
return;
296
}
297
if (!tokens) {
298
this._model.tokenization.setSemanticTokens(null, true);
299
rescheduleIfNeeded();
300
return;
301
}
302
303
if (isSemanticTokensEdits(tokens)) {
304
if (!currentResponse) {
305
// not possible!
306
this._model.tokenization.setSemanticTokens(null, true);
307
return;
308
}
309
if (tokens.edits.length === 0) {
310
// nothing to do!
311
tokens = {
312
resultId: tokens.resultId,
313
data: currentResponse.data
314
};
315
} else {
316
let deltaLength = 0;
317
for (const edit of tokens.edits) {
318
deltaLength += (edit.data ? edit.data.length : 0) - edit.deleteCount;
319
}
320
321
const srcData = currentResponse.data;
322
const destData = new Uint32Array(srcData.length + deltaLength);
323
324
let srcLastStart = srcData.length;
325
let destLastStart = destData.length;
326
for (let i = tokens.edits.length - 1; i >= 0; i--) {
327
const edit = tokens.edits[i];
328
329
if (edit.start > srcData.length) {
330
styling.warnInvalidEditStart(currentResponse.resultId, tokens.resultId, i, edit.start, srcData.length);
331
// The edits are invalid and there's no way to recover
332
this._model.tokenization.setSemanticTokens(null, true);
333
return;
334
}
335
336
const copyCount = srcLastStart - (edit.start + edit.deleteCount);
337
if (copyCount > 0) {
338
ModelSemanticColoring._copy(srcData, srcLastStart - copyCount, destData, destLastStart - copyCount, copyCount);
339
destLastStart -= copyCount;
340
}
341
342
if (edit.data) {
343
ModelSemanticColoring._copy(edit.data, 0, destData, destLastStart - edit.data.length, edit.data.length);
344
destLastStart -= edit.data.length;
345
}
346
347
srcLastStart = edit.start;
348
}
349
350
if (srcLastStart > 0) {
351
ModelSemanticColoring._copy(srcData, 0, destData, 0, srcLastStart);
352
}
353
354
tokens = {
355
resultId: tokens.resultId,
356
data: destData
357
};
358
}
359
}
360
361
if (isSemanticTokens(tokens)) {
362
363
this._currentDocumentResponse = new SemanticTokensResponse(provider, tokens.resultId, tokens.data);
364
365
const result = toMultilineTokens2(tokens, styling, this._model.getLanguageId());
366
367
// Adjust incoming semantic tokens
368
if (pendingChanges.length > 0) {
369
// More changes occurred while the request was running
370
// We need to:
371
// 1. Adjust incoming semantic tokens
372
// 2. Request them again
373
for (const change of pendingChanges) {
374
for (const area of result) {
375
for (const singleChange of change.changes) {
376
area.applyEdit(singleChange.range, singleChange.text);
377
}
378
}
379
}
380
}
381
382
this._model.tokenization.setSemanticTokens(result, true);
383
} else {
384
this._model.tokenization.setSemanticTokens(null, true);
385
}
386
387
rescheduleIfNeeded();
388
}
389
}
390
391
class SemanticTokensResponse {
392
constructor(
393
public readonly provider: DocumentSemanticTokensProvider,
394
public readonly resultId: string | undefined,
395
public readonly data: Uint32Array
396
) { }
397
398
public dispose(): void {
399
this.provider.releaseDocumentSemanticTokens(this.resultId);
400
}
401
}
402
403
registerEditorFeature(DocumentSemanticTokensFeature);
404
405