Path: blob/main/src/vs/editor/browser/services/editorWorkerService.ts
5240 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 { WebWorkerDescriptor } from '../../../platform/webWorker/browser/webWorkerDescriptor.js';10import { IWebWorkerService } from '../../../platform/webWorker/browser/webWorkerService.js';11import { Position } from '../../common/core/position.js';12import { IRange, Range } from '../../common/core/range.js';13import { ITextModel } from '../../common/model.js';14import * as languages from '../../common/languages.js';15import { ILanguageConfigurationService } from '../../common/languages/languageConfigurationRegistry.js';16import { EditorWorker } from '../../common/services/editorWebWorker.js';17import { DiffAlgorithmName, IEditorWorkerService, ILineChange, IUnicodeHighlightsResult } from '../../common/services/editorWorker.js';18import { IModelService } from '../../common/services/model.js';19import { ITextResourceConfigurationService } from '../../common/services/textResourceConfiguration.js';20import { isNonEmptyArray } from '../../../base/common/arrays.js';21import { ILogService } from '../../../platform/log/common/log.js';22import { StopWatch } from '../../../base/common/stopwatch.js';23import { canceled, onUnexpectedError } from '../../../base/common/errors.js';24import { UnicodeHighlighterOptions } from '../../common/services/unicodeTextModelHighlighter.js';25import { ILanguageFeaturesService } from '../../common/services/languageFeatures.js';26import { IChange } from '../../common/diff/legacyLinesDiffComputer.js';27import { IDocumentDiff, IDocumentDiffProviderOptions } from '../../common/diff/documentDiffProvider.js';28import { ILinesDiffComputerOptions, MovedText } from '../../common/diff/linesDiffComputer.js';29import { DetailedLineRangeMapping, RangeMapping, LineRangeMapping } from '../../common/diff/rangeMapping.js';30import { LineRange } from '../../common/core/ranges/lineRange.js';31import { SectionHeader, FindSectionHeaderOptions } from '../../common/services/findSectionHeaders.js';32import { mainWindow } from '../../../base/browser/window.js';33import { WindowIntervalTimer } from '../../../base/browser/dom.js';34import { WorkerTextModelSyncClient } from '../../common/services/textModelSync/textModelSync.impl.js';35import { EditorWorkerHost } from '../../common/services/editorWorkerHost.js';36import { StringEdit } from '../../common/core/edits/stringEdit.js';37import { OffsetRange } from '../../common/core/ranges/offsetRange.js';38import { FileAccess } from '../../../base/common/network.js';39import { isCompletionsEnabledWithTextResourceConfig } from '../../common/services/completionsEnablement.js';4041/**42* Stop the worker if it was not needed for 5 min.43*/44const STOP_WORKER_DELTA_TIME_MS = 5 * 60 * 1000;4546function canSyncModel(modelService: IModelService, resource: URI): boolean {47const model = modelService.getModel(resource);48if (!model) {49return false;50}51if (model.isTooLargeForSyncing()) {52return false;53}54return true;55}5657export class EditorWorkerService extends Disposable implements IEditorWorkerService {5859declare readonly _serviceBrand: undefined;6061public static readonly workerDescriptor = new WebWorkerDescriptor({62esmModuleLocation: () => FileAccess.asBrowserUri('vs/editor/common/services/editorWebWorkerMain.js'),63esmModuleLocationBundler: () => new URL('../../common/services/editorWebWorkerMain.ts?esm', import.meta.url),64label: 'editorWorkerService'65});6667private readonly _modelService: IModelService;68private readonly _workerManager: WorkerManager;69private readonly _logService: ILogService;7071constructor(72@IModelService modelService: IModelService,73@ITextResourceConfigurationService configurationService: ITextResourceConfigurationService,74@ILogService logService: ILogService,75@ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService,76@ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService,77@IWebWorkerService private readonly _webWorkerService: IWebWorkerService,78) {79super();80this._modelService = modelService;8182this._workerManager = this._register(new WorkerManager(EditorWorkerService.workerDescriptor, this._modelService, this._webWorkerService));83this._logService = logService;8485// register default link-provider and default completions-provider86this._register(languageFeaturesService.linkProvider.register({ language: '*', hasAccessToAllModels: true }, {87provideLinks: async (model, token) => {88if (!canSyncModel(this._modelService, model.uri)) {89return Promise.resolve({ links: [] }); // File too large90}91const worker = await this._workerWithResources([model.uri]);92const links = await worker.$computeLinks(model.uri.toString());93return links && { links };94}95}));96this._register(languageFeaturesService.completionProvider.register('*', new WordBasedCompletionItemProvider(this._workerManager, configurationService, this._modelService, this._languageConfigurationService, this._logService, languageFeaturesService)));97}9899public override dispose(): void {100super.dispose();101}102103public canComputeUnicodeHighlights(uri: URI): boolean {104return canSyncModel(this._modelService, uri);105}106107public async computedUnicodeHighlights(uri: URI, options: UnicodeHighlighterOptions, range?: IRange): Promise<IUnicodeHighlightsResult> {108const worker = await this._workerWithResources([uri]);109return worker.$computeUnicodeHighlights(uri.toString(), options, range);110}111112public async computeDiff(original: URI, modified: URI, options: IDocumentDiffProviderOptions, algorithm: DiffAlgorithmName): Promise<IDocumentDiff | null> {113const worker = await this._workerWithResources([original, modified], /* forceLargeModels */true);114const result = await worker.$computeDiff(original.toString(), modified.toString(), options, algorithm);115if (!result) {116return null;117}118// Convert from space efficient JSON data to rich objects.119const diff: IDocumentDiff = {120identical: result.identical,121quitEarly: result.quitEarly,122changes: toLineRangeMappings(result.changes),123moves: result.moves.map(m => new MovedText(124new LineRangeMapping(new LineRange(m[0], m[1]), new LineRange(m[2], m[3])),125toLineRangeMappings(m[4])126))127};128return diff;129130function toLineRangeMappings(changes: readonly ILineChange[]): readonly DetailedLineRangeMapping[] {131return changes.map(132(c) => new DetailedLineRangeMapping(133new LineRange(c[0], c[1]),134new LineRange(c[2], c[3]),135c[4]?.map(136(c) => new RangeMapping(137new Range(c[0], c[1], c[2], c[3]),138new Range(c[4], c[5], c[6], c[7])139)140)141)142);143}144}145146public canComputeDirtyDiff(original: URI, modified: URI): boolean {147return (canSyncModel(this._modelService, original) && canSyncModel(this._modelService, modified));148}149150public async computeDirtyDiff(original: URI, modified: URI, ignoreTrimWhitespace: boolean): Promise<IChange[] | null> {151const worker = await this._workerWithResources([original, modified]);152return worker.$computeDirtyDiff(original.toString(), modified.toString(), ignoreTrimWhitespace);153}154155public async computeMoreMinimalEdits(resource: URI, edits: languages.TextEdit[] | null | undefined, pretty: boolean = false): Promise<languages.TextEdit[] | undefined> {156if (isNonEmptyArray(edits)) {157if (!canSyncModel(this._modelService, resource)) {158return Promise.resolve(edits); // File too large159}160const sw = StopWatch.create();161const result = this._workerWithResources([resource]).then(worker => worker.$computeMoreMinimalEdits(resource.toString(), edits, pretty));162result.finally(() => this._logService.trace('FORMAT#computeMoreMinimalEdits', resource.toString(true), sw.elapsed()));163return Promise.race([result, timeout(1000).then(() => edits)]);164165} else {166return Promise.resolve(undefined);167}168}169170public computeHumanReadableDiff(resource: URI, edits: languages.TextEdit[] | null | undefined): Promise<languages.TextEdit[] | undefined> {171if (isNonEmptyArray(edits)) {172if (!canSyncModel(this._modelService, resource)) {173return Promise.resolve(edits); // File too large174}175const sw = StopWatch.create();176const opts: ILinesDiffComputerOptions = { ignoreTrimWhitespace: false, maxComputationTimeMs: 1000, computeMoves: false };177const result = (178this._workerWithResources([resource])179.then(worker => worker.$computeHumanReadableDiff(resource.toString(), edits, opts))180.catch((err) => {181onUnexpectedError(err);182// In case of an exception, fall back to computeMoreMinimalEdits183return this.computeMoreMinimalEdits(resource, edits, true);184})185);186result.finally(() => this._logService.trace('FORMAT#computeHumanReadableDiff', resource.toString(true), sw.elapsed()));187return result;188189} else {190return Promise.resolve(undefined);191}192}193194public async computeStringEditFromDiff(original: string, modified: string, options: { maxComputationTimeMs: number }, algorithm: DiffAlgorithmName): Promise<StringEdit> {195try {196const worker = await this._workerWithResources([]);197const edit = await worker.$computeStringDiff(original, modified, options, algorithm);198return StringEdit.fromJson(edit);199} catch (e) {200onUnexpectedError(e);201return StringEdit.replace(OffsetRange.ofLength(original.length), modified); // approximation202}203}204205public canNavigateValueSet(resource: URI): boolean {206return (canSyncModel(this._modelService, resource));207}208209public async navigateValueSet(resource: URI, range: IRange, up: boolean): Promise<languages.IInplaceReplaceSupportResult | null> {210const model = this._modelService.getModel(resource);211if (!model) {212return null;213}214const wordDefRegExp = this._languageConfigurationService.getLanguageConfiguration(model.getLanguageId()).getWordDefinition();215const wordDef = wordDefRegExp.source;216const wordDefFlags = wordDefRegExp.flags;217const worker = await this._workerWithResources([resource]);218return worker.$navigateValueSet(resource.toString(), range, up, wordDef, wordDefFlags);219}220221public canComputeWordRanges(resource: URI): boolean {222return canSyncModel(this._modelService, resource);223}224225public async computeWordRanges(resource: URI, range: IRange): Promise<{ [word: string]: IRange[] } | null> {226const model = this._modelService.getModel(resource);227if (!model) {228return Promise.resolve(null);229}230const wordDefRegExp = this._languageConfigurationService.getLanguageConfiguration(model.getLanguageId()).getWordDefinition();231const wordDef = wordDefRegExp.source;232const wordDefFlags = wordDefRegExp.flags;233const worker = await this._workerWithResources([resource]);234return worker.$computeWordRanges(resource.toString(), range, wordDef, wordDefFlags);235}236237public async findSectionHeaders(uri: URI, options: FindSectionHeaderOptions): Promise<SectionHeader[]> {238const worker = await this._workerWithResources([uri]);239return worker.$findSectionHeaders(uri.toString(), options);240}241242public async computeDefaultDocumentColors(uri: URI): Promise<languages.IColorInformation[] | null> {243const worker = await this._workerWithResources([uri]);244return worker.$computeDefaultDocumentColors(uri.toString());245}246247private async _workerWithResources(resources: URI[], forceLargeModels: boolean = false): Promise<Proxied<EditorWorker>> {248const worker = await this._workerManager.withWorker();249return await worker.workerWithSyncedResources(resources, forceLargeModels);250}251}252253class WordBasedCompletionItemProvider implements languages.CompletionItemProvider {254255private readonly _workerManager: WorkerManager;256private readonly _configurationService: ITextResourceConfigurationService;257private readonly _modelService: IModelService;258259readonly _debugDisplayName = 'wordbasedCompletions';260261constructor(262workerManager: WorkerManager,263configurationService: ITextResourceConfigurationService,264modelService: IModelService,265private readonly languageConfigurationService: ILanguageConfigurationService,266private readonly logService: ILogService,267private readonly languageFeaturesService: ILanguageFeaturesService,268) {269this._workerManager = workerManager;270this._configurationService = configurationService;271this._modelService = modelService;272}273274async provideCompletionItems(model: ITextModel, position: Position): Promise<languages.CompletionList | undefined> {275type WordBasedSuggestionsConfig = {276wordBasedSuggestions?: 'off' | 'currentDocument' | 'matchingDocuments' | 'allDocuments' | 'offWithInlineSuggestions';277};278const config = this._configurationService.getValue<WordBasedSuggestionsConfig>(model.uri, position, 'editor');279if (config.wordBasedSuggestions === 'off') {280return undefined;281}282283if (config.wordBasedSuggestions === 'offWithInlineSuggestions'284&& this.languageFeaturesService.inlineCompletionsProvider.has(model)285&& isCompletionsEnabledWithTextResourceConfig(this._configurationService, model.uri, model.getLanguageId())) {286return undefined;287}288289const models: URI[] = [];290if (config.wordBasedSuggestions === 'currentDocument') {291// only current file and only if not too large292if (canSyncModel(this._modelService, model.uri)) {293models.push(model.uri);294}295} else {296// either all files or files of same language297for (const candidate of this._modelService.getModels()) {298if (!canSyncModel(this._modelService, candidate.uri)) {299continue;300}301if (candidate === model) {302models.unshift(candidate.uri);303304} else if (config.wordBasedSuggestions === 'allDocuments' || candidate.getLanguageId() === model.getLanguageId()) {305models.push(candidate.uri);306}307}308}309310if (models.length === 0) {311return undefined; // File too large, no other files312}313314const wordDefRegExp = this.languageConfigurationService.getLanguageConfiguration(model.getLanguageId()).getWordDefinition();315const word = model.getWordAtPosition(position);316const replace = !word ? Range.fromPositions(position) : new Range(position.lineNumber, word.startColumn, position.lineNumber, word.endColumn);317const insert = replace.setEndPosition(position.lineNumber, position.column);318319// Trace logging about the word and replace/insert ranges320this.logService.trace('[WordBasedCompletionItemProvider]', `word: "${word?.word || ''}", wordDef: "${wordDefRegExp}", replace: [${replace.toString()}], insert: [${insert.toString()}]`);321322const client = await this._workerManager.withWorker();323const data = await client.textualSuggest(models, word?.word, wordDefRegExp);324if (!data) {325return undefined;326}327328return {329duration: data.duration,330suggestions: data.words.map((word): languages.CompletionItem => {331return {332kind: languages.CompletionItemKind.Text,333label: word,334insertText: word,335range: { insert, replace }336};337}),338};339}340}341342class WorkerManager extends Disposable {343344private readonly _modelService: IModelService;345private readonly _webWorkerService: IWebWorkerService;346private _editorWorkerClient: EditorWorkerClient | null;347private _lastWorkerUsedTime: number;348349constructor(350private readonly _workerDescriptor: WebWorkerDescriptor,351@IModelService modelService: IModelService,352@IWebWorkerService webWorkerService: IWebWorkerService353) {354super();355this._modelService = modelService;356this._webWorkerService = webWorkerService;357this._editorWorkerClient = null;358this._lastWorkerUsedTime = (new Date()).getTime();359360const stopWorkerInterval = this._register(new WindowIntervalTimer());361stopWorkerInterval.cancelAndSet(() => this._checkStopIdleWorker(), Math.round(STOP_WORKER_DELTA_TIME_MS / 2), mainWindow);362363this._register(this._modelService.onModelRemoved(_ => this._checkStopEmptyWorker()));364}365366public override dispose(): void {367if (this._editorWorkerClient) {368this._editorWorkerClient.dispose();369this._editorWorkerClient = null;370}371super.dispose();372}373374/**375* Check if the model service has no more models and stop the worker if that is the case.376*/377private _checkStopEmptyWorker(): void {378if (!this._editorWorkerClient) {379return;380}381382const models = this._modelService.getModels();383if (models.length === 0) {384// There are no more models => nothing possible for me to do385this._editorWorkerClient.dispose();386this._editorWorkerClient = null;387}388}389390/**391* Check if the worker has been idle for a while and then stop it.392*/393private _checkStopIdleWorker(): void {394if (!this._editorWorkerClient) {395return;396}397398const timeSinceLastWorkerUsedTime = (new Date()).getTime() - this._lastWorkerUsedTime;399if (timeSinceLastWorkerUsedTime > STOP_WORKER_DELTA_TIME_MS) {400this._editorWorkerClient.dispose();401this._editorWorkerClient = null;402}403}404405public withWorker(): Promise<EditorWorkerClient> {406this._lastWorkerUsedTime = (new Date()).getTime();407if (!this._editorWorkerClient) {408this._editorWorkerClient = new EditorWorkerClient(this._workerDescriptor, false, this._modelService, this._webWorkerService);409}410return Promise.resolve(this._editorWorkerClient);411}412}413414class SynchronousWorkerClient<T extends IDisposable> implements IWebWorkerClient<T> {415private readonly _instance: T;416public readonly proxy: Proxied<T>;417418constructor(instance: T) {419this._instance = instance;420this.proxy = this._instance as Proxied<T>;421}422423public dispose(): void {424this._instance.dispose();425}426427public setChannel<T extends object>(channel: string, handler: T): void {428throw new Error(`Not supported`);429}430431public getChannel<T extends object>(channel: string): Proxied<T> {432throw new Error(`Not supported`);433}434}435436export interface IEditorWorkerClient {437fhr(method: string, args: unknown[]): Promise<unknown>;438}439440export class EditorWorkerClient extends Disposable implements IEditorWorkerClient {441442private readonly _modelService: IModelService;443private readonly _webWorkerService: IWebWorkerService;444private readonly _keepIdleModels: boolean;445private _worker: IWebWorkerClient<EditorWorker> | null;446private _modelManager: WorkerTextModelSyncClient | null;447private _disposed = false;448449constructor(450private readonly _workerDescriptorOrWorker: WebWorkerDescriptor | Worker | Promise<Worker>,451keepIdleModels: boolean,452@IModelService modelService: IModelService,453@IWebWorkerService webWorkerService: IWebWorkerService454) {455super();456this._modelService = modelService;457this._webWorkerService = webWorkerService;458this._keepIdleModels = keepIdleModels;459this._worker = null;460this._modelManager = null;461}462463// foreign host request464public fhr(method: string, args: unknown[]): Promise<unknown> {465throw new Error(`Not implemented!`);466}467468private _getOrCreateWorker(): IWebWorkerClient<EditorWorker> {469if (!this._worker) {470try {471this._worker = this._register(this._webWorkerService.createWorkerClient<EditorWorker>(this._workerDescriptorOrWorker));472EditorWorkerHost.setChannel(this._worker, this._createEditorWorkerHost());473} catch (err) {474logOnceWebWorkerWarning(err);475this._worker = this._createFallbackLocalWorker();476}477}478return this._worker;479}480481protected async _getProxy(): Promise<Proxied<EditorWorker>> {482try {483const proxy = this._getOrCreateWorker().proxy;484await proxy.$ping();485return proxy;486} catch (err) {487logOnceWebWorkerWarning(err);488this._worker = this._createFallbackLocalWorker();489return this._worker.proxy;490}491}492493private _createFallbackLocalWorker(): SynchronousWorkerClient<EditorWorker> {494return new SynchronousWorkerClient(new EditorWorker(null));495}496497private _createEditorWorkerHost(): EditorWorkerHost {498return {499$fhr: (method, args) => this.fhr(method, args)500};501}502503private _getOrCreateModelManager(proxy: Proxied<EditorWorker>): WorkerTextModelSyncClient {504if (!this._modelManager) {505this._modelManager = this._register(new WorkerTextModelSyncClient(proxy, this._modelService, this._keepIdleModels));506}507return this._modelManager;508}509510public async workerWithSyncedResources(resources: URI[], forceLargeModels: boolean = false): Promise<Proxied<EditorWorker>> {511if (this._disposed) {512return Promise.reject(canceled());513}514const proxy = await this._getProxy();515this._getOrCreateModelManager(proxy).ensureSyncedResources(resources, forceLargeModels);516return proxy;517}518519public async textualSuggest(resources: URI[], leadingWord: string | undefined, wordDefRegExp: RegExp): Promise<{ words: string[]; duration: number } | null> {520const proxy = await this.workerWithSyncedResources(resources);521const wordDef = wordDefRegExp.source;522const wordDefFlags = wordDefRegExp.flags;523return proxy.$textualSuggest(resources.map(r => r.toString()), leadingWord, wordDef, wordDefFlags);524}525526override dispose(): void {527super.dispose();528this._disposed = true;529}530}531532533