Path: blob/main/src/vs/editor/browser/services/editorWorkerService.ts
3294 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 { timeout } from '../../../base/common/async.js';6import { Disposable, IDisposable } from '../../../base/common/lifecycle.js';7import { URI } from '../../../base/common/uri.js';8import { logOnceWebWorkerWarning, IWebWorkerClient, Proxied } from '../../../base/common/worker/webWorker.js';9import { createWebWorker, IWebWorkerDescriptor } from '../../../base/browser/webWorkerFactory.js';10import { Position } from '../../common/core/position.js';11import { IRange, Range } from '../../common/core/range.js';12import { ITextModel } from '../../common/model.js';13import * as languages from '../../common/languages.js';14import { ILanguageConfigurationService } from '../../common/languages/languageConfigurationRegistry.js';15import { EditorWorker } from '../../common/services/editorWebWorker.js';16import { DiffAlgorithmName, IEditorWorkerService, ILineChange, IUnicodeHighlightsResult } from '../../common/services/editorWorker.js';17import { IModelService } from '../../common/services/model.js';18import { ITextResourceConfigurationService } from '../../common/services/textResourceConfiguration.js';19import { isNonEmptyArray } from '../../../base/common/arrays.js';20import { ILogService } from '../../../platform/log/common/log.js';21import { StopWatch } from '../../../base/common/stopwatch.js';22import { canceled, onUnexpectedError } from '../../../base/common/errors.js';23import { UnicodeHighlighterOptions } from '../../common/services/unicodeTextModelHighlighter.js';24import { ILanguageFeaturesService } from '../../common/services/languageFeatures.js';25import { IChange } from '../../common/diff/legacyLinesDiffComputer.js';26import { IDocumentDiff, IDocumentDiffProviderOptions } from '../../common/diff/documentDiffProvider.js';27import { ILinesDiffComputerOptions, MovedText } from '../../common/diff/linesDiffComputer.js';28import { DetailedLineRangeMapping, RangeMapping, LineRangeMapping } from '../../common/diff/rangeMapping.js';29import { LineRange } from '../../common/core/ranges/lineRange.js';30import { SectionHeader, FindSectionHeaderOptions } from '../../common/services/findSectionHeaders.js';31import { mainWindow } from '../../../base/browser/window.js';32import { WindowIntervalTimer } from '../../../base/browser/dom.js';33import { WorkerTextModelSyncClient } from '../../common/services/textModelSync/textModelSync.impl.js';34import { EditorWorkerHost } from '../../common/services/editorWorkerHost.js';35import { StringEdit } from '../../common/core/edits/stringEdit.js';36import { OffsetRange } from '../../common/core/ranges/offsetRange.js';3738/**39* Stop the worker if it was not needed for 5 min.40*/41const STOP_WORKER_DELTA_TIME_MS = 5 * 60 * 1000;4243function canSyncModel(modelService: IModelService, resource: URI): boolean {44const model = modelService.getModel(resource);45if (!model) {46return false;47}48if (model.isTooLargeForSyncing()) {49return false;50}51return true;52}5354export abstract class EditorWorkerService extends Disposable implements IEditorWorkerService {5556declare readonly _serviceBrand: undefined;5758private readonly _modelService: IModelService;59private readonly _workerManager: WorkerManager;60private readonly _logService: ILogService;6162constructor(63workerDescriptor: IWebWorkerDescriptor,64@IModelService modelService: IModelService,65@ITextResourceConfigurationService configurationService: ITextResourceConfigurationService,66@ILogService logService: ILogService,67@ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService,68@ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService,69) {70super();71this._modelService = modelService;72this._workerManager = this._register(new WorkerManager(workerDescriptor, this._modelService));73this._logService = logService;7475// register default link-provider and default completions-provider76this._register(languageFeaturesService.linkProvider.register({ language: '*', hasAccessToAllModels: true }, {77provideLinks: async (model, token) => {78if (!canSyncModel(this._modelService, model.uri)) {79return Promise.resolve({ links: [] }); // File too large80}81const worker = await this._workerWithResources([model.uri]);82const links = await worker.$computeLinks(model.uri.toString());83return links && { links };84}85}));86this._register(languageFeaturesService.completionProvider.register('*', new WordBasedCompletionItemProvider(this._workerManager, configurationService, this._modelService, this._languageConfigurationService, this._logService)));87}8889public override dispose(): void {90super.dispose();91}9293public canComputeUnicodeHighlights(uri: URI): boolean {94return canSyncModel(this._modelService, uri);95}9697public async computedUnicodeHighlights(uri: URI, options: UnicodeHighlighterOptions, range?: IRange): Promise<IUnicodeHighlightsResult> {98const worker = await this._workerWithResources([uri]);99return worker.$computeUnicodeHighlights(uri.toString(), options, range);100}101102public async computeDiff(original: URI, modified: URI, options: IDocumentDiffProviderOptions, algorithm: DiffAlgorithmName): Promise<IDocumentDiff | null> {103const worker = await this._workerWithResources([original, modified], /* forceLargeModels */true);104const result = await worker.$computeDiff(original.toString(), modified.toString(), options, algorithm);105if (!result) {106return null;107}108// Convert from space efficient JSON data to rich objects.109const diff: IDocumentDiff = {110identical: result.identical,111quitEarly: result.quitEarly,112changes: toLineRangeMappings(result.changes),113moves: result.moves.map(m => new MovedText(114new LineRangeMapping(new LineRange(m[0], m[1]), new LineRange(m[2], m[3])),115toLineRangeMappings(m[4])116))117};118return diff;119120function toLineRangeMappings(changes: readonly ILineChange[]): readonly DetailedLineRangeMapping[] {121return changes.map(122(c) => new DetailedLineRangeMapping(123new LineRange(c[0], c[1]),124new LineRange(c[2], c[3]),125c[4]?.map(126(c) => new RangeMapping(127new Range(c[0], c[1], c[2], c[3]),128new Range(c[4], c[5], c[6], c[7])129)130)131)132);133}134}135136public canComputeDirtyDiff(original: URI, modified: URI): boolean {137return (canSyncModel(this._modelService, original) && canSyncModel(this._modelService, modified));138}139140public async computeDirtyDiff(original: URI, modified: URI, ignoreTrimWhitespace: boolean): Promise<IChange[] | null> {141const worker = await this._workerWithResources([original, modified]);142return worker.$computeDirtyDiff(original.toString(), modified.toString(), ignoreTrimWhitespace);143}144145public async computeMoreMinimalEdits(resource: URI, edits: languages.TextEdit[] | null | undefined, pretty: boolean = false): Promise<languages.TextEdit[] | undefined> {146if (isNonEmptyArray(edits)) {147if (!canSyncModel(this._modelService, resource)) {148return Promise.resolve(edits); // File too large149}150const sw = StopWatch.create();151const result = this._workerWithResources([resource]).then(worker => worker.$computeMoreMinimalEdits(resource.toString(), edits, pretty));152result.finally(() => this._logService.trace('FORMAT#computeMoreMinimalEdits', resource.toString(true), sw.elapsed()));153return Promise.race([result, timeout(1000).then(() => edits)]);154155} else {156return Promise.resolve(undefined);157}158}159160public computeHumanReadableDiff(resource: URI, edits: languages.TextEdit[] | null | undefined): Promise<languages.TextEdit[] | undefined> {161if (isNonEmptyArray(edits)) {162if (!canSyncModel(this._modelService, resource)) {163return Promise.resolve(edits); // File too large164}165const sw = StopWatch.create();166const opts: ILinesDiffComputerOptions = { ignoreTrimWhitespace: false, maxComputationTimeMs: 1000, computeMoves: false };167const result = (168this._workerWithResources([resource])169.then(worker => worker.$computeHumanReadableDiff(resource.toString(), edits, opts))170.catch((err) => {171onUnexpectedError(err);172// In case of an exception, fall back to computeMoreMinimalEdits173return this.computeMoreMinimalEdits(resource, edits, true);174})175);176result.finally(() => this._logService.trace('FORMAT#computeHumanReadableDiff', resource.toString(true), sw.elapsed()));177return result;178179} else {180return Promise.resolve(undefined);181}182}183184public async computeStringEditFromDiff(original: string, modified: string, options: { maxComputationTimeMs: number }, algorithm: DiffAlgorithmName): Promise<StringEdit> {185try {186const worker = await this._workerWithResources([]);187const edit = await worker.$computeStringDiff(original, modified, options, algorithm);188return StringEdit.fromJson(edit);189} catch (e) {190onUnexpectedError(e);191return StringEdit.replace(OffsetRange.ofLength(original.length), modified); // approximation192}193}194195public canNavigateValueSet(resource: URI): boolean {196return (canSyncModel(this._modelService, resource));197}198199public async navigateValueSet(resource: URI, range: IRange, up: boolean): Promise<languages.IInplaceReplaceSupportResult | null> {200const model = this._modelService.getModel(resource);201if (!model) {202return null;203}204const wordDefRegExp = this._languageConfigurationService.getLanguageConfiguration(model.getLanguageId()).getWordDefinition();205const wordDef = wordDefRegExp.source;206const wordDefFlags = wordDefRegExp.flags;207const worker = await this._workerWithResources([resource]);208return worker.$navigateValueSet(resource.toString(), range, up, wordDef, wordDefFlags);209}210211public canComputeWordRanges(resource: URI): boolean {212return canSyncModel(this._modelService, resource);213}214215public async computeWordRanges(resource: URI, range: IRange): Promise<{ [word: string]: IRange[] } | null> {216const model = this._modelService.getModel(resource);217if (!model) {218return Promise.resolve(null);219}220const wordDefRegExp = this._languageConfigurationService.getLanguageConfiguration(model.getLanguageId()).getWordDefinition();221const wordDef = wordDefRegExp.source;222const wordDefFlags = wordDefRegExp.flags;223const worker = await this._workerWithResources([resource]);224return worker.$computeWordRanges(resource.toString(), range, wordDef, wordDefFlags);225}226227public async findSectionHeaders(uri: URI, options: FindSectionHeaderOptions): Promise<SectionHeader[]> {228const worker = await this._workerWithResources([uri]);229return worker.$findSectionHeaders(uri.toString(), options);230}231232public async computeDefaultDocumentColors(uri: URI): Promise<languages.IColorInformation[] | null> {233const worker = await this._workerWithResources([uri]);234return worker.$computeDefaultDocumentColors(uri.toString());235}236237private async _workerWithResources(resources: URI[], forceLargeModels: boolean = false): Promise<Proxied<EditorWorker>> {238const worker = await this._workerManager.withWorker();239return await worker.workerWithSyncedResources(resources, forceLargeModels);240}241}242243class WordBasedCompletionItemProvider implements languages.CompletionItemProvider {244245private readonly _workerManager: WorkerManager;246private readonly _configurationService: ITextResourceConfigurationService;247private readonly _modelService: IModelService;248249readonly _debugDisplayName = 'wordbasedCompletions';250251constructor(252workerManager: WorkerManager,253configurationService: ITextResourceConfigurationService,254modelService: IModelService,255private readonly languageConfigurationService: ILanguageConfigurationService,256private readonly logService: ILogService257) {258this._workerManager = workerManager;259this._configurationService = configurationService;260this._modelService = modelService;261}262263async provideCompletionItems(model: ITextModel, position: Position): Promise<languages.CompletionList | undefined> {264type WordBasedSuggestionsConfig = {265wordBasedSuggestions?: 'off' | 'currentDocument' | 'matchingDocuments' | 'allDocuments';266};267const config = this._configurationService.getValue<WordBasedSuggestionsConfig>(model.uri, position, 'editor');268if (config.wordBasedSuggestions === 'off') {269return undefined;270}271272const models: URI[] = [];273if (config.wordBasedSuggestions === 'currentDocument') {274// only current file and only if not too large275if (canSyncModel(this._modelService, model.uri)) {276models.push(model.uri);277}278} else {279// either all files or files of same language280for (const candidate of this._modelService.getModels()) {281if (!canSyncModel(this._modelService, candidate.uri)) {282continue;283}284if (candidate === model) {285models.unshift(candidate.uri);286287} else if (config.wordBasedSuggestions === 'allDocuments' || candidate.getLanguageId() === model.getLanguageId()) {288models.push(candidate.uri);289}290}291}292293if (models.length === 0) {294return undefined; // File too large, no other files295}296297const wordDefRegExp = this.languageConfigurationService.getLanguageConfiguration(model.getLanguageId()).getWordDefinition();298const word = model.getWordAtPosition(position);299const replace = !word ? Range.fromPositions(position) : new Range(position.lineNumber, word.startColumn, position.lineNumber, word.endColumn);300const insert = replace.setEndPosition(position.lineNumber, position.column);301302// Trace logging about the word and replace/insert ranges303this.logService.trace('[WordBasedCompletionItemProvider]', `word: "${word?.word || ''}", wordDef: "${wordDefRegExp}", replace: [${replace.toString()}], insert: [${insert.toString()}]`);304305const client = await this._workerManager.withWorker();306const data = await client.textualSuggest(models, word?.word, wordDefRegExp);307if (!data) {308return undefined;309}310311return {312duration: data.duration,313suggestions: data.words.map((word): languages.CompletionItem => {314return {315kind: languages.CompletionItemKind.Text,316label: word,317insertText: word,318range: { insert, replace }319};320}),321};322}323}324325class WorkerManager extends Disposable {326327private readonly _modelService: IModelService;328private _editorWorkerClient: EditorWorkerClient | null;329private _lastWorkerUsedTime: number;330331constructor(332private readonly _workerDescriptor: IWebWorkerDescriptor,333@IModelService modelService: IModelService334) {335super();336this._modelService = modelService;337this._editorWorkerClient = null;338this._lastWorkerUsedTime = (new Date()).getTime();339340const stopWorkerInterval = this._register(new WindowIntervalTimer());341stopWorkerInterval.cancelAndSet(() => this._checkStopIdleWorker(), Math.round(STOP_WORKER_DELTA_TIME_MS / 2), mainWindow);342343this._register(this._modelService.onModelRemoved(_ => this._checkStopEmptyWorker()));344}345346public override dispose(): void {347if (this._editorWorkerClient) {348this._editorWorkerClient.dispose();349this._editorWorkerClient = null;350}351super.dispose();352}353354/**355* Check if the model service has no more models and stop the worker if that is the case.356*/357private _checkStopEmptyWorker(): void {358if (!this._editorWorkerClient) {359return;360}361362const models = this._modelService.getModels();363if (models.length === 0) {364// There are no more models => nothing possible for me to do365this._editorWorkerClient.dispose();366this._editorWorkerClient = null;367}368}369370/**371* Check if the worker has been idle for a while and then stop it.372*/373private _checkStopIdleWorker(): void {374if (!this._editorWorkerClient) {375return;376}377378const timeSinceLastWorkerUsedTime = (new Date()).getTime() - this._lastWorkerUsedTime;379if (timeSinceLastWorkerUsedTime > STOP_WORKER_DELTA_TIME_MS) {380this._editorWorkerClient.dispose();381this._editorWorkerClient = null;382}383}384385public withWorker(): Promise<EditorWorkerClient> {386this._lastWorkerUsedTime = (new Date()).getTime();387if (!this._editorWorkerClient) {388this._editorWorkerClient = new EditorWorkerClient(this._workerDescriptor, false, this._modelService);389}390return Promise.resolve(this._editorWorkerClient);391}392}393394class SynchronousWorkerClient<T extends IDisposable> implements IWebWorkerClient<T> {395private readonly _instance: T;396public readonly proxy: Proxied<T>;397398constructor(instance: T) {399this._instance = instance;400this.proxy = this._instance as Proxied<T>;401}402403public dispose(): void {404this._instance.dispose();405}406407public setChannel<T extends object>(channel: string, handler: T): void {408throw new Error(`Not supported`);409}410411public getChannel<T extends object>(channel: string): Proxied<T> {412throw new Error(`Not supported`);413}414}415416export interface IEditorWorkerClient {417fhr(method: string, args: any[]): Promise<any>;418}419420export class EditorWorkerClient extends Disposable implements IEditorWorkerClient {421422private readonly _modelService: IModelService;423private readonly _keepIdleModels: boolean;424private _worker: IWebWorkerClient<EditorWorker> | null;425private _modelManager: WorkerTextModelSyncClient | null;426private _disposed = false;427428constructor(429private readonly _workerDescriptorOrWorker: IWebWorkerDescriptor | Worker | Promise<Worker>,430keepIdleModels: boolean,431@IModelService modelService: IModelService,432) {433super();434this._modelService = modelService;435this._keepIdleModels = keepIdleModels;436this._worker = null;437this._modelManager = null;438}439440// foreign host request441public fhr(method: string, args: any[]): Promise<any> {442throw new Error(`Not implemented!`);443}444445private _getOrCreateWorker(): IWebWorkerClient<EditorWorker> {446if (!this._worker) {447try {448this._worker = this._register(createWebWorker<EditorWorker>(this._workerDescriptorOrWorker));449EditorWorkerHost.setChannel(this._worker, this._createEditorWorkerHost());450} catch (err) {451logOnceWebWorkerWarning(err);452this._worker = this._createFallbackLocalWorker();453}454}455return this._worker;456}457458protected async _getProxy(): Promise<Proxied<EditorWorker>> {459try {460const proxy = this._getOrCreateWorker().proxy;461await proxy.$ping();462return proxy;463} catch (err) {464logOnceWebWorkerWarning(err);465this._worker = this._createFallbackLocalWorker();466return this._worker.proxy;467}468}469470private _createFallbackLocalWorker(): SynchronousWorkerClient<EditorWorker> {471return new SynchronousWorkerClient(new EditorWorker(null));472}473474private _createEditorWorkerHost(): EditorWorkerHost {475return {476$fhr: (method, args) => this.fhr(method, args)477};478}479480private _getOrCreateModelManager(proxy: Proxied<EditorWorker>): WorkerTextModelSyncClient {481if (!this._modelManager) {482this._modelManager = this._register(new WorkerTextModelSyncClient(proxy, this._modelService, this._keepIdleModels));483}484return this._modelManager;485}486487public async workerWithSyncedResources(resources: URI[], forceLargeModels: boolean = false): Promise<Proxied<EditorWorker>> {488if (this._disposed) {489return Promise.reject(canceled());490}491const proxy = await this._getProxy();492this._getOrCreateModelManager(proxy).ensureSyncedResources(resources, forceLargeModels);493return proxy;494}495496public async textualSuggest(resources: URI[], leadingWord: string | undefined, wordDefRegExp: RegExp): Promise<{ words: string[]; duration: number } | null> {497const proxy = await this.workerWithSyncedResources(resources);498const wordDef = wordDefRegExp.source;499const wordDefFlags = wordDefRegExp.flags;500return proxy.$textualSuggest(resources.map(r => r.toString()), leadingWord, wordDef, wordDefFlags);501}502503override dispose(): void {504super.dispose();505this._disposed = true;506}507}508509510