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