Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/common/services/editorWebWorker.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 { stringDiff } from '../../../base/common/diff/diff.js';
7
import { IDisposable } from '../../../base/common/lifecycle.js';
8
import { URI } from '../../../base/common/uri.js';
9
import { IWebWorkerServerRequestHandler } from '../../../base/common/worker/webWorker.js';
10
import { Position } from '../core/position.js';
11
import { IRange, Range } from '../core/range.js';
12
import { EndOfLineSequence, ITextModel } from '../model.js';
13
import { IMirrorTextModel, IModelChangedEvent } from '../model/mirrorTextModel.js';
14
import { IColorInformation, IInplaceReplaceSupportResult, ILink, TextEdit } from '../languages.js';
15
import { computeLinks } from '../languages/linkComputer.js';
16
import { BasicInplaceReplace } from '../languages/supports/inplaceReplaceSupport.js';
17
import { DiffAlgorithmName, IDiffComputationResult, ILineChange, IUnicodeHighlightsResult } from './editorWorker.js';
18
import { createMonacoBaseAPI } from './editorBaseApi.js';
19
import { StopWatch } from '../../../base/common/stopwatch.js';
20
import { UnicodeTextModelHighlighter, UnicodeHighlighterOptions } from './unicodeTextModelHighlighter.js';
21
import { DiffComputer, IChange } from '../diff/legacyLinesDiffComputer.js';
22
import { ILinesDiffComputer, ILinesDiffComputerOptions } from '../diff/linesDiffComputer.js';
23
import { DetailedLineRangeMapping } from '../diff/rangeMapping.js';
24
import { linesDiffComputers } from '../diff/linesDiffComputers.js';
25
import { IDocumentDiffProviderOptions } from '../diff/documentDiffProvider.js';
26
import { BugIndicatingError } from '../../../base/common/errors.js';
27
import { computeDefaultDocumentColors } from '../languages/defaultDocumentColorsComputer.js';
28
import { FindSectionHeaderOptions, SectionHeader, findSectionHeaders } from './findSectionHeaders.js';
29
import { IRawModelData, IWorkerTextModelSyncChannelServer } from './textModelSync/textModelSync.protocol.js';
30
import { ICommonModel, WorkerTextModelSyncServer } from './textModelSync/textModelSync.impl.js';
31
import { ISerializedStringEdit, StringEdit } from '../core/edits/stringEdit.js';
32
import { StringText } from '../core/text/abstractText.js';
33
import { ensureDependenciesAreSet } from '../core/text/positionToOffset.js';
34
35
export interface IMirrorModel extends IMirrorTextModel {
36
readonly uri: URI;
37
readonly version: number;
38
getValue(): string;
39
}
40
41
export interface IWorkerContext<H = {}> {
42
/**
43
* A proxy to the main thread host object.
44
*/
45
host: H;
46
/**
47
* Get all available mirror models in this worker.
48
*/
49
getMirrorModels(): IMirrorModel[];
50
}
51
52
/**
53
* Range of a word inside a model.
54
* @internal
55
*/
56
export interface IWordRange {
57
/**
58
* The index where the word starts.
59
*/
60
readonly start: number;
61
/**
62
* The index where the word ends.
63
*/
64
readonly end: number;
65
}
66
67
/**
68
* @internal
69
*/
70
export class EditorWorker implements IDisposable, IWorkerTextModelSyncChannelServer, IWebWorkerServerRequestHandler {
71
_requestHandlerBrand: any;
72
73
private readonly _workerTextModelSyncServer = new WorkerTextModelSyncServer();
74
75
constructor(
76
private readonly _foreignModule: any | null = null
77
) { }
78
79
dispose(): void {
80
}
81
82
public async $ping() {
83
return 'pong';
84
}
85
86
protected _getModel(uri: string): ICommonModel | undefined {
87
return this._workerTextModelSyncServer.getModel(uri);
88
}
89
90
public getModels(): ICommonModel[] {
91
return this._workerTextModelSyncServer.getModels();
92
}
93
94
public $acceptNewModel(data: IRawModelData): void {
95
this._workerTextModelSyncServer.$acceptNewModel(data);
96
}
97
98
public $acceptModelChanged(uri: string, e: IModelChangedEvent): void {
99
this._workerTextModelSyncServer.$acceptModelChanged(uri, e);
100
}
101
102
public $acceptRemovedModel(uri: string): void {
103
this._workerTextModelSyncServer.$acceptRemovedModel(uri);
104
}
105
106
public async $computeUnicodeHighlights(url: string, options: UnicodeHighlighterOptions, range?: IRange): Promise<IUnicodeHighlightsResult> {
107
const model = this._getModel(url);
108
if (!model) {
109
return { ranges: [], hasMore: false, ambiguousCharacterCount: 0, invisibleCharacterCount: 0, nonBasicAsciiCharacterCount: 0 };
110
}
111
return UnicodeTextModelHighlighter.computeUnicodeHighlights(model, options, range);
112
}
113
114
public async $findSectionHeaders(url: string, options: FindSectionHeaderOptions): Promise<SectionHeader[]> {
115
const model = this._getModel(url);
116
if (!model) {
117
return [];
118
}
119
return findSectionHeaders(model, options);
120
}
121
122
// ---- BEGIN diff --------------------------------------------------------------------------
123
124
public async $computeDiff(originalUrl: string, modifiedUrl: string, options: IDocumentDiffProviderOptions, algorithm: DiffAlgorithmName): Promise<IDiffComputationResult | null> {
125
const original = this._getModel(originalUrl);
126
const modified = this._getModel(modifiedUrl);
127
if (!original || !modified) {
128
return null;
129
}
130
131
const result = EditorWorker.computeDiff(original, modified, options, algorithm);
132
return result;
133
}
134
135
private static computeDiff(originalTextModel: ICommonModel | ITextModel, modifiedTextModel: ICommonModel | ITextModel, options: IDocumentDiffProviderOptions, algorithm: DiffAlgorithmName): IDiffComputationResult {
136
const diffAlgorithm: ILinesDiffComputer = algorithm === 'advanced' ? linesDiffComputers.getDefault() : linesDiffComputers.getLegacy();
137
138
const originalLines = originalTextModel.getLinesContent();
139
const modifiedLines = modifiedTextModel.getLinesContent();
140
141
const result = diffAlgorithm.computeDiff(originalLines, modifiedLines, options);
142
143
const identical = (result.changes.length > 0 ? false : this._modelsAreIdentical(originalTextModel, modifiedTextModel));
144
145
function getLineChanges(changes: readonly DetailedLineRangeMapping[]): ILineChange[] {
146
return changes.map(m => ([m.original.startLineNumber, m.original.endLineNumberExclusive, m.modified.startLineNumber, m.modified.endLineNumberExclusive, m.innerChanges?.map(m => [
147
m.originalRange.startLineNumber,
148
m.originalRange.startColumn,
149
m.originalRange.endLineNumber,
150
m.originalRange.endColumn,
151
m.modifiedRange.startLineNumber,
152
m.modifiedRange.startColumn,
153
m.modifiedRange.endLineNumber,
154
m.modifiedRange.endColumn,
155
])]));
156
}
157
158
return {
159
identical,
160
quitEarly: result.hitTimeout,
161
changes: getLineChanges(result.changes),
162
moves: result.moves.map(m => ([
163
m.lineRangeMapping.original.startLineNumber,
164
m.lineRangeMapping.original.endLineNumberExclusive,
165
m.lineRangeMapping.modified.startLineNumber,
166
m.lineRangeMapping.modified.endLineNumberExclusive,
167
getLineChanges(m.changes)
168
])),
169
};
170
}
171
172
private static _modelsAreIdentical(original: ICommonModel | ITextModel, modified: ICommonModel | ITextModel): boolean {
173
const originalLineCount = original.getLineCount();
174
const modifiedLineCount = modified.getLineCount();
175
if (originalLineCount !== modifiedLineCount) {
176
return false;
177
}
178
for (let line = 1; line <= originalLineCount; line++) {
179
const originalLine = original.getLineContent(line);
180
const modifiedLine = modified.getLineContent(line);
181
if (originalLine !== modifiedLine) {
182
return false;
183
}
184
}
185
return true;
186
}
187
188
public async $computeDirtyDiff(originalUrl: string, modifiedUrl: string, ignoreTrimWhitespace: boolean): Promise<IChange[] | null> {
189
const original = this._getModel(originalUrl);
190
const modified = this._getModel(modifiedUrl);
191
if (!original || !modified) {
192
return null;
193
}
194
195
const originalLines = original.getLinesContent();
196
const modifiedLines = modified.getLinesContent();
197
const diffComputer = new DiffComputer(originalLines, modifiedLines, {
198
shouldComputeCharChanges: false,
199
shouldPostProcessCharChanges: false,
200
shouldIgnoreTrimWhitespace: ignoreTrimWhitespace,
201
shouldMakePrettyDiff: true,
202
maxComputationTime: 1000
203
});
204
return diffComputer.computeDiff().changes;
205
}
206
207
public $computeStringDiff(original: string, modified: string, options: { maxComputationTimeMs: number }, algorithm: DiffAlgorithmName): ISerializedStringEdit {
208
return computeStringDiff(original, modified, options, algorithm).toJson();
209
}
210
211
// ---- END diff --------------------------------------------------------------------------
212
213
214
// ---- BEGIN minimal edits ---------------------------------------------------------------
215
216
private static readonly _diffLimit = 100000;
217
218
public async $computeMoreMinimalEdits(modelUrl: string, edits: TextEdit[], pretty: boolean): Promise<TextEdit[]> {
219
const model = this._getModel(modelUrl);
220
if (!model) {
221
return edits;
222
}
223
224
const result: TextEdit[] = [];
225
let lastEol: EndOfLineSequence | undefined = undefined;
226
227
edits = edits.slice(0).sort((a, b) => {
228
if (a.range && b.range) {
229
return Range.compareRangesUsingStarts(a.range, b.range);
230
}
231
// eol only changes should go to the end
232
const aRng = a.range ? 0 : 1;
233
const bRng = b.range ? 0 : 1;
234
return aRng - bRng;
235
});
236
237
// merge adjacent edits
238
let writeIndex = 0;
239
for (let readIndex = 1; readIndex < edits.length; readIndex++) {
240
if (Range.getEndPosition(edits[writeIndex].range).equals(Range.getStartPosition(edits[readIndex].range))) {
241
edits[writeIndex].range = Range.fromPositions(Range.getStartPosition(edits[writeIndex].range), Range.getEndPosition(edits[readIndex].range));
242
edits[writeIndex].text += edits[readIndex].text;
243
} else {
244
writeIndex++;
245
edits[writeIndex] = edits[readIndex];
246
}
247
}
248
edits.length = writeIndex + 1;
249
250
for (let { range, text, eol } of edits) {
251
252
if (typeof eol === 'number') {
253
lastEol = eol;
254
}
255
256
if (Range.isEmpty(range) && !text) {
257
// empty change
258
continue;
259
}
260
261
const original = model.getValueInRange(range);
262
text = text.replace(/\r\n|\n|\r/g, model.eol);
263
264
if (original === text) {
265
// noop
266
continue;
267
}
268
269
// make sure diff won't take too long
270
if (Math.max(text.length, original.length) > EditorWorker._diffLimit) {
271
result.push({ range, text });
272
continue;
273
}
274
275
// compute diff between original and edit.text
276
const changes = stringDiff(original, text, pretty);
277
const editOffset = model.offsetAt(Range.lift(range).getStartPosition());
278
279
for (const change of changes) {
280
const start = model.positionAt(editOffset + change.originalStart);
281
const end = model.positionAt(editOffset + change.originalStart + change.originalLength);
282
const newEdit: TextEdit = {
283
text: text.substr(change.modifiedStart, change.modifiedLength),
284
range: { startLineNumber: start.lineNumber, startColumn: start.column, endLineNumber: end.lineNumber, endColumn: end.column }
285
};
286
287
if (model.getValueInRange(newEdit.range) !== newEdit.text) {
288
result.push(newEdit);
289
}
290
}
291
}
292
293
if (typeof lastEol === 'number') {
294
result.push({ eol: lastEol, text: '', range: { startLineNumber: 0, startColumn: 0, endLineNumber: 0, endColumn: 0 } });
295
}
296
297
return result;
298
}
299
300
public $computeHumanReadableDiff(modelUrl: string, edits: TextEdit[], options: ILinesDiffComputerOptions): TextEdit[] {
301
const model = this._getModel(modelUrl);
302
if (!model) {
303
return edits;
304
}
305
306
const result: TextEdit[] = [];
307
let lastEol: EndOfLineSequence | undefined = undefined;
308
309
edits = edits.slice(0).sort((a, b) => {
310
if (a.range && b.range) {
311
return Range.compareRangesUsingStarts(a.range, b.range);
312
}
313
// eol only changes should go to the end
314
const aRng = a.range ? 0 : 1;
315
const bRng = b.range ? 0 : 1;
316
return aRng - bRng;
317
});
318
319
for (let { range, text, eol } of edits) {
320
321
if (typeof eol === 'number') {
322
lastEol = eol;
323
}
324
325
if (Range.isEmpty(range) && !text) {
326
// empty change
327
continue;
328
}
329
330
const original = model.getValueInRange(range);
331
text = text.replace(/\r\n|\n|\r/g, model.eol);
332
333
if (original === text) {
334
// noop
335
continue;
336
}
337
338
// make sure diff won't take too long
339
if (Math.max(text.length, original.length) > EditorWorker._diffLimit) {
340
result.push({ range, text });
341
continue;
342
}
343
344
// compute diff between original and edit.text
345
346
const originalLines = original.split(/\r\n|\n|\r/);
347
const modifiedLines = text.split(/\r\n|\n|\r/);
348
349
const diff = linesDiffComputers.getDefault().computeDiff(originalLines, modifiedLines, options);
350
351
const start = Range.lift(range).getStartPosition();
352
353
function addPositions(pos1: Position, pos2: Position): Position {
354
return new Position(pos1.lineNumber + pos2.lineNumber - 1, pos2.lineNumber === 1 ? pos1.column + pos2.column - 1 : pos2.column);
355
}
356
357
function getText(lines: string[], range: Range): string[] {
358
const result: string[] = [];
359
for (let i = range.startLineNumber; i <= range.endLineNumber; i++) {
360
const line = lines[i - 1];
361
if (i === range.startLineNumber && i === range.endLineNumber) {
362
result.push(line.substring(range.startColumn - 1, range.endColumn - 1));
363
} else if (i === range.startLineNumber) {
364
result.push(line.substring(range.startColumn - 1));
365
} else if (i === range.endLineNumber) {
366
result.push(line.substring(0, range.endColumn - 1));
367
} else {
368
result.push(line);
369
}
370
}
371
return result;
372
}
373
374
for (const c of diff.changes) {
375
if (c.innerChanges) {
376
for (const x of c.innerChanges) {
377
result.push({
378
range: Range.fromPositions(
379
addPositions(start, x.originalRange.getStartPosition()),
380
addPositions(start, x.originalRange.getEndPosition())
381
),
382
text: getText(modifiedLines, x.modifiedRange).join(model.eol)
383
});
384
}
385
} else {
386
throw new BugIndicatingError('The experimental diff algorithm always produces inner changes');
387
}
388
}
389
}
390
391
if (typeof lastEol === 'number') {
392
result.push({ eol: lastEol, text: '', range: { startLineNumber: 0, startColumn: 0, endLineNumber: 0, endColumn: 0 } });
393
}
394
395
return result;
396
}
397
398
// ---- END minimal edits ---------------------------------------------------------------
399
400
public async $computeLinks(modelUrl: string): Promise<ILink[] | null> {
401
const model = this._getModel(modelUrl);
402
if (!model) {
403
return null;
404
}
405
406
return computeLinks(model);
407
}
408
409
// --- BEGIN default document colors -----------------------------------------------------------
410
411
public async $computeDefaultDocumentColors(modelUrl: string): Promise<IColorInformation[] | null> {
412
const model = this._getModel(modelUrl);
413
if (!model) {
414
return null;
415
}
416
return computeDefaultDocumentColors(model);
417
}
418
419
// ---- BEGIN suggest --------------------------------------------------------------------------
420
421
private static readonly _suggestionsLimit = 10000;
422
423
public async $textualSuggest(modelUrls: string[], leadingWord: string | undefined, wordDef: string, wordDefFlags: string): Promise<{ words: string[]; duration: number } | null> {
424
425
const sw = new StopWatch();
426
const wordDefRegExp = new RegExp(wordDef, wordDefFlags);
427
const seen = new Set<string>();
428
429
outer: for (const url of modelUrls) {
430
const model = this._getModel(url);
431
if (!model) {
432
continue;
433
}
434
435
for (const word of model.words(wordDefRegExp)) {
436
if (word === leadingWord || !isNaN(Number(word))) {
437
continue;
438
}
439
seen.add(word);
440
if (seen.size > EditorWorker._suggestionsLimit) {
441
break outer;
442
}
443
}
444
}
445
446
return { words: Array.from(seen), duration: sw.elapsed() };
447
}
448
449
450
// ---- END suggest --------------------------------------------------------------------------
451
452
//#region -- word ranges --
453
454
public async $computeWordRanges(modelUrl: string, range: IRange, wordDef: string, wordDefFlags: string): Promise<{ [word: string]: IRange[] }> {
455
const model = this._getModel(modelUrl);
456
if (!model) {
457
return Object.create(null);
458
}
459
const wordDefRegExp = new RegExp(wordDef, wordDefFlags);
460
const result: { [word: string]: IRange[] } = Object.create(null);
461
for (let line = range.startLineNumber; line < range.endLineNumber; line++) {
462
const words = model.getLineWords(line, wordDefRegExp);
463
for (const word of words) {
464
if (!isNaN(Number(word.word))) {
465
continue;
466
}
467
let array = result[word.word];
468
if (!array) {
469
array = [];
470
result[word.word] = array;
471
}
472
array.push({
473
startLineNumber: line,
474
startColumn: word.startColumn,
475
endLineNumber: line,
476
endColumn: word.endColumn
477
});
478
}
479
}
480
return result;
481
}
482
483
//#endregion
484
485
public async $navigateValueSet(modelUrl: string, range: IRange, up: boolean, wordDef: string, wordDefFlags: string): Promise<IInplaceReplaceSupportResult | null> {
486
const model = this._getModel(modelUrl);
487
if (!model) {
488
return null;
489
}
490
491
const wordDefRegExp = new RegExp(wordDef, wordDefFlags);
492
493
if (range.startColumn === range.endColumn) {
494
range = {
495
startLineNumber: range.startLineNumber,
496
startColumn: range.startColumn,
497
endLineNumber: range.endLineNumber,
498
endColumn: range.endColumn + 1
499
};
500
}
501
502
const selectionText = model.getValueInRange(range);
503
504
const wordRange = model.getWordAtPosition({ lineNumber: range.startLineNumber, column: range.startColumn }, wordDefRegExp);
505
if (!wordRange) {
506
return null;
507
}
508
const word = model.getValueInRange(wordRange);
509
const result = BasicInplaceReplace.INSTANCE.navigateValueSet(range, selectionText, wordRange, word, up);
510
return result;
511
}
512
513
// ---- BEGIN foreign module support --------------------------------------------------------------------------
514
515
// foreign method request
516
public $fmr(method: string, args: any[]): Promise<any> {
517
if (!this._foreignModule || typeof this._foreignModule[method] !== 'function') {
518
return Promise.reject(new Error('Missing requestHandler or method: ' + method));
519
}
520
521
try {
522
return Promise.resolve(this._foreignModule[method].apply(this._foreignModule, args));
523
} catch (e) {
524
return Promise.reject(e);
525
}
526
}
527
528
// ---- END foreign module support --------------------------------------------------------------------------
529
}
530
531
// This is only available in a Web Worker
532
declare function importScripts(...urls: string[]): void;
533
534
if (typeof importScripts === 'function') {
535
// Running in a web worker
536
globalThis.monaco = createMonacoBaseAPI();
537
}
538
539
/**
540
* @internal
541
*/
542
export function computeStringDiff(original: string, modified: string, options: { maxComputationTimeMs: number }, algorithm: DiffAlgorithmName): StringEdit {
543
const diffAlgorithm: ILinesDiffComputer = algorithm === 'advanced' ? linesDiffComputers.getDefault() : linesDiffComputers.getLegacy();
544
545
ensureDependenciesAreSet();
546
547
const originalText = new StringText(original);
548
const originalLines = originalText.getLines();
549
const modifiedText = new StringText(modified);
550
const modifiedLines = modifiedText.getLines();
551
552
const result = diffAlgorithm.computeDiff(originalLines, modifiedLines, { ignoreTrimWhitespace: false, maxComputationTimeMs: options.maxComputationTimeMs, computeMoves: false, extendToSubwords: false });
553
554
const textEdit = DetailedLineRangeMapping.toTextEdit(result.changes, modifiedText);
555
const strEdit = originalText.getTransformer().getStringEdit(textEdit);
556
557
return strEdit;
558
}
559
560