Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/api/browser/mainThreadDocuments.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 { toErrorMessage } from '../../../base/common/errorMessage.js';
7
import { IReference, dispose, Disposable } from '../../../base/common/lifecycle.js';
8
import { Schemas } from '../../../base/common/network.js';
9
import { URI, UriComponents } from '../../../base/common/uri.js';
10
import { ITextModel, shouldSynchronizeModel } from '../../../editor/common/model.js';
11
import { IModelService } from '../../../editor/common/services/model.js';
12
import { ITextModelService } from '../../../editor/common/services/resolverService.js';
13
import { IFileService, FileOperation } from '../../../platform/files/common/files.js';
14
import { ExtHostContext, ExtHostDocumentsShape, MainThreadDocumentsShape } from '../common/extHost.protocol.js';
15
import { EncodingMode, ITextFileEditorModel, ITextFileService, TextFileResolveReason } from '../../services/textfile/common/textfiles.js';
16
import { IUntitledTextEditorModel } from '../../services/untitled/common/untitledTextEditorModel.js';
17
import { IWorkbenchEnvironmentService } from '../../services/environment/common/environmentService.js';
18
import { toLocalResource, extUri, IExtUri } from '../../../base/common/resources.js';
19
import { IWorkingCopyFileService } from '../../services/workingCopy/common/workingCopyFileService.js';
20
import { IUriIdentityService } from '../../../platform/uriIdentity/common/uriIdentity.js';
21
import { Emitter, Event } from '../../../base/common/event.js';
22
import { IPathService } from '../../services/path/common/pathService.js';
23
import { ResourceMap } from '../../../base/common/map.js';
24
import { IExtHostContext } from '../../services/extensions/common/extHostCustomers.js';
25
import { ErrorNoTelemetry, onUnexpectedError } from '../../../base/common/errors.js';
26
import { ISerializedModelContentChangedEvent } from '../../../editor/common/textModelEvents.js';
27
28
export class BoundModelReferenceCollection {
29
30
private _data = new Array<{ uri: URI; length: number; dispose(): void }>();
31
private _length = 0;
32
33
constructor(
34
private readonly _extUri: IExtUri,
35
private readonly _maxAge: number = 1000 * 60 * 3, // auto-dispse by age
36
private readonly _maxLength: number = 1024 * 1024 * 80, // auto-dispose by total length
37
private readonly _maxSize: number = 50 // auto-dispose by number of references
38
) {
39
//
40
}
41
42
dispose(): void {
43
this._data = dispose(this._data);
44
}
45
46
remove(uri: URI): void {
47
for (const entry of [...this._data] /* copy array because dispose will modify it */) {
48
if (this._extUri.isEqualOrParent(entry.uri, uri)) {
49
entry.dispose();
50
}
51
}
52
}
53
54
add(uri: URI, ref: IReference<any>, length: number = 0): void {
55
// const length = ref.object.textEditorModel.getValueLength();
56
const dispose = () => {
57
const idx = this._data.indexOf(entry);
58
if (idx >= 0) {
59
this._length -= length;
60
ref.dispose();
61
clearTimeout(handle);
62
this._data.splice(idx, 1);
63
}
64
};
65
const handle = setTimeout(dispose, this._maxAge);
66
const entry = { uri, length, dispose };
67
68
this._data.push(entry);
69
this._length += length;
70
this._cleanup();
71
}
72
73
private _cleanup(): void {
74
// clean-up wrt total length
75
while (this._length > this._maxLength) {
76
this._data[0].dispose();
77
}
78
// clean-up wrt number of documents
79
const extraSize = Math.ceil(this._maxSize * 1.2);
80
if (this._data.length >= extraSize) {
81
dispose(this._data.slice(0, extraSize - this._maxSize));
82
}
83
}
84
}
85
86
class ModelTracker extends Disposable {
87
88
private _knownVersionId: number;
89
90
constructor(
91
private readonly _model: ITextModel,
92
private readonly _onIsCaughtUpWithContentChanges: Emitter<URI>,
93
private readonly _proxy: ExtHostDocumentsShape,
94
private readonly _textFileService: ITextFileService,
95
) {
96
super();
97
this._knownVersionId = this._model.getVersionId();
98
this._store.add(this._model.onDidChangeContent((e) => {
99
this._knownVersionId = e.versionId;
100
if (e.detailedReasonsChangeLengths.length !== 1) {
101
onUnexpectedError(new Error(`Unexpected reasons: ${e.detailedReasons.map(r => r.toString())}`));
102
}
103
104
const evt: ISerializedModelContentChangedEvent = {
105
changes: e.changes,
106
isEolChange: e.isEolChange,
107
isUndoing: e.isUndoing,
108
isRedoing: e.isRedoing,
109
isFlush: e.isFlush,
110
eol: e.eol,
111
versionId: e.versionId,
112
detailedReason: e.detailedReasons[0].metadata,
113
};
114
this._proxy.$acceptModelChanged(this._model.uri, evt, this._textFileService.isDirty(this._model.uri));
115
if (this.isCaughtUpWithContentChanges()) {
116
this._onIsCaughtUpWithContentChanges.fire(this._model.uri);
117
}
118
}));
119
}
120
121
isCaughtUpWithContentChanges(): boolean {
122
return (this._model.getVersionId() === this._knownVersionId);
123
}
124
}
125
126
export class MainThreadDocuments extends Disposable implements MainThreadDocumentsShape {
127
128
private _onIsCaughtUpWithContentChanges = this._store.add(new Emitter<URI>());
129
readonly onIsCaughtUpWithContentChanges = this._onIsCaughtUpWithContentChanges.event;
130
131
private readonly _proxy: ExtHostDocumentsShape;
132
private readonly _modelTrackers = new ResourceMap<ModelTracker>();
133
private readonly _modelReferenceCollection: BoundModelReferenceCollection;
134
135
constructor(
136
extHostContext: IExtHostContext,
137
@IModelService private readonly _modelService: IModelService,
138
@ITextFileService private readonly _textFileService: ITextFileService,
139
@IFileService private readonly _fileService: IFileService,
140
@ITextModelService private readonly _textModelResolverService: ITextModelService,
141
@IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService,
142
@IUriIdentityService private readonly _uriIdentityService: IUriIdentityService,
143
@IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService,
144
@IPathService private readonly _pathService: IPathService
145
) {
146
super();
147
148
this._modelReferenceCollection = this._store.add(new BoundModelReferenceCollection(_uriIdentityService.extUri));
149
150
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostDocuments);
151
152
this._store.add(_modelService.onModelLanguageChanged(this._onModelModeChanged, this));
153
154
this._store.add(_textFileService.files.onDidSave(e => {
155
if (this._shouldHandleFileEvent(e.model.resource)) {
156
this._proxy.$acceptModelSaved(e.model.resource);
157
}
158
}));
159
this._store.add(_textFileService.files.onDidChangeDirty(m => {
160
if (this._shouldHandleFileEvent(m.resource)) {
161
this._proxy.$acceptDirtyStateChanged(m.resource, m.isDirty());
162
}
163
}));
164
this._store.add(Event.any<ITextFileEditorModel | IUntitledTextEditorModel>(_textFileService.files.onDidChangeEncoding, _textFileService.untitled.onDidChangeEncoding)(m => {
165
if (this._shouldHandleFileEvent(m.resource)) {
166
const encoding = m.getEncoding();
167
if (encoding) {
168
this._proxy.$acceptEncodingChanged(m.resource, encoding);
169
}
170
}
171
}));
172
173
this._store.add(workingCopyFileService.onDidRunWorkingCopyFileOperation(e => {
174
const isMove = e.operation === FileOperation.MOVE;
175
if (isMove || e.operation === FileOperation.DELETE) {
176
for (const pair of e.files) {
177
const removed = isMove ? pair.source : pair.target;
178
if (removed) {
179
this._modelReferenceCollection.remove(removed);
180
}
181
}
182
}
183
}));
184
}
185
186
override dispose(): void {
187
dispose(this._modelTrackers.values());
188
this._modelTrackers.clear();
189
super.dispose();
190
}
191
192
isCaughtUpWithContentChanges(resource: URI): boolean {
193
const tracker = this._modelTrackers.get(resource);
194
if (tracker) {
195
return tracker.isCaughtUpWithContentChanges();
196
}
197
return true;
198
}
199
200
private _shouldHandleFileEvent(resource: URI): boolean {
201
const model = this._modelService.getModel(resource);
202
return !!model && shouldSynchronizeModel(model);
203
}
204
205
handleModelAdded(model: ITextModel): void {
206
// Same filter as in mainThreadEditorsTracker
207
if (!shouldSynchronizeModel(model)) {
208
// don't synchronize too large models
209
return;
210
}
211
this._modelTrackers.set(model.uri, new ModelTracker(model, this._onIsCaughtUpWithContentChanges, this._proxy, this._textFileService));
212
}
213
214
private _onModelModeChanged(event: { model: ITextModel; oldLanguageId: string }): void {
215
const { model } = event;
216
if (!this._modelTrackers.has(model.uri)) {
217
return;
218
}
219
this._proxy.$acceptModelLanguageChanged(model.uri, model.getLanguageId());
220
}
221
222
handleModelRemoved(modelUrl: URI): void {
223
if (!this._modelTrackers.has(modelUrl)) {
224
return;
225
}
226
this._modelTrackers.get(modelUrl)!.dispose();
227
this._modelTrackers.delete(modelUrl);
228
}
229
230
// --- from extension host process
231
232
async $trySaveDocument(uri: UriComponents): Promise<boolean> {
233
const target = await this._textFileService.save(URI.revive(uri));
234
return Boolean(target);
235
}
236
237
async $tryOpenDocument(uriData: UriComponents, options?: { encoding?: string }): Promise<URI> {
238
const inputUri = URI.revive(uriData);
239
if (!inputUri.scheme || !(inputUri.fsPath || inputUri.authority)) {
240
throw new ErrorNoTelemetry(`Invalid uri. Scheme and authority or path must be set.`);
241
}
242
243
const canonicalUri = this._uriIdentityService.asCanonicalUri(inputUri);
244
245
let promise: Promise<URI>;
246
switch (canonicalUri.scheme) {
247
case Schemas.untitled:
248
promise = this._handleUntitledScheme(canonicalUri, options);
249
break;
250
case Schemas.file:
251
default:
252
promise = this._handleAsResourceInput(canonicalUri, options);
253
break;
254
}
255
256
let documentUri: URI | undefined;
257
try {
258
documentUri = await promise;
259
} catch (err) {
260
throw new ErrorNoTelemetry(`cannot open ${canonicalUri.toString()}. Detail: ${toErrorMessage(err)}`);
261
}
262
if (!documentUri) {
263
throw new ErrorNoTelemetry(`cannot open ${canonicalUri.toString()}`);
264
} else if (!extUri.isEqual(documentUri, canonicalUri)) {
265
throw new ErrorNoTelemetry(`cannot open ${canonicalUri.toString()}. Detail: Actual document opened as ${documentUri.toString()}`);
266
} else if (!this._modelTrackers.has(canonicalUri)) {
267
throw new ErrorNoTelemetry(`cannot open ${canonicalUri.toString()}. Detail: Files above 50MB cannot be synchronized with extensions.`);
268
} else {
269
return canonicalUri;
270
}
271
}
272
273
$tryCreateDocument(options?: { language?: string; content?: string; encoding?: string }): Promise<URI> {
274
return this._doCreateUntitled(undefined, options);
275
}
276
277
private async _handleAsResourceInput(uri: URI, options?: { encoding?: string }): Promise<URI> {
278
if (options?.encoding) {
279
const model = await this._textFileService.files.resolve(uri, { encoding: options.encoding, reason: TextFileResolveReason.REFERENCE });
280
if (model.isDirty()) {
281
throw new ErrorNoTelemetry(`Cannot re-open a dirty text document with different encoding. Save it first.`);
282
}
283
await model.setEncoding(options.encoding, EncodingMode.Decode);
284
}
285
286
const ref = await this._textModelResolverService.createModelReference(uri);
287
this._modelReferenceCollection.add(uri, ref, ref.object.textEditorModel.getValueLength());
288
return ref.object.textEditorModel.uri;
289
}
290
291
private async _handleUntitledScheme(uri: URI, options?: { encoding?: string }): Promise<URI> {
292
const asLocalUri = toLocalResource(uri, this._environmentService.remoteAuthority, this._pathService.defaultUriScheme);
293
const exists = await this._fileService.exists(asLocalUri);
294
if (exists) {
295
// don't create a new file ontop of an existing file
296
return Promise.reject(new Error('file already exists'));
297
}
298
return await this._doCreateUntitled(Boolean(uri.path) ? uri : undefined, options);
299
}
300
301
private async _doCreateUntitled(associatedResource?: URI, options?: { language?: string; content?: string; encoding?: string }): Promise<URI> {
302
const model = this._textFileService.untitled.create({
303
associatedResource,
304
languageId: options?.language,
305
initialValue: options?.content,
306
encoding: options?.encoding
307
});
308
if (options?.encoding) {
309
await model.setEncoding(options.encoding);
310
}
311
const resource = model.resource;
312
const ref = await this._textModelResolverService.createModelReference(resource);
313
if (!this._modelTrackers.has(resource)) {
314
ref.dispose();
315
throw new Error(`expected URI ${resource.toString()} to have come to LIFE`);
316
}
317
this._modelReferenceCollection.add(resource, ref, ref.object.textEditorModel.getValueLength());
318
Event.once(model.onDidRevert)(() => this._modelReferenceCollection.remove(resource));
319
this._proxy.$acceptDirtyStateChanged(resource, true); // mark as dirty
320
return resource;
321
}
322
}
323
324