Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/notebook/common/notebookEditorInput.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 * as glob from '../../../../base/common/glob.js';
7
import { GroupIdentifier, ISaveOptions, IMoveResult, IRevertOptions, EditorInputCapabilities, Verbosity, IUntypedEditorInput, IFileLimitedEditorInputOptions, isResourceEditorInput } from '../../../common/editor.js';
8
import { EditorInput } from '../../../common/editor/editorInput.js';
9
import { INotebookService, SimpleNotebookProviderInfo } from './notebookService.js';
10
import { URI } from '../../../../base/common/uri.js';
11
import { isEqual, joinPath } from '../../../../base/common/resources.js';
12
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
13
import { IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js';
14
import { INotebookEditorModelResolverService } from './notebookEditorModelResolverService.js';
15
import { IDisposable, IReference } from '../../../../base/common/lifecycle.js';
16
import { CellEditType, CellUri, IResolvedNotebookEditorModel } from './notebookCommon.js';
17
import { ILabelService } from '../../../../platform/label/common/label.js';
18
import { Schemas } from '../../../../base/common/network.js';
19
import { IFileService } from '../../../../platform/files/common/files.js';
20
import { AbstractResourceEditorInput } from '../../../common/editor/resourceEditorInput.js';
21
import { IResourceEditorInput } from '../../../../platform/editor/common/editor.js';
22
import { onUnexpectedError } from '../../../../base/common/errors.js';
23
import { VSBuffer } from '../../../../base/common/buffer.js';
24
import { IWorkingCopyIdentifier } from '../../../services/workingCopy/common/workingCopy.js';
25
import { NotebookProviderInfo } from './notebookProvider.js';
26
import { NotebookPerfMarks } from './notebookPerformance.js';
27
import { IFilesConfigurationService } from '../../../services/filesConfiguration/common/filesConfigurationService.js';
28
import { IExtensionService } from '../../../services/extensions/common/extensions.js';
29
import { localize } from '../../../../nls.js';
30
import { IEditorService } from '../../../services/editor/common/editorService.js';
31
import { IMarkdownString } from '../../../../base/common/htmlContent.js';
32
import { ITextResourceConfigurationService } from '../../../../editor/common/services/textResourceConfiguration.js';
33
import { ICustomEditorLabelService } from '../../../services/editor/common/customEditorLabelService.js';
34
35
export interface NotebookEditorInputOptions {
36
startDirty?: boolean;
37
/**
38
* backupId for webview
39
*/
40
_backupId?: string;
41
_workingCopy?: IWorkingCopyIdentifier;
42
}
43
44
export class NotebookEditorInput extends AbstractResourceEditorInput {
45
46
static getOrCreate(instantiationService: IInstantiationService, resource: URI, preferredResource: URI | undefined, viewType: string, options: NotebookEditorInputOptions = {}) {
47
const editor = instantiationService.createInstance(NotebookEditorInput, resource, preferredResource, viewType, options);
48
if (preferredResource) {
49
editor.setPreferredResource(preferredResource);
50
}
51
return editor;
52
}
53
54
static readonly ID: string = 'workbench.input.notebook';
55
56
protected editorModelReference: IReference<IResolvedNotebookEditorModel> | null = null;
57
private _sideLoadedListener: IDisposable;
58
private _defaultDirtyState: boolean = false;
59
60
constructor(
61
resource: URI,
62
preferredResource: URI | undefined,
63
public readonly viewType: string,
64
public readonly options: NotebookEditorInputOptions,
65
@INotebookService private readonly _notebookService: INotebookService,
66
@INotebookEditorModelResolverService private readonly _notebookModelResolverService: INotebookEditorModelResolverService,
67
@IFileDialogService private readonly _fileDialogService: IFileDialogService,
68
@ILabelService labelService: ILabelService,
69
@IFileService fileService: IFileService,
70
@IFilesConfigurationService filesConfigurationService: IFilesConfigurationService,
71
@IExtensionService extensionService: IExtensionService,
72
@IEditorService editorService: IEditorService,
73
@ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService,
74
@ICustomEditorLabelService customEditorLabelService: ICustomEditorLabelService
75
) {
76
super(resource, preferredResource, labelService, fileService, filesConfigurationService, textResourceConfigurationService, customEditorLabelService);
77
this._defaultDirtyState = !!options.startDirty;
78
79
// Automatically resolve this input when the "wanted" model comes to life via
80
// some other way. This happens only once per input and resolve disposes
81
// this listener
82
this._sideLoadedListener = _notebookService.onDidAddNotebookDocument(e => {
83
if (e.viewType === this.viewType && e.uri.toString() === this.resource.toString()) {
84
this.resolve().catch(onUnexpectedError);
85
}
86
});
87
88
this._register(extensionService.onWillStop(e => {
89
if (!e.auto && !this.isDirty()) {
90
return;
91
}
92
93
const reason = e.auto
94
? localize('vetoAutoExtHostRestart', "An extension provided notebook for '{0}' is still open that would close otherwise.", this.getName())
95
: localize('vetoExtHostRestart', "An extension provided notebook for '{0}' could not be saved.", this.getName());
96
97
e.veto((async () => {
98
const editors = editorService.findEditors(this);
99
if (e.auto) {
100
return true;
101
}
102
if (editors.length > 0) {
103
const result = await editorService.save(editors[0]);
104
if (result.success) {
105
return false; // Don't Veto
106
}
107
}
108
return true; // Veto
109
})(), reason);
110
}));
111
}
112
113
override dispose() {
114
this._sideLoadedListener.dispose();
115
this.editorModelReference?.dispose();
116
this.editorModelReference = null;
117
super.dispose();
118
}
119
120
override get typeId(): string {
121
return NotebookEditorInput.ID;
122
}
123
124
override get editorId(): string | undefined {
125
return this.viewType;
126
}
127
128
override get capabilities(): EditorInputCapabilities {
129
let capabilities = EditorInputCapabilities.None;
130
131
if (this.resource.scheme === Schemas.untitled) {
132
capabilities |= EditorInputCapabilities.Untitled;
133
}
134
135
if (this.editorModelReference) {
136
if (this.editorModelReference.object.isReadonly()) {
137
capabilities |= EditorInputCapabilities.Readonly;
138
}
139
} else {
140
if (this.filesConfigurationService.isReadonly(this.resource)) {
141
capabilities |= EditorInputCapabilities.Readonly;
142
}
143
}
144
145
if (!(capabilities & EditorInputCapabilities.Readonly)) {
146
capabilities |= EditorInputCapabilities.CanDropIntoEditor;
147
}
148
149
return capabilities;
150
}
151
152
override getDescription(verbosity = Verbosity.MEDIUM): string | undefined {
153
if (!this.hasCapability(EditorInputCapabilities.Untitled) || this.editorModelReference?.object.hasAssociatedFilePath()) {
154
return super.getDescription(verbosity);
155
}
156
157
return undefined; // no description for untitled notebooks without associated file path
158
}
159
160
override isReadonly(): boolean | IMarkdownString {
161
if (!this.editorModelReference) {
162
return this.filesConfigurationService.isReadonly(this.resource);
163
}
164
return this.editorModelReference.object.isReadonly();
165
}
166
167
override isDirty() {
168
if (!this.editorModelReference) {
169
return this._defaultDirtyState;
170
}
171
return this.editorModelReference.object.isDirty();
172
}
173
174
override isSaving(): boolean {
175
const model = this.editorModelReference?.object;
176
if (!model || !model.isDirty() || model.hasErrorState || this.hasCapability(EditorInputCapabilities.Untitled)) {
177
return false; // require the model to be dirty, file-backed and not in an error state
178
}
179
180
// if a short auto save is configured, treat this as being saved
181
return this.filesConfigurationService.hasShortAutoSaveDelay(this);
182
}
183
184
override async save(group: GroupIdentifier, options?: ISaveOptions): Promise<EditorInput | IUntypedEditorInput | undefined> {
185
if (this.editorModelReference) {
186
187
if (this.hasCapability(EditorInputCapabilities.Untitled)) {
188
return this.saveAs(group, options);
189
} else {
190
await this.editorModelReference.object.save(options);
191
}
192
193
return this;
194
}
195
196
return undefined;
197
}
198
199
override async saveAs(group: GroupIdentifier, options?: ISaveOptions): Promise<IUntypedEditorInput | undefined> {
200
if (!this.editorModelReference) {
201
return undefined;
202
}
203
204
const provider = this._notebookService.getContributedNotebookType(this.viewType);
205
206
if (!provider) {
207
return undefined;
208
}
209
210
const pathCandidate = this.hasCapability(EditorInputCapabilities.Untitled) ? await this._suggestName(provider, this.labelService.getUriBasenameLabel(this.resource)) : this.editorModelReference.object.resource;
211
let target: URI | undefined;
212
if (this.editorModelReference.object.hasAssociatedFilePath()) {
213
target = pathCandidate;
214
} else {
215
target = await this._fileDialogService.pickFileToSave(pathCandidate, options?.availableFileSystems);
216
if (!target) {
217
return undefined; // save cancelled
218
}
219
}
220
221
if (!provider.matches(target)) {
222
const patterns = provider.selectors.map(pattern => {
223
if (typeof pattern === 'string') {
224
return pattern;
225
}
226
227
if (glob.isRelativePattern(pattern)) {
228
return `${pattern} (base ${pattern.base})`;
229
}
230
231
if (pattern.exclude) {
232
return `${pattern.include} (exclude: ${pattern.exclude})`;
233
} else {
234
return `${pattern.include}`;
235
}
236
237
}).join(', ');
238
throw new Error(`File name ${target} is not supported by ${provider.providerDisplayName}.\n\nPlease make sure the file name matches following patterns:\n${patterns}`);
239
}
240
241
return await this.editorModelReference.object.saveAs(target);
242
}
243
244
private async _suggestName(provider: NotebookProviderInfo, suggestedFilename: string) {
245
// guess file extensions
246
const firstSelector = provider.selectors[0];
247
let selectorStr = firstSelector && typeof firstSelector === 'string' ? firstSelector : undefined;
248
if (!selectorStr && firstSelector) {
249
const include = (firstSelector as { include?: string }).include;
250
if (typeof include === 'string') {
251
selectorStr = include;
252
}
253
}
254
255
if (selectorStr) {
256
const matches = /^\*\.([A-Za-z_-]*)$/.exec(selectorStr);
257
if (matches && matches.length > 1) {
258
const fileExt = matches[1];
259
if (!suggestedFilename.endsWith(fileExt)) {
260
return joinPath(await this._fileDialogService.defaultFilePath(), suggestedFilename + '.' + fileExt);
261
}
262
}
263
}
264
265
return joinPath(await this._fileDialogService.defaultFilePath(), suggestedFilename);
266
}
267
268
// called when users rename a notebook document
269
override async rename(group: GroupIdentifier, target: URI): Promise<IMoveResult | undefined> {
270
if (this.editorModelReference) {
271
return { editor: { resource: target }, options: { override: this.viewType } };
272
273
}
274
return undefined;
275
}
276
277
override async revert(_group: GroupIdentifier, options?: IRevertOptions): Promise<void> {
278
if (this.editorModelReference && this.editorModelReference.object.isDirty()) {
279
await this.editorModelReference.object.revert(options);
280
}
281
}
282
283
override async resolve(_options?: IFileLimitedEditorInputOptions, perf?: NotebookPerfMarks): Promise<IResolvedNotebookEditorModel | null> {
284
if (!await this._notebookService.canResolve(this.viewType)) {
285
return null;
286
}
287
288
perf?.mark('extensionActivated');
289
290
// we are now loading the notebook and don't need to listen to
291
// "other" loading anymore
292
this._sideLoadedListener.dispose();
293
294
if (!this.editorModelReference) {
295
const scratchpad = this.capabilities & EditorInputCapabilities.Scratchpad ? true : false;
296
const ref = await this._notebookModelResolverService.resolve(this.resource, this.viewType, { limits: this.ensureLimits(_options), scratchpad, viewType: this.editorId });
297
if (this.editorModelReference) {
298
// Re-entrant, double resolve happened. Dispose the addition references and proceed
299
// with the truth.
300
ref.dispose();
301
return (<IReference<IResolvedNotebookEditorModel>>this.editorModelReference).object;
302
}
303
this.editorModelReference = ref;
304
if (this.isDisposed()) {
305
this.editorModelReference.dispose();
306
this.editorModelReference = null;
307
return null;
308
}
309
this._register(this.editorModelReference.object.onDidChangeDirty(() => this._onDidChangeDirty.fire()));
310
this._register(this.editorModelReference.object.onDidChangeReadonly(() => this._onDidChangeCapabilities.fire()));
311
this._register(this.editorModelReference.object.onDidRevertUntitled(() => this.dispose()));
312
if (this.editorModelReference.object.isDirty()) {
313
this._onDidChangeDirty.fire();
314
}
315
} else {
316
this.editorModelReference.object.load({ limits: this.ensureLimits(_options) });
317
}
318
319
if (this.options._backupId) {
320
const info = await this._notebookService.withNotebookDataProvider(this.editorModelReference.object.notebook.viewType);
321
if (!(info instanceof SimpleNotebookProviderInfo)) {
322
throw new Error('CANNOT open file notebook with this provider');
323
}
324
325
const data = await info.serializer.dataToNotebook(VSBuffer.fromString(JSON.stringify({ __webview_backup: this.options._backupId })));
326
this.editorModelReference.object.notebook.applyEdits([
327
{
328
editType: CellEditType.Replace,
329
index: 0,
330
count: this.editorModelReference.object.notebook.length,
331
cells: data.cells
332
}
333
], true, undefined, () => undefined, undefined, false);
334
335
if (this.options._workingCopy) {
336
this.options._backupId = undefined;
337
this.options._workingCopy = undefined;
338
this.options.startDirty = undefined;
339
}
340
}
341
342
return this.editorModelReference.object;
343
}
344
345
override toUntyped(): IResourceEditorInput {
346
return {
347
resource: this.resource,
348
options: {
349
override: this.viewType
350
}
351
};
352
}
353
354
override matches(otherInput: EditorInput | IUntypedEditorInput): boolean {
355
if (super.matches(otherInput)) {
356
return true;
357
}
358
if (otherInput instanceof NotebookEditorInput) {
359
return this.viewType === otherInput.viewType && isEqual(this.resource, otherInput.resource);
360
}
361
if (isResourceEditorInput(otherInput) && otherInput.resource.scheme === CellUri.scheme) {
362
return isEqual(this.resource, CellUri.parse(otherInput.resource)?.notebook);
363
}
364
return false;
365
}
366
}
367
368
export interface ICompositeNotebookEditorInput {
369
readonly editorInputs: NotebookEditorInput[];
370
}
371
372
export function isCompositeNotebookEditorInput(thing: unknown): thing is ICompositeNotebookEditorInput {
373
return !!thing
374
&& typeof thing === 'object'
375
&& Array.isArray((<ICompositeNotebookEditorInput>thing).editorInputs)
376
&& ((<ICompositeNotebookEditorInput>thing).editorInputs.every(input => input instanceof NotebookEditorInput));
377
}
378
379
export function isNotebookEditorInput(thing: EditorInput | undefined): thing is NotebookEditorInput {
380
return !!thing
381
&& typeof thing === 'object'
382
&& thing.typeId === NotebookEditorInput.ID;
383
}
384
385