Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/common/services/textModelSync/textModelSync.impl.ts
3296 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 { IntervalTimer } from '../../../../base/common/async.js';
7
import { Disposable, DisposableStore, dispose, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
8
import { URI } from '../../../../base/common/uri.js';
9
import { IWebWorkerClient, IWebWorkerServer } from '../../../../base/common/worker/webWorker.js';
10
import { IPosition, Position } from '../../core/position.js';
11
import { IRange, Range } from '../../core/range.js';
12
import { ensureValidWordDefinition, getWordAtText, IWordAtPosition } from '../../core/wordHelper.js';
13
import { IDocumentColorComputerTarget } from '../../languages/defaultDocumentColorsComputer.js';
14
import { ILinkComputerTarget } from '../../languages/linkComputer.js';
15
import { MirrorTextModel as BaseMirrorModel, IModelChangedEvent } from '../../model/mirrorTextModel.js';
16
import { IMirrorModel, IWordRange } from '../editorWebWorker.js';
17
import { IModelService } from '../model.js';
18
import { IRawModelData, IWorkerTextModelSyncChannelServer } from './textModelSync.protocol.js';
19
20
/**
21
* Stop syncing a model to the worker if it was not needed for 1 min.
22
*/
23
export const STOP_SYNC_MODEL_DELTA_TIME_MS = 60 * 1000;
24
25
export const WORKER_TEXT_MODEL_SYNC_CHANNEL = 'workerTextModelSync';
26
27
export class WorkerTextModelSyncClient extends Disposable {
28
29
public static create(workerClient: IWebWorkerClient<any>, modelService: IModelService): WorkerTextModelSyncClient {
30
return new WorkerTextModelSyncClient(
31
workerClient.getChannel<IWorkerTextModelSyncChannelServer>(WORKER_TEXT_MODEL_SYNC_CHANNEL),
32
modelService
33
);
34
}
35
36
private readonly _proxy: IWorkerTextModelSyncChannelServer;
37
private readonly _modelService: IModelService;
38
private _syncedModels: { [modelUrl: string]: IDisposable } = Object.create(null);
39
private _syncedModelsLastUsedTime: { [modelUrl: string]: number } = Object.create(null);
40
41
constructor(proxy: IWorkerTextModelSyncChannelServer, modelService: IModelService, keepIdleModels: boolean = false) {
42
super();
43
this._proxy = proxy;
44
this._modelService = modelService;
45
46
if (!keepIdleModels) {
47
const timer = new IntervalTimer();
48
timer.cancelAndSet(() => this._checkStopModelSync(), Math.round(STOP_SYNC_MODEL_DELTA_TIME_MS / 2));
49
this._register(timer);
50
}
51
}
52
53
public override dispose(): void {
54
for (const modelUrl in this._syncedModels) {
55
dispose(this._syncedModels[modelUrl]);
56
}
57
this._syncedModels = Object.create(null);
58
this._syncedModelsLastUsedTime = Object.create(null);
59
super.dispose();
60
}
61
62
public ensureSyncedResources(resources: URI[], forceLargeModels: boolean = false): void {
63
for (const resource of resources) {
64
const resourceStr = resource.toString();
65
66
if (!this._syncedModels[resourceStr]) {
67
this._beginModelSync(resource, forceLargeModels);
68
}
69
if (this._syncedModels[resourceStr]) {
70
this._syncedModelsLastUsedTime[resourceStr] = (new Date()).getTime();
71
}
72
}
73
}
74
75
private _checkStopModelSync(): void {
76
const currentTime = (new Date()).getTime();
77
78
const toRemove: string[] = [];
79
for (const modelUrl in this._syncedModelsLastUsedTime) {
80
const elapsedTime = currentTime - this._syncedModelsLastUsedTime[modelUrl];
81
if (elapsedTime > STOP_SYNC_MODEL_DELTA_TIME_MS) {
82
toRemove.push(modelUrl);
83
}
84
}
85
86
for (const e of toRemove) {
87
this._stopModelSync(e);
88
}
89
}
90
91
private _beginModelSync(resource: URI, forceLargeModels: boolean): void {
92
const model = this._modelService.getModel(resource);
93
if (!model) {
94
return;
95
}
96
if (!forceLargeModels && model.isTooLargeForSyncing()) {
97
return;
98
}
99
100
const modelUrl = resource.toString();
101
102
this._proxy.$acceptNewModel({
103
url: model.uri.toString(),
104
lines: model.getLinesContent(),
105
EOL: model.getEOL(),
106
versionId: model.getVersionId()
107
});
108
109
const toDispose = new DisposableStore();
110
toDispose.add(model.onDidChangeContent((e) => {
111
this._proxy.$acceptModelChanged(modelUrl.toString(), e);
112
}));
113
toDispose.add(model.onWillDispose(() => {
114
this._stopModelSync(modelUrl);
115
}));
116
toDispose.add(toDisposable(() => {
117
this._proxy.$acceptRemovedModel(modelUrl);
118
}));
119
120
this._syncedModels[modelUrl] = toDispose;
121
}
122
123
private _stopModelSync(modelUrl: string): void {
124
const toDispose = this._syncedModels[modelUrl];
125
delete this._syncedModels[modelUrl];
126
delete this._syncedModelsLastUsedTime[modelUrl];
127
dispose(toDispose);
128
}
129
}
130
131
export class WorkerTextModelSyncServer implements IWorkerTextModelSyncChannelServer {
132
133
private readonly _models: { [uri: string]: MirrorModel };
134
135
constructor() {
136
this._models = Object.create(null);
137
}
138
139
public bindToServer(workerServer: IWebWorkerServer): void {
140
workerServer.setChannel(WORKER_TEXT_MODEL_SYNC_CHANNEL, this);
141
}
142
143
public getModel(uri: string): ICommonModel | undefined {
144
return this._models[uri];
145
}
146
147
public getModels(): ICommonModel[] {
148
const all: MirrorModel[] = [];
149
Object.keys(this._models).forEach((key) => all.push(this._models[key]));
150
return all;
151
}
152
153
$acceptNewModel(data: IRawModelData): void {
154
this._models[data.url] = new MirrorModel(URI.parse(data.url), data.lines, data.EOL, data.versionId);
155
}
156
157
$acceptModelChanged(uri: string, e: IModelChangedEvent): void {
158
if (!this._models[uri]) {
159
return;
160
}
161
const model = this._models[uri];
162
model.onEvents(e);
163
}
164
165
$acceptRemovedModel(uri: string): void {
166
if (!this._models[uri]) {
167
return;
168
}
169
delete this._models[uri];
170
}
171
}
172
173
export class MirrorModel extends BaseMirrorModel implements ICommonModel {
174
175
public get uri(): URI {
176
return this._uri;
177
}
178
179
public get eol(): string {
180
return this._eol;
181
}
182
183
public getValue(): string {
184
return this.getText();
185
}
186
187
public findMatches(regex: RegExp): RegExpMatchArray[] {
188
const matches = [];
189
for (let i = 0; i < this._lines.length; i++) {
190
const line = this._lines[i];
191
const offsetToAdd = this.offsetAt(new Position(i + 1, 1));
192
const iteratorOverMatches = line.matchAll(regex);
193
for (const match of iteratorOverMatches) {
194
if (match.index || match.index === 0) {
195
match.index = match.index + offsetToAdd;
196
}
197
matches.push(match);
198
}
199
}
200
return matches;
201
}
202
203
public getLinesContent(): string[] {
204
return this._lines.slice(0);
205
}
206
207
public getLineCount(): number {
208
return this._lines.length;
209
}
210
211
public getLineContent(lineNumber: number): string {
212
return this._lines[lineNumber - 1];
213
}
214
215
public getWordAtPosition(position: IPosition, wordDefinition: RegExp): Range | null {
216
217
const wordAtText = getWordAtText(
218
position.column,
219
ensureValidWordDefinition(wordDefinition),
220
this._lines[position.lineNumber - 1],
221
0
222
);
223
224
if (wordAtText) {
225
return new Range(position.lineNumber, wordAtText.startColumn, position.lineNumber, wordAtText.endColumn);
226
}
227
228
return null;
229
}
230
231
public getWordUntilPosition(position: IPosition, wordDefinition: RegExp): IWordAtPosition {
232
const wordAtPosition = this.getWordAtPosition(position, wordDefinition);
233
if (!wordAtPosition) {
234
return {
235
word: '',
236
startColumn: position.column,
237
endColumn: position.column
238
};
239
}
240
return {
241
word: this._lines[position.lineNumber - 1].substring(wordAtPosition.startColumn - 1, position.column - 1),
242
startColumn: wordAtPosition.startColumn,
243
endColumn: position.column
244
};
245
}
246
247
248
public words(wordDefinition: RegExp): Iterable<string> {
249
250
const lines = this._lines;
251
const wordenize = this._wordenize.bind(this);
252
253
let lineNumber = 0;
254
let lineText = '';
255
let wordRangesIdx = 0;
256
let wordRanges: IWordRange[] = [];
257
258
return {
259
*[Symbol.iterator]() {
260
while (true) {
261
if (wordRangesIdx < wordRanges.length) {
262
const value = lineText.substring(wordRanges[wordRangesIdx].start, wordRanges[wordRangesIdx].end);
263
wordRangesIdx += 1;
264
yield value;
265
} else {
266
if (lineNumber < lines.length) {
267
lineText = lines[lineNumber];
268
wordRanges = wordenize(lineText, wordDefinition);
269
wordRangesIdx = 0;
270
lineNumber += 1;
271
} else {
272
break;
273
}
274
}
275
}
276
}
277
};
278
}
279
280
public getLineWords(lineNumber: number, wordDefinition: RegExp): IWordAtPosition[] {
281
const content = this._lines[lineNumber - 1];
282
const ranges = this._wordenize(content, wordDefinition);
283
const words: IWordAtPosition[] = [];
284
for (const range of ranges) {
285
words.push({
286
word: content.substring(range.start, range.end),
287
startColumn: range.start + 1,
288
endColumn: range.end + 1
289
});
290
}
291
return words;
292
}
293
294
private _wordenize(content: string, wordDefinition: RegExp): IWordRange[] {
295
const result: IWordRange[] = [];
296
let match: RegExpExecArray | null;
297
298
wordDefinition.lastIndex = 0; // reset lastIndex just to be sure
299
300
while (match = wordDefinition.exec(content)) {
301
if (match[0].length === 0) {
302
// it did match the empty string
303
break;
304
}
305
result.push({ start: match.index, end: match.index + match[0].length });
306
}
307
return result;
308
}
309
310
public getValueInRange(range: IRange): string {
311
range = this._validateRange(range);
312
313
if (range.startLineNumber === range.endLineNumber) {
314
return this._lines[range.startLineNumber - 1].substring(range.startColumn - 1, range.endColumn - 1);
315
}
316
317
const lineEnding = this._eol;
318
const startLineIndex = range.startLineNumber - 1;
319
const endLineIndex = range.endLineNumber - 1;
320
const resultLines: string[] = [];
321
322
resultLines.push(this._lines[startLineIndex].substring(range.startColumn - 1));
323
for (let i = startLineIndex + 1; i < endLineIndex; i++) {
324
resultLines.push(this._lines[i]);
325
}
326
resultLines.push(this._lines[endLineIndex].substring(0, range.endColumn - 1));
327
328
return resultLines.join(lineEnding);
329
}
330
331
public offsetAt(position: IPosition): number {
332
position = this._validatePosition(position);
333
this._ensureLineStarts();
334
return this._lineStarts!.getPrefixSum(position.lineNumber - 2) + (position.column - 1);
335
}
336
337
public positionAt(offset: number): IPosition {
338
offset = Math.floor(offset);
339
offset = Math.max(0, offset);
340
341
this._ensureLineStarts();
342
const out = this._lineStarts!.getIndexOf(offset);
343
const lineLength = this._lines[out.index].length;
344
345
// Ensure we return a valid position
346
return {
347
lineNumber: 1 + out.index,
348
column: 1 + Math.min(out.remainder, lineLength)
349
};
350
}
351
352
private _validateRange(range: IRange): IRange {
353
354
const start = this._validatePosition({ lineNumber: range.startLineNumber, column: range.startColumn });
355
const end = this._validatePosition({ lineNumber: range.endLineNumber, column: range.endColumn });
356
357
if (start.lineNumber !== range.startLineNumber
358
|| start.column !== range.startColumn
359
|| end.lineNumber !== range.endLineNumber
360
|| end.column !== range.endColumn) {
361
362
return {
363
startLineNumber: start.lineNumber,
364
startColumn: start.column,
365
endLineNumber: end.lineNumber,
366
endColumn: end.column
367
};
368
}
369
370
return range;
371
}
372
373
private _validatePosition(position: IPosition): IPosition {
374
if (!Position.isIPosition(position)) {
375
throw new Error('bad position');
376
}
377
let { lineNumber, column } = position;
378
let hasChanged = false;
379
380
if (lineNumber < 1) {
381
lineNumber = 1;
382
column = 1;
383
hasChanged = true;
384
385
} else if (lineNumber > this._lines.length) {
386
lineNumber = this._lines.length;
387
column = this._lines[lineNumber - 1].length + 1;
388
hasChanged = true;
389
390
} else {
391
const maxCharacter = this._lines[lineNumber - 1].length + 1;
392
if (column < 1) {
393
column = 1;
394
hasChanged = true;
395
}
396
else if (column > maxCharacter) {
397
column = maxCharacter;
398
hasChanged = true;
399
}
400
}
401
402
if (!hasChanged) {
403
return position;
404
} else {
405
return { lineNumber, column };
406
}
407
}
408
}
409
410
export interface ICommonModel extends ILinkComputerTarget, IDocumentColorComputerTarget, IMirrorModel {
411
uri: URI;
412
version: number;
413
eol: string;
414
getValue(): string;
415
416
getLinesContent(): string[];
417
getLineCount(): number;
418
getLineContent(lineNumber: number): string;
419
getLineWords(lineNumber: number, wordDefinition: RegExp): IWordAtPosition[];
420
words(wordDefinition: RegExp): Iterable<string>;
421
getWordUntilPosition(position: IPosition, wordDefinition: RegExp): IWordAtPosition;
422
getValueInRange(range: IRange): string;
423
getWordAtPosition(position: IPosition, wordDefinition: RegExp): Range | null;
424
offsetAt(position: IPosition): number;
425
positionAt(offset: number): IPosition;
426
findMatches(regex: RegExp): RegExpMatchArray[];
427
}
428
429