Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/browser/services/editorWorkerService.ts
5238 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 { timeout } from '../../../base/common/async.js';
7
import { Disposable, IDisposable } from '../../../base/common/lifecycle.js';
8
import { URI } from '../../../base/common/uri.js';
9
import { logOnceWebWorkerWarning, IWebWorkerClient, Proxied } from '../../../base/common/worker/webWorker.js';
10
import { WebWorkerDescriptor } from '../../../platform/webWorker/browser/webWorkerDescriptor.js';
11
import { IWebWorkerService } from '../../../platform/webWorker/browser/webWorkerService.js';
12
import { Position } from '../../common/core/position.js';
13
import { IRange, Range } from '../../common/core/range.js';
14
import { ITextModel } from '../../common/model.js';
15
import * as languages from '../../common/languages.js';
16
import { ILanguageConfigurationService } from '../../common/languages/languageConfigurationRegistry.js';
17
import { EditorWorker } from '../../common/services/editorWebWorker.js';
18
import { DiffAlgorithmName, IEditorWorkerService, ILineChange, IUnicodeHighlightsResult } from '../../common/services/editorWorker.js';
19
import { IModelService } from '../../common/services/model.js';
20
import { ITextResourceConfigurationService } from '../../common/services/textResourceConfiguration.js';
21
import { isNonEmptyArray } from '../../../base/common/arrays.js';
22
import { ILogService } from '../../../platform/log/common/log.js';
23
import { StopWatch } from '../../../base/common/stopwatch.js';
24
import { canceled, onUnexpectedError } from '../../../base/common/errors.js';
25
import { UnicodeHighlighterOptions } from '../../common/services/unicodeTextModelHighlighter.js';
26
import { ILanguageFeaturesService } from '../../common/services/languageFeatures.js';
27
import { IChange } from '../../common/diff/legacyLinesDiffComputer.js';
28
import { IDocumentDiff, IDocumentDiffProviderOptions } from '../../common/diff/documentDiffProvider.js';
29
import { ILinesDiffComputerOptions, MovedText } from '../../common/diff/linesDiffComputer.js';
30
import { DetailedLineRangeMapping, RangeMapping, LineRangeMapping } from '../../common/diff/rangeMapping.js';
31
import { LineRange } from '../../common/core/ranges/lineRange.js';
32
import { SectionHeader, FindSectionHeaderOptions } from '../../common/services/findSectionHeaders.js';
33
import { mainWindow } from '../../../base/browser/window.js';
34
import { WindowIntervalTimer } from '../../../base/browser/dom.js';
35
import { WorkerTextModelSyncClient } from '../../common/services/textModelSync/textModelSync.impl.js';
36
import { EditorWorkerHost } from '../../common/services/editorWorkerHost.js';
37
import { StringEdit } from '../../common/core/edits/stringEdit.js';
38
import { OffsetRange } from '../../common/core/ranges/offsetRange.js';
39
import { FileAccess } from '../../../base/common/network.js';
40
import { isCompletionsEnabledWithTextResourceConfig } from '../../common/services/completionsEnablement.js';
41
42
/**
43
* Stop the worker if it was not needed for 5 min.
44
*/
45
const STOP_WORKER_DELTA_TIME_MS = 5 * 60 * 1000;
46
47
function canSyncModel(modelService: IModelService, resource: URI): boolean {
48
const model = modelService.getModel(resource);
49
if (!model) {
50
return false;
51
}
52
if (model.isTooLargeForSyncing()) {
53
return false;
54
}
55
return true;
56
}
57
58
export class EditorWorkerService extends Disposable implements IEditorWorkerService {
59
60
declare readonly _serviceBrand: undefined;
61
62
public static readonly workerDescriptor = new WebWorkerDescriptor({
63
esmModuleLocation: () => FileAccess.asBrowserUri('vs/editor/common/services/editorWebWorkerMain.js'),
64
esmModuleLocationBundler: () => new URL('../../common/services/editorWebWorkerMain.ts?esm', import.meta.url),
65
label: 'editorWorkerService'
66
});
67
68
private readonly _modelService: IModelService;
69
private readonly _workerManager: WorkerManager;
70
private readonly _logService: ILogService;
71
72
constructor(
73
@IModelService modelService: IModelService,
74
@ITextResourceConfigurationService configurationService: ITextResourceConfigurationService,
75
@ILogService logService: ILogService,
76
@ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService,
77
@ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService,
78
@IWebWorkerService private readonly _webWorkerService: IWebWorkerService,
79
) {
80
super();
81
this._modelService = modelService;
82
83
this._workerManager = this._register(new WorkerManager(EditorWorkerService.workerDescriptor, this._modelService, this._webWorkerService));
84
this._logService = logService;
85
86
// register default link-provider and default completions-provider
87
this._register(languageFeaturesService.linkProvider.register({ language: '*', hasAccessToAllModels: true }, {
88
provideLinks: async (model, token) => {
89
if (!canSyncModel(this._modelService, model.uri)) {
90
return Promise.resolve({ links: [] }); // File too large
91
}
92
const worker = await this._workerWithResources([model.uri]);
93
const links = await worker.$computeLinks(model.uri.toString());
94
return links && { links };
95
}
96
}));
97
this._register(languageFeaturesService.completionProvider.register('*', new WordBasedCompletionItemProvider(this._workerManager, configurationService, this._modelService, this._languageConfigurationService, this._logService, languageFeaturesService)));
98
}
99
100
public override dispose(): void {
101
super.dispose();
102
}
103
104
public canComputeUnicodeHighlights(uri: URI): boolean {
105
return canSyncModel(this._modelService, uri);
106
}
107
108
public async computedUnicodeHighlights(uri: URI, options: UnicodeHighlighterOptions, range?: IRange): Promise<IUnicodeHighlightsResult> {
109
const worker = await this._workerWithResources([uri]);
110
return worker.$computeUnicodeHighlights(uri.toString(), options, range);
111
}
112
113
public async computeDiff(original: URI, modified: URI, options: IDocumentDiffProviderOptions, algorithm: DiffAlgorithmName): Promise<IDocumentDiff | null> {
114
const worker = await this._workerWithResources([original, modified], /* forceLargeModels */true);
115
const result = await worker.$computeDiff(original.toString(), modified.toString(), options, algorithm);
116
if (!result) {
117
return null;
118
}
119
// Convert from space efficient JSON data to rich objects.
120
const diff: IDocumentDiff = {
121
identical: result.identical,
122
quitEarly: result.quitEarly,
123
changes: toLineRangeMappings(result.changes),
124
moves: result.moves.map(m => new MovedText(
125
new LineRangeMapping(new LineRange(m[0], m[1]), new LineRange(m[2], m[3])),
126
toLineRangeMappings(m[4])
127
))
128
};
129
return diff;
130
131
function toLineRangeMappings(changes: readonly ILineChange[]): readonly DetailedLineRangeMapping[] {
132
return changes.map(
133
(c) => new DetailedLineRangeMapping(
134
new LineRange(c[0], c[1]),
135
new LineRange(c[2], c[3]),
136
c[4]?.map(
137
(c) => new RangeMapping(
138
new Range(c[0], c[1], c[2], c[3]),
139
new Range(c[4], c[5], c[6], c[7])
140
)
141
)
142
)
143
);
144
}
145
}
146
147
public canComputeDirtyDiff(original: URI, modified: URI): boolean {
148
return (canSyncModel(this._modelService, original) && canSyncModel(this._modelService, modified));
149
}
150
151
public async computeDirtyDiff(original: URI, modified: URI, ignoreTrimWhitespace: boolean): Promise<IChange[] | null> {
152
const worker = await this._workerWithResources([original, modified]);
153
return worker.$computeDirtyDiff(original.toString(), modified.toString(), ignoreTrimWhitespace);
154
}
155
156
public async computeMoreMinimalEdits(resource: URI, edits: languages.TextEdit[] | null | undefined, pretty: boolean = false): Promise<languages.TextEdit[] | undefined> {
157
if (isNonEmptyArray(edits)) {
158
if (!canSyncModel(this._modelService, resource)) {
159
return Promise.resolve(edits); // File too large
160
}
161
const sw = StopWatch.create();
162
const result = this._workerWithResources([resource]).then(worker => worker.$computeMoreMinimalEdits(resource.toString(), edits, pretty));
163
result.finally(() => this._logService.trace('FORMAT#computeMoreMinimalEdits', resource.toString(true), sw.elapsed()));
164
return Promise.race([result, timeout(1000).then(() => edits)]);
165
166
} else {
167
return Promise.resolve(undefined);
168
}
169
}
170
171
public computeHumanReadableDiff(resource: URI, edits: languages.TextEdit[] | null | undefined): Promise<languages.TextEdit[] | undefined> {
172
if (isNonEmptyArray(edits)) {
173
if (!canSyncModel(this._modelService, resource)) {
174
return Promise.resolve(edits); // File too large
175
}
176
const sw = StopWatch.create();
177
const opts: ILinesDiffComputerOptions = { ignoreTrimWhitespace: false, maxComputationTimeMs: 1000, computeMoves: false };
178
const result = (
179
this._workerWithResources([resource])
180
.then(worker => worker.$computeHumanReadableDiff(resource.toString(), edits, opts))
181
.catch((err) => {
182
onUnexpectedError(err);
183
// In case of an exception, fall back to computeMoreMinimalEdits
184
return this.computeMoreMinimalEdits(resource, edits, true);
185
})
186
);
187
result.finally(() => this._logService.trace('FORMAT#computeHumanReadableDiff', resource.toString(true), sw.elapsed()));
188
return result;
189
190
} else {
191
return Promise.resolve(undefined);
192
}
193
}
194
195
public async computeStringEditFromDiff(original: string, modified: string, options: { maxComputationTimeMs: number }, algorithm: DiffAlgorithmName): Promise<StringEdit> {
196
try {
197
const worker = await this._workerWithResources([]);
198
const edit = await worker.$computeStringDiff(original, modified, options, algorithm);
199
return StringEdit.fromJson(edit);
200
} catch (e) {
201
onUnexpectedError(e);
202
return StringEdit.replace(OffsetRange.ofLength(original.length), modified); // approximation
203
}
204
}
205
206
public canNavigateValueSet(resource: URI): boolean {
207
return (canSyncModel(this._modelService, resource));
208
}
209
210
public async navigateValueSet(resource: URI, range: IRange, up: boolean): Promise<languages.IInplaceReplaceSupportResult | null> {
211
const model = this._modelService.getModel(resource);
212
if (!model) {
213
return null;
214
}
215
const wordDefRegExp = this._languageConfigurationService.getLanguageConfiguration(model.getLanguageId()).getWordDefinition();
216
const wordDef = wordDefRegExp.source;
217
const wordDefFlags = wordDefRegExp.flags;
218
const worker = await this._workerWithResources([resource]);
219
return worker.$navigateValueSet(resource.toString(), range, up, wordDef, wordDefFlags);
220
}
221
222
public canComputeWordRanges(resource: URI): boolean {
223
return canSyncModel(this._modelService, resource);
224
}
225
226
public async computeWordRanges(resource: URI, range: IRange): Promise<{ [word: string]: IRange[] } | null> {
227
const model = this._modelService.getModel(resource);
228
if (!model) {
229
return Promise.resolve(null);
230
}
231
const wordDefRegExp = this._languageConfigurationService.getLanguageConfiguration(model.getLanguageId()).getWordDefinition();
232
const wordDef = wordDefRegExp.source;
233
const wordDefFlags = wordDefRegExp.flags;
234
const worker = await this._workerWithResources([resource]);
235
return worker.$computeWordRanges(resource.toString(), range, wordDef, wordDefFlags);
236
}
237
238
public async findSectionHeaders(uri: URI, options: FindSectionHeaderOptions): Promise<SectionHeader[]> {
239
const worker = await this._workerWithResources([uri]);
240
return worker.$findSectionHeaders(uri.toString(), options);
241
}
242
243
public async computeDefaultDocumentColors(uri: URI): Promise<languages.IColorInformation[] | null> {
244
const worker = await this._workerWithResources([uri]);
245
return worker.$computeDefaultDocumentColors(uri.toString());
246
}
247
248
private async _workerWithResources(resources: URI[], forceLargeModels: boolean = false): Promise<Proxied<EditorWorker>> {
249
const worker = await this._workerManager.withWorker();
250
return await worker.workerWithSyncedResources(resources, forceLargeModels);
251
}
252
}
253
254
class WordBasedCompletionItemProvider implements languages.CompletionItemProvider {
255
256
private readonly _workerManager: WorkerManager;
257
private readonly _configurationService: ITextResourceConfigurationService;
258
private readonly _modelService: IModelService;
259
260
readonly _debugDisplayName = 'wordbasedCompletions';
261
262
constructor(
263
workerManager: WorkerManager,
264
configurationService: ITextResourceConfigurationService,
265
modelService: IModelService,
266
private readonly languageConfigurationService: ILanguageConfigurationService,
267
private readonly logService: ILogService,
268
private readonly languageFeaturesService: ILanguageFeaturesService,
269
) {
270
this._workerManager = workerManager;
271
this._configurationService = configurationService;
272
this._modelService = modelService;
273
}
274
275
async provideCompletionItems(model: ITextModel, position: Position): Promise<languages.CompletionList | undefined> {
276
type WordBasedSuggestionsConfig = {
277
wordBasedSuggestions?: 'off' | 'currentDocument' | 'matchingDocuments' | 'allDocuments' | 'offWithInlineSuggestions';
278
};
279
const config = this._configurationService.getValue<WordBasedSuggestionsConfig>(model.uri, position, 'editor');
280
if (config.wordBasedSuggestions === 'off') {
281
return undefined;
282
}
283
284
if (config.wordBasedSuggestions === 'offWithInlineSuggestions'
285
&& this.languageFeaturesService.inlineCompletionsProvider.has(model)
286
&& isCompletionsEnabledWithTextResourceConfig(this._configurationService, model.uri, model.getLanguageId())) {
287
return undefined;
288
}
289
290
const models: URI[] = [];
291
if (config.wordBasedSuggestions === 'currentDocument') {
292
// only current file and only if not too large
293
if (canSyncModel(this._modelService, model.uri)) {
294
models.push(model.uri);
295
}
296
} else {
297
// either all files or files of same language
298
for (const candidate of this._modelService.getModels()) {
299
if (!canSyncModel(this._modelService, candidate.uri)) {
300
continue;
301
}
302
if (candidate === model) {
303
models.unshift(candidate.uri);
304
305
} else if (config.wordBasedSuggestions === 'allDocuments' || candidate.getLanguageId() === model.getLanguageId()) {
306
models.push(candidate.uri);
307
}
308
}
309
}
310
311
if (models.length === 0) {
312
return undefined; // File too large, no other files
313
}
314
315
const wordDefRegExp = this.languageConfigurationService.getLanguageConfiguration(model.getLanguageId()).getWordDefinition();
316
const word = model.getWordAtPosition(position);
317
const replace = !word ? Range.fromPositions(position) : new Range(position.lineNumber, word.startColumn, position.lineNumber, word.endColumn);
318
const insert = replace.setEndPosition(position.lineNumber, position.column);
319
320
// Trace logging about the word and replace/insert ranges
321
this.logService.trace('[WordBasedCompletionItemProvider]', `word: "${word?.word || ''}", wordDef: "${wordDefRegExp}", replace: [${replace.toString()}], insert: [${insert.toString()}]`);
322
323
const client = await this._workerManager.withWorker();
324
const data = await client.textualSuggest(models, word?.word, wordDefRegExp);
325
if (!data) {
326
return undefined;
327
}
328
329
return {
330
duration: data.duration,
331
suggestions: data.words.map((word): languages.CompletionItem => {
332
return {
333
kind: languages.CompletionItemKind.Text,
334
label: word,
335
insertText: word,
336
range: { insert, replace }
337
};
338
}),
339
};
340
}
341
}
342
343
class WorkerManager extends Disposable {
344
345
private readonly _modelService: IModelService;
346
private readonly _webWorkerService: IWebWorkerService;
347
private _editorWorkerClient: EditorWorkerClient | null;
348
private _lastWorkerUsedTime: number;
349
350
constructor(
351
private readonly _workerDescriptor: WebWorkerDescriptor,
352
@IModelService modelService: IModelService,
353
@IWebWorkerService webWorkerService: IWebWorkerService
354
) {
355
super();
356
this._modelService = modelService;
357
this._webWorkerService = webWorkerService;
358
this._editorWorkerClient = null;
359
this._lastWorkerUsedTime = (new Date()).getTime();
360
361
const stopWorkerInterval = this._register(new WindowIntervalTimer());
362
stopWorkerInterval.cancelAndSet(() => this._checkStopIdleWorker(), Math.round(STOP_WORKER_DELTA_TIME_MS / 2), mainWindow);
363
364
this._register(this._modelService.onModelRemoved(_ => this._checkStopEmptyWorker()));
365
}
366
367
public override dispose(): void {
368
if (this._editorWorkerClient) {
369
this._editorWorkerClient.dispose();
370
this._editorWorkerClient = null;
371
}
372
super.dispose();
373
}
374
375
/**
376
* Check if the model service has no more models and stop the worker if that is the case.
377
*/
378
private _checkStopEmptyWorker(): void {
379
if (!this._editorWorkerClient) {
380
return;
381
}
382
383
const models = this._modelService.getModels();
384
if (models.length === 0) {
385
// There are no more models => nothing possible for me to do
386
this._editorWorkerClient.dispose();
387
this._editorWorkerClient = null;
388
}
389
}
390
391
/**
392
* Check if the worker has been idle for a while and then stop it.
393
*/
394
private _checkStopIdleWorker(): void {
395
if (!this._editorWorkerClient) {
396
return;
397
}
398
399
const timeSinceLastWorkerUsedTime = (new Date()).getTime() - this._lastWorkerUsedTime;
400
if (timeSinceLastWorkerUsedTime > STOP_WORKER_DELTA_TIME_MS) {
401
this._editorWorkerClient.dispose();
402
this._editorWorkerClient = null;
403
}
404
}
405
406
public withWorker(): Promise<EditorWorkerClient> {
407
this._lastWorkerUsedTime = (new Date()).getTime();
408
if (!this._editorWorkerClient) {
409
this._editorWorkerClient = new EditorWorkerClient(this._workerDescriptor, false, this._modelService, this._webWorkerService);
410
}
411
return Promise.resolve(this._editorWorkerClient);
412
}
413
}
414
415
class SynchronousWorkerClient<T extends IDisposable> implements IWebWorkerClient<T> {
416
private readonly _instance: T;
417
public readonly proxy: Proxied<T>;
418
419
constructor(instance: T) {
420
this._instance = instance;
421
this.proxy = this._instance as Proxied<T>;
422
}
423
424
public dispose(): void {
425
this._instance.dispose();
426
}
427
428
public setChannel<T extends object>(channel: string, handler: T): void {
429
throw new Error(`Not supported`);
430
}
431
432
public getChannel<T extends object>(channel: string): Proxied<T> {
433
throw new Error(`Not supported`);
434
}
435
}
436
437
export interface IEditorWorkerClient {
438
fhr(method: string, args: unknown[]): Promise<unknown>;
439
}
440
441
export class EditorWorkerClient extends Disposable implements IEditorWorkerClient {
442
443
private readonly _modelService: IModelService;
444
private readonly _webWorkerService: IWebWorkerService;
445
private readonly _keepIdleModels: boolean;
446
private _worker: IWebWorkerClient<EditorWorker> | null;
447
private _modelManager: WorkerTextModelSyncClient | null;
448
private _disposed = false;
449
450
constructor(
451
private readonly _workerDescriptorOrWorker: WebWorkerDescriptor | Worker | Promise<Worker>,
452
keepIdleModels: boolean,
453
@IModelService modelService: IModelService,
454
@IWebWorkerService webWorkerService: IWebWorkerService
455
) {
456
super();
457
this._modelService = modelService;
458
this._webWorkerService = webWorkerService;
459
this._keepIdleModels = keepIdleModels;
460
this._worker = null;
461
this._modelManager = null;
462
}
463
464
// foreign host request
465
public fhr(method: string, args: unknown[]): Promise<unknown> {
466
throw new Error(`Not implemented!`);
467
}
468
469
private _getOrCreateWorker(): IWebWorkerClient<EditorWorker> {
470
if (!this._worker) {
471
try {
472
this._worker = this._register(this._webWorkerService.createWorkerClient<EditorWorker>(this._workerDescriptorOrWorker));
473
EditorWorkerHost.setChannel(this._worker, this._createEditorWorkerHost());
474
} catch (err) {
475
logOnceWebWorkerWarning(err);
476
this._worker = this._createFallbackLocalWorker();
477
}
478
}
479
return this._worker;
480
}
481
482
protected async _getProxy(): Promise<Proxied<EditorWorker>> {
483
try {
484
const proxy = this._getOrCreateWorker().proxy;
485
await proxy.$ping();
486
return proxy;
487
} catch (err) {
488
logOnceWebWorkerWarning(err);
489
this._worker = this._createFallbackLocalWorker();
490
return this._worker.proxy;
491
}
492
}
493
494
private _createFallbackLocalWorker(): SynchronousWorkerClient<EditorWorker> {
495
return new SynchronousWorkerClient(new EditorWorker(null));
496
}
497
498
private _createEditorWorkerHost(): EditorWorkerHost {
499
return {
500
$fhr: (method, args) => this.fhr(method, args)
501
};
502
}
503
504
private _getOrCreateModelManager(proxy: Proxied<EditorWorker>): WorkerTextModelSyncClient {
505
if (!this._modelManager) {
506
this._modelManager = this._register(new WorkerTextModelSyncClient(proxy, this._modelService, this._keepIdleModels));
507
}
508
return this._modelManager;
509
}
510
511
public async workerWithSyncedResources(resources: URI[], forceLargeModels: boolean = false): Promise<Proxied<EditorWorker>> {
512
if (this._disposed) {
513
return Promise.reject(canceled());
514
}
515
const proxy = await this._getProxy();
516
this._getOrCreateModelManager(proxy).ensureSyncedResources(resources, forceLargeModels);
517
return proxy;
518
}
519
520
public async textualSuggest(resources: URI[], leadingWord: string | undefined, wordDefRegExp: RegExp): Promise<{ words: string[]; duration: number } | null> {
521
const proxy = await this.workerWithSyncedResources(resources);
522
const wordDef = wordDefRegExp.source;
523
const wordDefFlags = wordDefRegExp.flags;
524
return proxy.$textualSuggest(resources.map(r => r.toString()), leadingWord, wordDef, wordDefFlags);
525
}
526
527
override dispose(): void {
528
super.dispose();
529
this._disposed = true;
530
}
531
}
532
533