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