Path: blob/main/src/vs/editor/contrib/semanticTokens/browser/documentSemanticTokens.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 { RunOnceScheduler } from '../../../../base/common/async.js';6import { CancellationTokenSource } from '../../../../base/common/cancellation.js';7import * as errors from '../../../../base/common/errors.js';8import { Disposable, IDisposable, dispose } from '../../../../base/common/lifecycle.js';9import { ResourceMap } from '../../../../base/common/map.js';10import { StopWatch } from '../../../../base/common/stopwatch.js';11import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';12import { IThemeService } from '../../../../platform/theme/common/themeService.js';13import { registerEditorFeature } from '../../../common/editorFeatures.js';14import { LanguageFeatureRegistry } from '../../../common/languageFeatureRegistry.js';15import { DocumentSemanticTokensProvider, SemanticTokens, SemanticTokensEdits } from '../../../common/languages.js';16import { ITextModel } from '../../../common/model.js';17import { IFeatureDebounceInformation, ILanguageFeatureDebounceService } from '../../../common/services/languageFeatureDebounce.js';18import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js';19import { IModelService } from '../../../common/services/model.js';20import { SemanticTokensProviderStyling, toMultilineTokens2 } from '../../../common/services/semanticTokensProviderStyling.js';21import { ISemanticTokensStylingService } from '../../../common/services/semanticTokensStyling.js';22import { IModelContentChangedEvent } from '../../../common/textModelEvents.js';23import { getDocumentSemanticTokens, hasDocumentSemanticTokensProvider, isSemanticTokens, isSemanticTokensEdits } from '../common/getSemanticTokens.js';24import { SEMANTIC_HIGHLIGHTING_SETTING_ID, isSemanticColoringEnabled } from '../common/semanticTokensConfig.js';2526export class DocumentSemanticTokensFeature extends Disposable {2728private readonly _watchers = new ResourceMap<ModelSemanticColoring>();2930constructor(31@ISemanticTokensStylingService semanticTokensStylingService: ISemanticTokensStylingService,32@IModelService modelService: IModelService,33@IThemeService themeService: IThemeService,34@IConfigurationService configurationService: IConfigurationService,35@ILanguageFeatureDebounceService languageFeatureDebounceService: ILanguageFeatureDebounceService,36@ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService,37) {38super();3940const register = (model: ITextModel) => {41this._watchers.get(model.uri)?.dispose();42this._watchers.set(model.uri, new ModelSemanticColoring(model, semanticTokensStylingService, themeService, languageFeatureDebounceService, languageFeaturesService));43};44const deregister = (model: ITextModel, modelSemanticColoring: ModelSemanticColoring) => {45modelSemanticColoring.dispose();46this._watchers.delete(model.uri);47};48const handleSettingOrThemeChange = () => {49for (const model of modelService.getModels()) {50const curr = this._watchers.get(model.uri);51if (isSemanticColoringEnabled(model, themeService, configurationService)) {52if (!curr) {53register(model);54}55} else {56if (curr) {57deregister(model, curr);58}59}60}61};62modelService.getModels().forEach(model => {63if (isSemanticColoringEnabled(model, themeService, configurationService)) {64register(model);65}66});67this._register(modelService.onModelAdded((model) => {68if (isSemanticColoringEnabled(model, themeService, configurationService)) {69register(model);70}71}));72this._register(modelService.onModelRemoved((model) => {73const curr = this._watchers.get(model.uri);74if (curr) {75deregister(model, curr);76}77}));78this._register(configurationService.onDidChangeConfiguration(e => {79if (e.affectsConfiguration(SEMANTIC_HIGHLIGHTING_SETTING_ID)) {80handleSettingOrThemeChange();81}82}));83this._register(themeService.onDidColorThemeChange(handleSettingOrThemeChange));84}8586override dispose(): void {87dispose(this._watchers.values());88this._watchers.clear();8990super.dispose();91}92}9394class ModelSemanticColoring extends Disposable {9596public static REQUEST_MIN_DELAY = 300;97public static REQUEST_MAX_DELAY = 2000;9899private _isDisposed: boolean;100private readonly _model: ITextModel;101private readonly _provider: LanguageFeatureRegistry<DocumentSemanticTokensProvider>;102private readonly _debounceInformation: IFeatureDebounceInformation;103private readonly _fetchDocumentSemanticTokens: RunOnceScheduler;104private _currentDocumentResponse: SemanticTokensResponse | null;105private _currentDocumentRequestCancellationTokenSource: CancellationTokenSource | null;106private _documentProvidersChangeListeners: IDisposable[];107private _providersChangedDuringRequest: boolean;108109constructor(110model: ITextModel,111@ISemanticTokensStylingService private readonly _semanticTokensStylingService: ISemanticTokensStylingService,112@IThemeService themeService: IThemeService,113@ILanguageFeatureDebounceService languageFeatureDebounceService: ILanguageFeatureDebounceService,114@ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService,115) {116super();117118this._isDisposed = false;119this._model = model;120this._provider = languageFeaturesService.documentSemanticTokensProvider;121this._debounceInformation = languageFeatureDebounceService.for(this._provider, 'DocumentSemanticTokens', { min: ModelSemanticColoring.REQUEST_MIN_DELAY, max: ModelSemanticColoring.REQUEST_MAX_DELAY });122this._fetchDocumentSemanticTokens = this._register(new RunOnceScheduler(() => this._fetchDocumentSemanticTokensNow(), ModelSemanticColoring.REQUEST_MIN_DELAY));123this._currentDocumentResponse = null;124this._currentDocumentRequestCancellationTokenSource = null;125this._documentProvidersChangeListeners = [];126this._providersChangedDuringRequest = false;127128this._register(this._model.onDidChangeContent(() => {129if (!this._fetchDocumentSemanticTokens.isScheduled()) {130this._fetchDocumentSemanticTokens.schedule(this._debounceInformation.get(this._model));131}132}));133this._register(this._model.onDidChangeAttached(() => {134if (!this._fetchDocumentSemanticTokens.isScheduled()) {135this._fetchDocumentSemanticTokens.schedule(this._debounceInformation.get(this._model));136}137}));138this._register(this._model.onDidChangeLanguage(() => {139// clear any outstanding state140if (this._currentDocumentResponse) {141this._currentDocumentResponse.dispose();142this._currentDocumentResponse = null;143}144if (this._currentDocumentRequestCancellationTokenSource) {145this._currentDocumentRequestCancellationTokenSource.cancel();146this._currentDocumentRequestCancellationTokenSource = null;147}148this._setDocumentSemanticTokens(null, null, null, []);149this._fetchDocumentSemanticTokens.schedule(0);150}));151152const bindDocumentChangeListeners = () => {153dispose(this._documentProvidersChangeListeners);154this._documentProvidersChangeListeners = [];155for (const provider of this._provider.all(model)) {156if (typeof provider.onDidChange === 'function') {157this._documentProvidersChangeListeners.push(provider.onDidChange(() => {158if (this._currentDocumentRequestCancellationTokenSource) {159// there is already a request running,160this._providersChangedDuringRequest = true;161return;162}163this._fetchDocumentSemanticTokens.schedule(0);164}));165}166}167};168bindDocumentChangeListeners();169this._register(this._provider.onDidChange(() => {170bindDocumentChangeListeners();171this._fetchDocumentSemanticTokens.schedule(this._debounceInformation.get(this._model));172}));173174this._register(themeService.onDidColorThemeChange(_ => {175// clear out existing tokens176this._setDocumentSemanticTokens(null, null, null, []);177this._fetchDocumentSemanticTokens.schedule(this._debounceInformation.get(this._model));178}));179180this._fetchDocumentSemanticTokens.schedule(0);181}182183public override dispose(): void {184if (this._currentDocumentResponse) {185this._currentDocumentResponse.dispose();186this._currentDocumentResponse = null;187}188if (this._currentDocumentRequestCancellationTokenSource) {189this._currentDocumentRequestCancellationTokenSource.cancel();190this._currentDocumentRequestCancellationTokenSource = null;191}192dispose(this._documentProvidersChangeListeners);193this._documentProvidersChangeListeners = [];194this._setDocumentSemanticTokens(null, null, null, []);195this._isDisposed = true;196197super.dispose();198}199200private _fetchDocumentSemanticTokensNow(): void {201if (this._currentDocumentRequestCancellationTokenSource) {202// there is already a request running, let it finish...203return;204}205206if (!hasDocumentSemanticTokensProvider(this._provider, this._model)) {207// there is no provider208if (this._currentDocumentResponse) {209// there are semantic tokens set210this._model.tokenization.setSemanticTokens(null, false);211}212return;213}214215if (!this._model.isAttachedToEditor()) {216// this document is not visible, there is no need to fetch semantic tokens for it217return;218}219220const cancellationTokenSource = new CancellationTokenSource();221const lastProvider = this._currentDocumentResponse ? this._currentDocumentResponse.provider : null;222const lastResultId = this._currentDocumentResponse ? this._currentDocumentResponse.resultId || null : null;223const request = getDocumentSemanticTokens(this._provider, this._model, lastProvider, lastResultId, cancellationTokenSource.token);224this._currentDocumentRequestCancellationTokenSource = cancellationTokenSource;225this._providersChangedDuringRequest = false;226227const pendingChanges: IModelContentChangedEvent[] = [];228const contentChangeListener = this._model.onDidChangeContent((e) => {229pendingChanges.push(e);230});231232const sw = new StopWatch(false);233request.then((res) => {234this._debounceInformation.update(this._model, sw.elapsed());235this._currentDocumentRequestCancellationTokenSource = null;236contentChangeListener.dispose();237238if (!res) {239this._setDocumentSemanticTokens(null, null, null, pendingChanges);240} else {241const { provider, tokens } = res;242const styling = this._semanticTokensStylingService.getStyling(provider);243this._setDocumentSemanticTokens(provider, tokens || null, styling, pendingChanges);244}245}, (err) => {246const isExpectedError = err && (errors.isCancellationError(err) || (typeof err.message === 'string' && err.message.indexOf('busy') !== -1));247if (!isExpectedError) {248errors.onUnexpectedError(err);249}250251// Semantic tokens eats up all errors and considers errors to mean that the result is temporarily not available252// The API does not have a special error kind to express this...253this._currentDocumentRequestCancellationTokenSource = null;254contentChangeListener.dispose();255256if (pendingChanges.length > 0 || this._providersChangedDuringRequest) {257// More changes occurred while the request was running258if (!this._fetchDocumentSemanticTokens.isScheduled()) {259this._fetchDocumentSemanticTokens.schedule(this._debounceInformation.get(this._model));260}261}262});263}264265private static _copy(src: Uint32Array, srcOffset: number, dest: Uint32Array, destOffset: number, length: number): void {266// protect against overflows267length = Math.min(length, dest.length - destOffset, src.length - srcOffset);268for (let i = 0; i < length; i++) {269dest[destOffset + i] = src[srcOffset + i];270}271}272273private _setDocumentSemanticTokens(provider: DocumentSemanticTokensProvider | null, tokens: SemanticTokens | SemanticTokensEdits | null, styling: SemanticTokensProviderStyling | null, pendingChanges: IModelContentChangedEvent[]): void {274const currentResponse = this._currentDocumentResponse;275const rescheduleIfNeeded = () => {276if ((pendingChanges.length > 0 || this._providersChangedDuringRequest) && !this._fetchDocumentSemanticTokens.isScheduled()) {277this._fetchDocumentSemanticTokens.schedule(this._debounceInformation.get(this._model));278}279};280281if (this._currentDocumentResponse) {282this._currentDocumentResponse.dispose();283this._currentDocumentResponse = null;284}285if (this._isDisposed) {286// disposed!287if (provider && tokens) {288provider.releaseDocumentSemanticTokens(tokens.resultId);289}290return;291}292if (!provider || !styling) {293this._model.tokenization.setSemanticTokens(null, false);294return;295}296if (!tokens) {297this._model.tokenization.setSemanticTokens(null, true);298rescheduleIfNeeded();299return;300}301302if (isSemanticTokensEdits(tokens)) {303if (!currentResponse) {304// not possible!305this._model.tokenization.setSemanticTokens(null, true);306return;307}308if (tokens.edits.length === 0) {309// nothing to do!310tokens = {311resultId: tokens.resultId,312data: currentResponse.data313};314} else {315let deltaLength = 0;316for (const edit of tokens.edits) {317deltaLength += (edit.data ? edit.data.length : 0) - edit.deleteCount;318}319320const srcData = currentResponse.data;321const destData = new Uint32Array(srcData.length + deltaLength);322323let srcLastStart = srcData.length;324let destLastStart = destData.length;325for (let i = tokens.edits.length - 1; i >= 0; i--) {326const edit = tokens.edits[i];327328if (edit.start > srcData.length) {329styling.warnInvalidEditStart(currentResponse.resultId, tokens.resultId, i, edit.start, srcData.length);330// The edits are invalid and there's no way to recover331this._model.tokenization.setSemanticTokens(null, true);332return;333}334335const copyCount = srcLastStart - (edit.start + edit.deleteCount);336if (copyCount > 0) {337ModelSemanticColoring._copy(srcData, srcLastStart - copyCount, destData, destLastStart - copyCount, copyCount);338destLastStart -= copyCount;339}340341if (edit.data) {342ModelSemanticColoring._copy(edit.data, 0, destData, destLastStart - edit.data.length, edit.data.length);343destLastStart -= edit.data.length;344}345346srcLastStart = edit.start;347}348349if (srcLastStart > 0) {350ModelSemanticColoring._copy(srcData, 0, destData, 0, srcLastStart);351}352353tokens = {354resultId: tokens.resultId,355data: destData356};357}358}359360if (isSemanticTokens(tokens)) {361362this._currentDocumentResponse = new SemanticTokensResponse(provider, tokens.resultId, tokens.data);363364const result = toMultilineTokens2(tokens, styling, this._model.getLanguageId());365366// Adjust incoming semantic tokens367if (pendingChanges.length > 0) {368// More changes occurred while the request was running369// We need to:370// 1. Adjust incoming semantic tokens371// 2. Request them again372for (const change of pendingChanges) {373for (const area of result) {374for (const singleChange of change.changes) {375area.applyEdit(singleChange.range, singleChange.text);376}377}378}379}380381this._model.tokenization.setSemanticTokens(result, true);382} else {383this._model.tokenization.setSemanticTokens(null, true);384}385386rescheduleIfNeeded();387}388}389390class SemanticTokensResponse {391constructor(392public readonly provider: DocumentSemanticTokensProvider,393public readonly resultId: string | undefined,394public readonly data: Uint32Array395) { }396397public dispose(): void {398this.provider.releaseDocumentSemanticTokens(this.resultId);399}400}401402registerEditorFeature(DocumentSemanticTokensFeature);403404405