Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/notebook/common/notebookEditorModel.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 { VSBufferReadableStream, streamToBuffer } from '../../../../base/common/buffer.js';
7
import { CancellationToken } from '../../../../base/common/cancellation.js';
8
import { CancellationError } from '../../../../base/common/errors.js';
9
import { Emitter, Event } from '../../../../base/common/event.js';
10
import { IMarkdownString } from '../../../../base/common/htmlContent.js';
11
import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';
12
import { Schemas } from '../../../../base/common/network.js';
13
import { assertType } from '../../../../base/common/types.js';
14
import { URI } from '../../../../base/common/uri.js';
15
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
16
import { IWriteFileOptions, IFileStatWithMetadata, FileOperationError, FileOperationResult } from '../../../../platform/files/common/files.js';
17
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
18
import { IRevertOptions, ISaveOptions, IUntypedEditorInput } from '../../../common/editor.js';
19
import { EditorModel } from '../../../common/editor/editorModel.js';
20
import { NotebookTextModel } from './model/notebookTextModel.js';
21
import { INotebookEditorModel, INotebookLoadOptions, IResolvedNotebookEditorModel, NotebookCellsChangeType, NotebookSetting } from './notebookCommon.js';
22
import { INotebookLoggingService } from './notebookLoggingService.js';
23
import { INotebookSerializer, INotebookService, SimpleNotebookProviderInfo } from './notebookService.js';
24
import { IFilesConfigurationService } from '../../../services/filesConfiguration/common/filesConfigurationService.js';
25
import { IFileWorkingCopyModelConfiguration, SnapshotContext } from '../../../services/workingCopy/common/fileWorkingCopy.js';
26
import { IFileWorkingCopyManager } from '../../../services/workingCopy/common/fileWorkingCopyManager.js';
27
import { IStoredFileWorkingCopy, IStoredFileWorkingCopyModel, IStoredFileWorkingCopyModelContentChangedEvent, IStoredFileWorkingCopyModelFactory, IStoredFileWorkingCopySaveEvent, StoredFileWorkingCopyState } from '../../../services/workingCopy/common/storedFileWorkingCopy.js';
28
import { IUntitledFileWorkingCopy, IUntitledFileWorkingCopyModel, IUntitledFileWorkingCopyModelContentChangedEvent, IUntitledFileWorkingCopyModelFactory } from '../../../services/workingCopy/common/untitledFileWorkingCopy.js';
29
import { WorkingCopyCapabilities } from '../../../services/workingCopy/common/workingCopy.js';
30
31
//#region --- simple content provider
32
33
export class SimpleNotebookEditorModel extends EditorModel implements INotebookEditorModel {
34
35
private readonly _onDidChangeDirty = this._register(new Emitter<void>());
36
private readonly _onDidSave = this._register(new Emitter<IStoredFileWorkingCopySaveEvent>());
37
private readonly _onDidChangeOrphaned = this._register(new Emitter<void>());
38
private readonly _onDidChangeReadonly = this._register(new Emitter<void>());
39
private readonly _onDidRevertUntitled = this._register(new Emitter<void>());
40
41
readonly onDidChangeDirty: Event<void> = this._onDidChangeDirty.event;
42
readonly onDidSave: Event<IStoredFileWorkingCopySaveEvent> = this._onDidSave.event;
43
readonly onDidChangeOrphaned: Event<void> = this._onDidChangeOrphaned.event;
44
readonly onDidChangeReadonly: Event<void> = this._onDidChangeReadonly.event;
45
readonly onDidRevertUntitled: Event<void> = this._onDidRevertUntitled.event;
46
47
private _workingCopy?: IStoredFileWorkingCopy<NotebookFileWorkingCopyModel> | IUntitledFileWorkingCopy<NotebookFileWorkingCopyModel>;
48
private readonly _workingCopyListeners = this._register(new DisposableStore());
49
private readonly scratchPad: boolean;
50
51
constructor(
52
readonly resource: URI,
53
private readonly _hasAssociatedFilePath: boolean,
54
readonly viewType: string,
55
private readonly _workingCopyManager: IFileWorkingCopyManager<NotebookFileWorkingCopyModel, NotebookFileWorkingCopyModel>,
56
scratchpad: boolean,
57
@IFilesConfigurationService private readonly _filesConfigurationService: IFilesConfigurationService,
58
) {
59
super();
60
61
this.scratchPad = scratchpad;
62
}
63
64
override dispose(): void {
65
this._workingCopy?.dispose();
66
super.dispose();
67
}
68
69
get notebook(): NotebookTextModel | undefined {
70
return this._workingCopy?.model?.notebookModel;
71
}
72
73
override isResolved(): this is IResolvedNotebookEditorModel {
74
return Boolean(this._workingCopy?.model?.notebookModel);
75
}
76
77
async canDispose(): Promise<boolean> {
78
if (!this._workingCopy) {
79
return true;
80
}
81
82
if (SimpleNotebookEditorModel._isStoredFileWorkingCopy(this._workingCopy)) {
83
return this._workingCopyManager.stored.canDispose(this._workingCopy);
84
} else {
85
return true;
86
}
87
}
88
89
isDirty(): boolean {
90
return this._workingCopy?.isDirty() ?? false;
91
}
92
93
isModified(): boolean {
94
return this._workingCopy?.isModified() ?? false;
95
}
96
97
isOrphaned(): boolean {
98
return SimpleNotebookEditorModel._isStoredFileWorkingCopy(this._workingCopy) && this._workingCopy.hasState(StoredFileWorkingCopyState.ORPHAN);
99
}
100
101
hasAssociatedFilePath(): boolean {
102
return !SimpleNotebookEditorModel._isStoredFileWorkingCopy(this._workingCopy) && !!this._workingCopy?.hasAssociatedFilePath;
103
}
104
105
isReadonly(): boolean | IMarkdownString {
106
if (SimpleNotebookEditorModel._isStoredFileWorkingCopy(this._workingCopy)) {
107
return this._workingCopy?.isReadonly();
108
} else {
109
return this._filesConfigurationService.isReadonly(this.resource);
110
}
111
}
112
113
get hasErrorState(): boolean {
114
if (this._workingCopy && 'hasState' in this._workingCopy) {
115
return this._workingCopy.hasState(StoredFileWorkingCopyState.ERROR);
116
}
117
118
return false;
119
}
120
121
async revert(options?: IRevertOptions): Promise<void> {
122
assertType(this.isResolved());
123
return this._workingCopy!.revert(options);
124
}
125
126
async save(options?: ISaveOptions): Promise<boolean> {
127
assertType(this.isResolved());
128
return this._workingCopy!.save(options);
129
}
130
131
async load(options?: INotebookLoadOptions): Promise<IResolvedNotebookEditorModel> {
132
if (!this._workingCopy || !this._workingCopy.model) {
133
if (this.resource.scheme === Schemas.untitled) {
134
if (this._hasAssociatedFilePath) {
135
this._workingCopy = await this._workingCopyManager.resolve({ associatedResource: this.resource });
136
} else {
137
this._workingCopy = await this._workingCopyManager.resolve({ untitledResource: this.resource, isScratchpad: this.scratchPad });
138
}
139
this._register(this._workingCopy.onDidRevert(() => this._onDidRevertUntitled.fire()));
140
} else {
141
this._workingCopy = await this._workingCopyManager.resolve(this.resource, {
142
limits: options?.limits,
143
reload: options?.forceReadFromFile ? { async: false, force: true } : undefined
144
});
145
this._workingCopyListeners.add(this._workingCopy.onDidSave(e => this._onDidSave.fire(e)));
146
this._workingCopyListeners.add(this._workingCopy.onDidChangeOrphaned(() => this._onDidChangeOrphaned.fire()));
147
this._workingCopyListeners.add(this._workingCopy.onDidChangeReadonly(() => this._onDidChangeReadonly.fire()));
148
}
149
this._workingCopyListeners.add(this._workingCopy.onDidChangeDirty(() => this._onDidChangeDirty.fire(), undefined));
150
151
this._workingCopyListeners.add(this._workingCopy.onWillDispose(() => {
152
this._workingCopyListeners.clear();
153
this._workingCopy?.model?.dispose();
154
}));
155
} else {
156
await this._workingCopyManager.resolve(this.resource, {
157
reload: {
158
async: !options?.forceReadFromFile,
159
force: options?.forceReadFromFile
160
},
161
limits: options?.limits
162
});
163
}
164
165
assertType(this.isResolved());
166
return this;
167
}
168
169
async saveAs(target: URI): Promise<IUntypedEditorInput | undefined> {
170
const newWorkingCopy = await this._workingCopyManager.saveAs(this.resource, target);
171
if (!newWorkingCopy) {
172
return undefined;
173
}
174
// this is a little hacky because we leave the new working copy alone. BUT
175
// the newly created editor input will pick it up and claim ownership of it.
176
return { resource: newWorkingCopy.resource };
177
}
178
179
private static _isStoredFileWorkingCopy(candidate?: IStoredFileWorkingCopy<NotebookFileWorkingCopyModel> | IUntitledFileWorkingCopy<NotebookFileWorkingCopyModel>): candidate is IStoredFileWorkingCopy<NotebookFileWorkingCopyModel> {
180
const isUntitled = candidate && candidate.capabilities & WorkingCopyCapabilities.Untitled;
181
182
return !isUntitled;
183
}
184
}
185
186
export class NotebookFileWorkingCopyModel extends Disposable implements IStoredFileWorkingCopyModel, IUntitledFileWorkingCopyModel {
187
188
private readonly _onDidChangeContent = this._register(new Emitter<IStoredFileWorkingCopyModelContentChangedEvent & IUntitledFileWorkingCopyModelContentChangedEvent>());
189
readonly onDidChangeContent = this._onDidChangeContent.event;
190
191
readonly onWillDispose: Event<void>;
192
193
readonly configuration: IFileWorkingCopyModelConfiguration | undefined = undefined;
194
save: ((options: IWriteFileOptions, token: CancellationToken) => Promise<IFileStatWithMetadata>) | undefined;
195
196
constructor(
197
private readonly _notebookModel: NotebookTextModel,
198
private readonly _notebookService: INotebookService,
199
private readonly _configurationService: IConfigurationService,
200
private readonly _telemetryService: ITelemetryService,
201
private readonly _notebookLogService: INotebookLoggingService,
202
) {
203
super();
204
205
this.onWillDispose = _notebookModel.onWillDispose.bind(_notebookModel);
206
207
this._register(_notebookModel.onDidChangeContent(e => {
208
for (const rawEvent of e.rawEvents) {
209
if (rawEvent.kind === NotebookCellsChangeType.Initialize) {
210
continue;
211
}
212
if (rawEvent.transient) {
213
continue;
214
}
215
this._onDidChangeContent.fire({
216
isRedoing: false, //todo@rebornix forward this information from notebook model
217
isUndoing: false,
218
isInitial: false, //_notebookModel.cells.length === 0 // todo@jrieken non transient metadata?
219
});
220
break;
221
}
222
}));
223
224
const saveWithReducedCommunication = this._configurationService.getValue(NotebookSetting.remoteSaving);
225
226
if (saveWithReducedCommunication || _notebookModel.uri.scheme === Schemas.vscodeRemote) {
227
this.configuration = {
228
// Intentionally pick a larger delay for triggering backups to allow auto-save
229
// to complete first on the optimized save path
230
backupDelay: 10000
231
};
232
}
233
234
// Override save behavior to avoid transferring the buffer across the wire 3 times
235
if (saveWithReducedCommunication) {
236
this.setSaveDelegate().catch(error => this._notebookLogService.error('WorkingCopyModel', `Failed to set save delegate: ${error}`));
237
}
238
}
239
240
private async setSaveDelegate() {
241
// make sure we wait for a serializer to resolve before we try to handle saves in the EH
242
await this.getNotebookSerializer();
243
244
this.save = async (options: IWriteFileOptions, token: CancellationToken) => {
245
try {
246
let serializer = this._notebookService.tryGetDataProviderSync(this.notebookModel.viewType)?.serializer;
247
248
if (!serializer) {
249
this._notebookLogService.info('WorkingCopyModel', 'No serializer found for notebook model, checking if provider still needs to be resolved');
250
serializer = await this.getNotebookSerializer().catch(error => {
251
this._notebookLogService.error('WorkingCopyModel', `Failed to get notebook serializer: ${error}`);
252
// The serializer was set initially but somehow is no longer available
253
this.save = undefined;
254
throw new NotebookSaveError('Failed to get notebook serializer');
255
});
256
}
257
258
if (token.isCancellationRequested) {
259
throw new CancellationError();
260
}
261
262
const stat = await serializer.save(this._notebookModel.uri, this._notebookModel.versionId, options, token);
263
return stat;
264
} catch (error) {
265
if (!token.isCancellationRequested && error.name !== 'Canceled') {
266
type notebookSaveErrorData = {
267
isRemote: boolean;
268
isIPyNbWorkerSerializer: boolean;
269
error: string;
270
};
271
type notebookSaveErrorClassification = {
272
owner: 'amunger';
273
comment: 'Detect if we are having issues saving a notebook on the Extension Host';
274
isRemote: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Whether the save is happening on a remote file system' };
275
isIPyNbWorkerSerializer: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Whether the IPynb files are serialized in workers' };
276
error: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Info about the error that occurred' };
277
};
278
const isIPynb = this._notebookModel.viewType === 'jupyter-notebook' || this._notebookModel.viewType === 'interactive';
279
const errorMessage = getSaveErrorMessage(error);
280
this._telemetryService.publicLogError2<notebookSaveErrorData, notebookSaveErrorClassification>('notebook/SaveError', {
281
isRemote: this._notebookModel.uri.scheme === Schemas.vscodeRemote,
282
isIPyNbWorkerSerializer: isIPynb && this._configurationService.getValue<boolean>('ipynb.experimental.serialization'),
283
error: errorMessage
284
});
285
}
286
287
throw error;
288
}
289
};
290
}
291
292
override dispose(): void {
293
this._notebookModel.dispose();
294
super.dispose();
295
}
296
297
get notebookModel() {
298
return this._notebookModel;
299
}
300
301
async snapshot(context: SnapshotContext, token: CancellationToken): Promise<VSBufferReadableStream> {
302
return this._notebookService.createNotebookTextDocumentSnapshot(this._notebookModel.uri, context, token);
303
}
304
305
async update(stream: VSBufferReadableStream, token: CancellationToken): Promise<void> {
306
const serializer = await this.getNotebookSerializer();
307
308
const bytes = await streamToBuffer(stream);
309
const data = await serializer.dataToNotebook(bytes);
310
311
if (token.isCancellationRequested) {
312
throw new CancellationError();
313
}
314
315
this._notebookLogService.info('WorkingCopyModel', 'Notebook content updated from file system - ' + this._notebookModel.uri.toString());
316
this._notebookModel.reset(data.cells, data.metadata, serializer.options);
317
}
318
319
async getNotebookSerializer(): Promise<INotebookSerializer> {
320
const info = await this._notebookService.withNotebookDataProvider(this.notebookModel.viewType);
321
if (!(info instanceof SimpleNotebookProviderInfo)) {
322
const message = 'CANNOT open notebook with this provider';
323
throw new NotebookSaveError(message);
324
}
325
326
return info.serializer;
327
}
328
329
get versionId() {
330
return this._notebookModel.alternativeVersionId;
331
}
332
333
pushStackElement(): void {
334
this._notebookModel.pushStackElement();
335
}
336
}
337
338
export class NotebookFileWorkingCopyModelFactory implements IStoredFileWorkingCopyModelFactory<NotebookFileWorkingCopyModel>, IUntitledFileWorkingCopyModelFactory<NotebookFileWorkingCopyModel> {
339
340
constructor(
341
private readonly _viewType: string,
342
@INotebookService private readonly _notebookService: INotebookService,
343
@IConfigurationService private readonly _configurationService: IConfigurationService,
344
@ITelemetryService private readonly _telemetryService: ITelemetryService,
345
@INotebookLoggingService private readonly _notebookLogService: INotebookLoggingService
346
) { }
347
348
async createModel(resource: URI, stream: VSBufferReadableStream, token: CancellationToken): Promise<NotebookFileWorkingCopyModel> {
349
350
const notebookModel = this._notebookService.getNotebookTextModel(resource) ??
351
await this._notebookService.createNotebookTextModel(this._viewType, resource, stream);
352
353
return new NotebookFileWorkingCopyModel(notebookModel, this._notebookService, this._configurationService, this._telemetryService, this._notebookLogService);
354
}
355
}
356
357
//#endregion
358
359
class NotebookSaveError extends Error {
360
constructor(message: string) {
361
super(message);
362
this.name = 'NotebookSaveError';
363
}
364
}
365
366
function getSaveErrorMessage(error: Error): string {
367
if (error.name === 'NotebookSaveError') {
368
return error.message;
369
} else if (error instanceof FileOperationError) {
370
switch (error.fileOperationResult) {
371
case FileOperationResult.FILE_IS_DIRECTORY:
372
return 'File is a directory';
373
case FileOperationResult.FILE_NOT_FOUND:
374
return 'File not found';
375
case FileOperationResult.FILE_NOT_MODIFIED_SINCE:
376
return 'File not modified since';
377
case FileOperationResult.FILE_MODIFIED_SINCE:
378
return 'File modified since';
379
case FileOperationResult.FILE_MOVE_CONFLICT:
380
return 'File move conflict';
381
case FileOperationResult.FILE_WRITE_LOCKED:
382
return 'File write locked';
383
case FileOperationResult.FILE_PERMISSION_DENIED:
384
return 'File permission denied';
385
case FileOperationResult.FILE_TOO_LARGE:
386
return 'File too large';
387
case FileOperationResult.FILE_INVALID_PATH:
388
return 'File invalid path';
389
case FileOperationResult.FILE_NOT_DIRECTORY:
390
return 'File not directory';
391
case FileOperationResult.FILE_OTHER_ERROR:
392
return 'File other error';
393
}
394
}
395
return 'Unknown error';
396
}
397
398