Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts
5250 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 { localize } from '../../../../../nls.js';
7
import { toAction } from '../../../../../base/common/actions.js';
8
import { createErrorWithActions } from '../../../../../base/common/errorMessage.js';
9
import { Emitter, Event } from '../../../../../base/common/event.js';
10
import * as glob from '../../../../../base/common/glob.js';
11
import { Iterable } from '../../../../../base/common/iterator.js';
12
import { Lazy } from '../../../../../base/common/lazy.js';
13
import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js';
14
import { ResourceMap } from '../../../../../base/common/map.js';
15
import { Schemas } from '../../../../../base/common/network.js';
16
import { basename, isEqual } from '../../../../../base/common/resources.js';
17
import { isDefined } from '../../../../../base/common/types.js';
18
import { URI } from '../../../../../base/common/uri.js';
19
import { IAccessibilityService } from '../../../../../platform/accessibility/common/accessibility.js';
20
import { ConfigurationTarget, IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
21
import { IResourceEditorInput } from '../../../../../platform/editor/common/editor.js';
22
import { IFileService } from '../../../../../platform/files/common/files.js';
23
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
24
import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js';
25
import { Memento } from '../../../../common/memento.js';
26
import { INotebookEditorContribution, notebookPreloadExtensionPoint, notebookRendererExtensionPoint, notebooksExtensionPoint } from '../notebookExtensionPoint.js';
27
import { INotebookEditorOptions } from '../notebookBrowser.js';
28
import { NotebookDiffEditorInput } from '../../common/notebookDiffEditorInput.js';
29
import { NotebookCellTextModel } from '../../common/model/notebookCellTextModel.js';
30
import { NotebookTextModel } from '../../common/model/notebookTextModel.js';
31
import { ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER, CellUri, NotebookSetting, INotebookContributionData, INotebookExclusiveDocumentFilter, INotebookRendererInfo, INotebookTextModel, IOrderedMimeType, IOutputDto, MimeTypeDisplayOrder, NotebookEditorPriority, NotebookRendererMatch, NOTEBOOK_DISPLAY_ORDER, RENDERER_EQUIVALENT_EXTENSIONS, RENDERER_NOT_AVAILABLE, NotebookExtensionDescription, INotebookStaticPreloadInfo, NotebookData } from '../../common/notebookCommon.js';
32
import { NotebookEditorInput } from '../../common/notebookEditorInput.js';
33
import { INotebookEditorModelResolverService } from '../../common/notebookEditorModelResolverService.js';
34
import { NotebookOutputRendererInfo, NotebookStaticPreloadInfo } from '../../common/notebookOutputRenderer.js';
35
import { NotebookEditorDescriptor, NotebookProviderInfo } from '../../common/notebookProvider.js';
36
import { INotebookSerializer, INotebookService, SimpleNotebookProviderInfo } from '../../common/notebookService.js';
37
import { DiffEditorInputFactoryFunction, EditorInputFactoryFunction, EditorInputFactoryObject, IEditorResolverService, IEditorType, RegisteredEditorInfo, RegisteredEditorPriority, UntitledEditorInputFactoryFunction, type MergeEditorInputFactoryFunction } from '../../../../services/editor/common/editorResolverService.js';
38
import { IExtensionService, isProposedApiEnabled } from '../../../../services/extensions/common/extensions.js';
39
import { IExtensionPointUser } from '../../../../services/extensions/common/extensionsRegistry.js';
40
import { InstallRecommendedExtensionAction } from '../../../extensions/browser/extensionsActions.js';
41
import { IUriIdentityService } from '../../../../../platform/uriIdentity/common/uriIdentity.js';
42
import { INotebookDocument, INotebookDocumentService } from '../../../../services/notebook/common/notebookDocumentService.js';
43
import { MergeEditorInput } from '../../../mergeEditor/browser/mergeEditorInput.js';
44
import type { EditorInputWithOptions, IResourceDiffEditorInput, IResourceMergeEditorInput } from '../../../../common/editor.js';
45
import { bufferToStream, streamToBuffer, VSBuffer, VSBufferReadableStream } from '../../../../../base/common/buffer.js';
46
import type { IEditorGroup } from '../../../../services/editor/common/editorGroupsService.js';
47
import { NotebookMultiDiffEditorInput } from '../diff/notebookMultiDiffEditorInput.js';
48
import { SnapshotContext } from '../../../../services/workingCopy/common/fileWorkingCopy.js';
49
import { CancellationToken } from '../../../../../base/common/cancellation.js';
50
import { CancellationError } from '../../../../../base/common/errors.js';
51
import { ICellRange } from '../../common/notebookRange.js';
52
53
interface NotebookProviderInfoStoreMemento {
54
editors: NotebookProviderInfo[];
55
}
56
57
export class NotebookProviderInfoStore extends Disposable {
58
59
private static readonly CUSTOM_EDITORS_STORAGE_ID = 'notebookEditors';
60
private static readonly CUSTOM_EDITORS_ENTRY_ID = 'editors';
61
62
private readonly _memento: Memento<NotebookProviderInfoStoreMemento>;
63
private _handled: boolean = false;
64
65
private readonly _contributedEditors = new Map<string, NotebookProviderInfo>();
66
private readonly _contributedEditorDisposables = this._register(new DisposableStore());
67
68
constructor(
69
@IStorageService storageService: IStorageService,
70
@IExtensionService extensionService: IExtensionService,
71
@IEditorResolverService private readonly _editorResolverService: IEditorResolverService,
72
@IConfigurationService private readonly _configurationService: IConfigurationService,
73
@IAccessibilityService private readonly _accessibilityService: IAccessibilityService,
74
@IInstantiationService private readonly _instantiationService: IInstantiationService,
75
@IFileService private readonly _fileService: IFileService,
76
@INotebookEditorModelResolverService private readonly _notebookEditorModelResolverService: INotebookEditorModelResolverService,
77
@IUriIdentityService private readonly uriIdentService: IUriIdentityService,
78
) {
79
super();
80
81
this._memento = new Memento(NotebookProviderInfoStore.CUSTOM_EDITORS_STORAGE_ID, storageService);
82
83
const mementoObject = this._memento.getMemento(StorageScope.PROFILE, StorageTarget.MACHINE);
84
// Process the notebook contributions but buffer changes from the resolver
85
this._editorResolverService.bufferChangeEvents(() => {
86
for (const info of (mementoObject[NotebookProviderInfoStore.CUSTOM_EDITORS_ENTRY_ID] || []) as NotebookEditorDescriptor[]) {
87
this.add(new NotebookProviderInfo(info), false);
88
}
89
});
90
91
this._register(extensionService.onDidRegisterExtensions(() => {
92
if (!this._handled) {
93
// there is no extension point registered for notebook content provider
94
// clear the memento and cache
95
this._clear();
96
mementoObject[NotebookProviderInfoStore.CUSTOM_EDITORS_ENTRY_ID] = [];
97
this._memento.saveMemento();
98
}
99
}));
100
101
notebooksExtensionPoint.setHandler(extensions => this._setupHandler(extensions));
102
}
103
104
override dispose(): void {
105
this._clear();
106
super.dispose();
107
}
108
109
private _setupHandler(extensions: readonly IExtensionPointUser<INotebookEditorContribution[]>[]) {
110
this._handled = true;
111
const builtins: NotebookProviderInfo[] = [...this._contributedEditors.values()].filter(info => !info.extension);
112
this._clear();
113
114
const builtinProvidersFromCache: Map<string, IDisposable> = new Map();
115
builtins.forEach(builtin => {
116
builtinProvidersFromCache.set(builtin.id, this.add(builtin));
117
});
118
119
for (const extension of extensions) {
120
for (const notebookContribution of extension.value) {
121
122
if (!notebookContribution.type) {
123
extension.collector.error(`Notebook does not specify type-property`);
124
continue;
125
}
126
127
const existing = this.get(notebookContribution.type);
128
129
if (existing) {
130
if (!existing.extension && extension.description.isBuiltin && builtins.find(builtin => builtin.id === notebookContribution.type)) {
131
// we are registering an extension which is using the same view type which is already cached
132
builtinProvidersFromCache.get(notebookContribution.type)?.dispose();
133
} else {
134
extension.collector.error(`Notebook type '${notebookContribution.type}' already used`);
135
continue;
136
}
137
}
138
139
this.add(new NotebookProviderInfo({
140
extension: extension.description.identifier,
141
id: notebookContribution.type,
142
displayName: notebookContribution.displayName,
143
selectors: notebookContribution.selector || [],
144
priority: this._convertPriority(notebookContribution.priority),
145
providerDisplayName: extension.description.displayName ?? extension.description.identifier.value,
146
}));
147
}
148
}
149
150
const mementoObject = this._memento.getMemento(StorageScope.PROFILE, StorageTarget.MACHINE);
151
mementoObject[NotebookProviderInfoStore.CUSTOM_EDITORS_ENTRY_ID] = Array.from(this._contributedEditors.values());
152
this._memento.saveMemento();
153
}
154
155
clearEditorCache() {
156
const mementoObject = this._memento.getMemento(StorageScope.PROFILE, StorageTarget.MACHINE);
157
mementoObject[NotebookProviderInfoStore.CUSTOM_EDITORS_ENTRY_ID] = [];
158
this._memento.saveMemento();
159
}
160
161
private _convertPriority(priority?: string) {
162
if (!priority) {
163
return RegisteredEditorPriority.default;
164
}
165
166
if (priority === NotebookEditorPriority.default) {
167
return RegisteredEditorPriority.default;
168
}
169
170
return RegisteredEditorPriority.option;
171
172
}
173
174
private _registerContributionPoint(notebookProviderInfo: NotebookProviderInfo): IDisposable {
175
176
const disposables = new DisposableStore();
177
178
for (const selector of notebookProviderInfo.selectors) {
179
const globPattern = (selector as INotebookExclusiveDocumentFilter).include || selector as glob.IRelativePattern | string;
180
const notebookEditorInfo: RegisteredEditorInfo = {
181
id: notebookProviderInfo.id,
182
label: notebookProviderInfo.displayName,
183
detail: notebookProviderInfo.providerDisplayName,
184
priority: notebookProviderInfo.priority,
185
};
186
const notebookEditorOptions = {
187
canHandleDiff: () => !!this._configurationService.getValue(NotebookSetting.textDiffEditorPreview) && !this._accessibilityService.isScreenReaderOptimized(),
188
canSupportResource: (resource: URI) => {
189
if (resource.scheme === Schemas.vscodeNotebookCellOutput) {
190
const params = new URLSearchParams(resource.query);
191
return params.get('openIn') === 'notebook';
192
}
193
return resource.scheme === Schemas.untitled || resource.scheme === Schemas.vscodeNotebookCell || this._fileService.hasProvider(resource);
194
}
195
};
196
const notebookEditorInputFactory: EditorInputFactoryFunction = async ({ resource, options }) => {
197
let data;
198
if (resource.scheme === Schemas.vscodeNotebookCellOutput) {
199
const outputUriData = CellUri.parseCellOutputUri(resource);
200
if (!outputUriData || !outputUriData.notebook || outputUriData.cellHandle === undefined) {
201
throw new Error('Invalid cell output uri');
202
}
203
204
data = {
205
notebook: outputUriData.notebook,
206
handle: outputUriData.cellHandle
207
};
208
209
} else {
210
data = CellUri.parse(resource);
211
}
212
213
let notebookUri: URI;
214
215
let cellOptions: IResourceEditorInput | undefined;
216
217
if (data) {
218
// resource is a notebook cell
219
notebookUri = this.uriIdentService.asCanonicalUri(data.notebook);
220
cellOptions = { resource, options };
221
} else {
222
notebookUri = this.uriIdentService.asCanonicalUri(resource);
223
}
224
225
if (!cellOptions) {
226
cellOptions = (options as INotebookEditorOptions | undefined)?.cellOptions;
227
}
228
229
let notebookOptions: INotebookEditorOptions;
230
231
if (resource.scheme === Schemas.vscodeNotebookCellOutput) {
232
if (data?.handle === undefined || !data?.notebook) {
233
throw new Error('Invalid cell handle');
234
}
235
236
const cellUri = CellUri.generate(data.notebook, data.handle);
237
238
cellOptions = { resource: cellUri, options };
239
240
const cellIndex = await this._notebookEditorModelResolverService.resolve(notebookUri)
241
.then(model => model.object.notebook.cells.findIndex(cell => cell.handle === data?.handle))
242
.then(index => index >= 0 ? index : 0);
243
244
const cellIndexesToRanges: ICellRange[] = [{ start: cellIndex, end: cellIndex + 1 }];
245
246
notebookOptions = {
247
...options,
248
cellOptions,
249
viewState: undefined,
250
cellSelections: cellIndexesToRanges
251
};
252
} else {
253
notebookOptions = {
254
...options,
255
cellOptions,
256
viewState: undefined,
257
};
258
}
259
const preferredResourceParam = cellOptions?.resource;
260
const editor = NotebookEditorInput.getOrCreate(this._instantiationService, notebookUri, preferredResourceParam, notebookProviderInfo.id);
261
return { editor, options: notebookOptions };
262
};
263
264
const notebookUntitledEditorFactory: UntitledEditorInputFactoryFunction = async ({ resource, options }) => {
265
const ref = await this._notebookEditorModelResolverService.resolve({ untitledResource: resource }, notebookProviderInfo.id);
266
267
// untitled notebooks are disposed when they get saved. we should not hold a reference
268
// to such a disposed notebook and therefore dispose the reference as well
269
Event.once(ref.object.notebook.onWillDispose)(() => {
270
ref.dispose();
271
});
272
273
return { editor: NotebookEditorInput.getOrCreate(this._instantiationService, ref.object.resource, undefined, notebookProviderInfo.id), options };
274
};
275
const notebookDiffEditorInputFactory: DiffEditorInputFactoryFunction = (diffEditorInput: IResourceDiffEditorInput, group: IEditorGroup) => {
276
const { modified, original, label, description } = diffEditorInput;
277
278
if (this._configurationService.getValue('notebook.experimental.enableNewDiffEditor')) {
279
return { editor: NotebookMultiDiffEditorInput.create(this._instantiationService, modified.resource!, label, description, original.resource!, notebookProviderInfo.id) };
280
}
281
return { editor: NotebookDiffEditorInput.create(this._instantiationService, modified.resource!, label, description, original.resource!, notebookProviderInfo.id) };
282
};
283
const mergeEditorInputFactory: MergeEditorInputFactoryFunction = (mergeEditor: IResourceMergeEditorInput): EditorInputWithOptions => {
284
return {
285
editor: this._instantiationService.createInstance(
286
MergeEditorInput,
287
mergeEditor.base.resource,
288
{
289
uri: mergeEditor.input1.resource,
290
title: mergeEditor.input1.label ?? basename(mergeEditor.input1.resource),
291
description: mergeEditor.input1.description ?? '',
292
detail: mergeEditor.input1.detail
293
},
294
{
295
uri: mergeEditor.input2.resource,
296
title: mergeEditor.input2.label ?? basename(mergeEditor.input2.resource),
297
description: mergeEditor.input2.description ?? '',
298
detail: mergeEditor.input2.detail
299
},
300
mergeEditor.result.resource
301
)
302
};
303
};
304
305
const notebookFactoryObject: EditorInputFactoryObject = {
306
createEditorInput: notebookEditorInputFactory,
307
createDiffEditorInput: notebookDiffEditorInputFactory,
308
createUntitledEditorInput: notebookUntitledEditorFactory,
309
createMergeEditorInput: mergeEditorInputFactory
310
};
311
const notebookCellFactoryObject: EditorInputFactoryObject = {
312
createEditorInput: notebookEditorInputFactory,
313
createDiffEditorInput: notebookDiffEditorInputFactory,
314
};
315
316
// TODO @lramos15 find a better way to toggle handling diff editors than needing these listeners for every registration
317
// This is a lot of event listeners especially if there are many notebooks
318
disposables.add(this._configurationService.onDidChangeConfiguration(e => {
319
if (e.affectsConfiguration(NotebookSetting.textDiffEditorPreview)) {
320
const canHandleDiff = !!this._configurationService.getValue(NotebookSetting.textDiffEditorPreview) && !this._accessibilityService.isScreenReaderOptimized();
321
if (canHandleDiff) {
322
notebookFactoryObject.createDiffEditorInput = notebookDiffEditorInputFactory;
323
notebookCellFactoryObject.createDiffEditorInput = notebookDiffEditorInputFactory;
324
} else {
325
notebookFactoryObject.createDiffEditorInput = undefined;
326
notebookCellFactoryObject.createDiffEditorInput = undefined;
327
}
328
}
329
}));
330
331
disposables.add(this._accessibilityService.onDidChangeScreenReaderOptimized(() => {
332
const canHandleDiff = !!this._configurationService.getValue(NotebookSetting.textDiffEditorPreview) && !this._accessibilityService.isScreenReaderOptimized();
333
if (canHandleDiff) {
334
notebookFactoryObject.createDiffEditorInput = notebookDiffEditorInputFactory;
335
notebookCellFactoryObject.createDiffEditorInput = notebookDiffEditorInputFactory;
336
} else {
337
notebookFactoryObject.createDiffEditorInput = undefined;
338
notebookCellFactoryObject.createDiffEditorInput = undefined;
339
}
340
}));
341
342
// Register the notebook editor
343
disposables.add(this._editorResolverService.registerEditor(
344
globPattern,
345
notebookEditorInfo,
346
notebookEditorOptions,
347
notebookFactoryObject,
348
));
349
// Then register the schema handler as exclusive for that notebook
350
disposables.add(this._editorResolverService.registerEditor(
351
`${Schemas.vscodeNotebookCell}:/**/${globPattern}`,
352
{ ...notebookEditorInfo, priority: RegisteredEditorPriority.exclusive },
353
notebookEditorOptions,
354
notebookCellFactoryObject
355
));
356
}
357
358
return disposables;
359
}
360
361
362
private _clear(): void {
363
this._contributedEditors.clear();
364
this._contributedEditorDisposables.clear();
365
}
366
367
get(viewType: string): NotebookProviderInfo | undefined {
368
return this._contributedEditors.get(viewType);
369
}
370
371
add(info: NotebookProviderInfo, saveMemento = true): IDisposable {
372
if (this._contributedEditors.has(info.id)) {
373
throw new Error(`notebook type '${info.id}' ALREADY EXISTS`);
374
}
375
this._contributedEditors.set(info.id, info);
376
let editorRegistration: IDisposable | undefined;
377
378
// built-in notebook providers contribute their own editors
379
if (info.extension) {
380
editorRegistration = this._registerContributionPoint(info);
381
this._contributedEditorDisposables.add(editorRegistration);
382
}
383
384
if (saveMemento) {
385
const mementoObject = this._memento.getMemento(StorageScope.PROFILE, StorageTarget.MACHINE);
386
mementoObject[NotebookProviderInfoStore.CUSTOM_EDITORS_ENTRY_ID] = Array.from(this._contributedEditors.values());
387
this._memento.saveMemento();
388
}
389
390
return this._register(toDisposable(() => {
391
const mementoObject = this._memento.getMemento(StorageScope.PROFILE, StorageTarget.MACHINE);
392
mementoObject[NotebookProviderInfoStore.CUSTOM_EDITORS_ENTRY_ID] = Array.from(this._contributedEditors.values());
393
this._memento.saveMemento();
394
editorRegistration?.dispose();
395
this._contributedEditors.delete(info.id);
396
}));
397
}
398
399
getContributedNotebook(resource: URI): readonly NotebookProviderInfo[] {
400
const result: NotebookProviderInfo[] = [];
401
for (const info of this._contributedEditors.values()) {
402
if (info.matches(resource)) {
403
result.push(info);
404
}
405
}
406
if (result.length === 0 && resource.scheme === Schemas.untitled) {
407
// untitled resource and no path-specific match => all providers apply
408
return Array.from(this._contributedEditors.values());
409
}
410
return result;
411
}
412
413
[Symbol.iterator](): Iterator<NotebookProviderInfo> {
414
return this._contributedEditors.values();
415
}
416
}
417
418
interface NotebookOutputRendererInfoStoreMemento {
419
[notebookType: string]: { [mimeType: string]: string } | undefined;
420
}
421
422
export class NotebookOutputRendererInfoStore {
423
private readonly contributedRenderers = new Map</* rendererId */ string, NotebookOutputRendererInfo>();
424
private readonly preferredMimetypeMemento: Memento<NotebookOutputRendererInfoStoreMemento>;
425
private readonly preferredMimetype = new Lazy<NotebookOutputRendererInfoStoreMemento>(
426
() => this.preferredMimetypeMemento.getMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE));
427
428
constructor(
429
@IStorageService storageService: IStorageService,
430
) {
431
this.preferredMimetypeMemento = new Memento('workbench.editor.notebook.preferredRenderer2', storageService);
432
}
433
434
clear() {
435
this.contributedRenderers.clear();
436
}
437
438
get(rendererId: string): NotebookOutputRendererInfo | undefined {
439
return this.contributedRenderers.get(rendererId);
440
}
441
442
getAll(): NotebookOutputRendererInfo[] {
443
return Array.from(this.contributedRenderers.values());
444
}
445
446
add(info: NotebookOutputRendererInfo): void {
447
if (this.contributedRenderers.has(info.id)) {
448
return;
449
}
450
this.contributedRenderers.set(info.id, info);
451
}
452
453
/** Update and remember the preferred renderer for the given mimetype in this workspace */
454
setPreferred(notebookProviderInfo: NotebookProviderInfo, mimeType: string, rendererId: string) {
455
const mementoObj = this.preferredMimetype.value;
456
const forNotebook = mementoObj[notebookProviderInfo.id];
457
if (forNotebook) {
458
forNotebook[mimeType] = rendererId;
459
} else {
460
mementoObj[notebookProviderInfo.id] = { [mimeType]: rendererId };
461
}
462
463
this.preferredMimetypeMemento.saveMemento();
464
}
465
466
findBestRenderers(notebookProviderInfo: NotebookProviderInfo | undefined, mimeType: string, kernelProvides: readonly string[] | undefined): IOrderedMimeType[] {
467
468
const enum ReuseOrder {
469
PreviouslySelected = 1 << 8,
470
SameExtensionAsNotebook = 2 << 8,
471
OtherRenderer = 3 << 8,
472
BuiltIn = 4 << 8,
473
}
474
475
const preferred = notebookProviderInfo && this.preferredMimetype.value[notebookProviderInfo.id]?.[mimeType];
476
const notebookExtId = notebookProviderInfo?.extension?.value;
477
const notebookId = notebookProviderInfo?.id;
478
const renderers: { ordered: IOrderedMimeType; score: number }[] = Array.from(this.contributedRenderers.values())
479
.map(renderer => {
480
const ownScore = kernelProvides === undefined
481
? renderer.matchesWithoutKernel(mimeType)
482
: renderer.matches(mimeType, kernelProvides);
483
484
if (ownScore === NotebookRendererMatch.Never) {
485
return undefined;
486
}
487
488
const rendererExtId = renderer.extensionId.value;
489
const reuseScore = preferred === renderer.id
490
? ReuseOrder.PreviouslySelected
491
: rendererExtId === notebookExtId || RENDERER_EQUIVALENT_EXTENSIONS.get(rendererExtId)?.has(notebookId!)
492
? ReuseOrder.SameExtensionAsNotebook
493
: renderer.isBuiltin ? ReuseOrder.BuiltIn : ReuseOrder.OtherRenderer;
494
return {
495
ordered: { mimeType, rendererId: renderer.id, isTrusted: true },
496
score: reuseScore | ownScore,
497
};
498
}).filter(isDefined);
499
500
if (renderers.length === 0) {
501
return [{ mimeType, rendererId: RENDERER_NOT_AVAILABLE, isTrusted: true }];
502
}
503
504
return renderers.sort((a, b) => a.score - b.score).map(r => r.ordered);
505
}
506
}
507
508
class ModelData implements IDisposable, INotebookDocument {
509
private readonly _modelEventListeners = new DisposableStore();
510
get uri() { return this.model.uri; }
511
512
constructor(
513
readonly model: NotebookTextModel,
514
onWillDispose: (model: INotebookTextModel) => void
515
) {
516
this._modelEventListeners.add(model.onWillDispose(() => onWillDispose(model)));
517
}
518
519
getCellIndex(cellUri: URI): number | undefined {
520
return this.model.cells.findIndex(cell => isEqual(cell.uri, cellUri));
521
}
522
523
dispose(): void {
524
this._modelEventListeners.dispose();
525
}
526
}
527
528
interface NotebookServiceMemento {
529
[viewType: string]: string | undefined;
530
}
531
532
export class NotebookService extends Disposable implements INotebookService {
533
534
declare readonly _serviceBrand: undefined;
535
private static _storageNotebookViewTypeProvider = 'notebook.viewTypeProvider';
536
private readonly _memento: Memento<NotebookServiceMemento>;
537
private readonly _viewTypeCache: NotebookServiceMemento;
538
539
private readonly _notebookProviders;
540
private _notebookProviderInfoStore: NotebookProviderInfoStore | undefined;
541
private get notebookProviderInfoStore(): NotebookProviderInfoStore {
542
if (!this._notebookProviderInfoStore) {
543
this._notebookProviderInfoStore = this._register(this._instantiationService.createInstance(NotebookProviderInfoStore));
544
}
545
546
return this._notebookProviderInfoStore;
547
}
548
private readonly _notebookRenderersInfoStore;
549
private readonly _onDidChangeOutputRenderers;
550
readonly onDidChangeOutputRenderers;
551
552
private readonly _notebookStaticPreloadInfoStore;
553
554
private readonly _models;
555
556
private readonly _onWillAddNotebookDocument;
557
private readonly _onDidAddNotebookDocument;
558
private readonly _onWillRemoveNotebookDocument;
559
private readonly _onDidRemoveNotebookDocument;
560
561
readonly onWillAddNotebookDocument;
562
readonly onDidAddNotebookDocument;
563
readonly onDidRemoveNotebookDocument;
564
readonly onWillRemoveNotebookDocument;
565
566
private readonly _onAddViewType;
567
readonly onAddViewType;
568
569
private readonly _onWillRemoveViewType;
570
readonly onWillRemoveViewType;
571
572
private readonly _onDidChangeEditorTypes;
573
readonly onDidChangeEditorTypes: Event<void>;
574
575
private _cutItems: NotebookCellTextModel[] | undefined;
576
private _lastClipboardIsCopy: boolean;
577
578
private _displayOrder!: MimeTypeDisplayOrder;
579
580
constructor(
581
@IExtensionService private readonly _extensionService: IExtensionService,
582
@IConfigurationService private readonly _configurationService: IConfigurationService,
583
@IAccessibilityService private readonly _accessibilityService: IAccessibilityService,
584
@IInstantiationService private readonly _instantiationService: IInstantiationService,
585
@IStorageService private readonly _storageService: IStorageService,
586
@INotebookDocumentService private readonly _notebookDocumentService: INotebookDocumentService
587
) {
588
super();
589
this._notebookProviders = new Map<string, SimpleNotebookProviderInfo>();
590
this._notebookProviderInfoStore = undefined;
591
this._notebookRenderersInfoStore = this._instantiationService.createInstance(NotebookOutputRendererInfoStore);
592
this._onDidChangeOutputRenderers = this._register(new Emitter<void>());
593
this.onDidChangeOutputRenderers = this._onDidChangeOutputRenderers.event;
594
this._notebookStaticPreloadInfoStore = new Set<NotebookStaticPreloadInfo>();
595
this._models = new ResourceMap<ModelData>();
596
this._onWillAddNotebookDocument = this._register(new Emitter<NotebookTextModel>());
597
this._onDidAddNotebookDocument = this._register(new Emitter<NotebookTextModel>());
598
this._onWillRemoveNotebookDocument = this._register(new Emitter<NotebookTextModel>());
599
this._onDidRemoveNotebookDocument = this._register(new Emitter<NotebookTextModel>());
600
this.onWillAddNotebookDocument = this._onWillAddNotebookDocument.event;
601
this.onDidAddNotebookDocument = this._onDidAddNotebookDocument.event;
602
this.onDidRemoveNotebookDocument = this._onDidRemoveNotebookDocument.event;
603
this.onWillRemoveNotebookDocument = this._onWillRemoveNotebookDocument.event;
604
this._onAddViewType = this._register(new Emitter<string>());
605
this.onAddViewType = this._onAddViewType.event;
606
this._onWillRemoveViewType = this._register(new Emitter<string>());
607
this.onWillRemoveViewType = this._onWillRemoveViewType.event;
608
this._onDidChangeEditorTypes = this._register(new Emitter<void>());
609
this.onDidChangeEditorTypes = this._onDidChangeEditorTypes.event;
610
this._lastClipboardIsCopy = true;
611
612
notebookRendererExtensionPoint.setHandler((renderers) => {
613
this._notebookRenderersInfoStore.clear();
614
615
for (const extension of renderers) {
616
for (const notebookContribution of extension.value) {
617
if (!notebookContribution.entrypoint) { // avoid crashing
618
extension.collector.error(`Notebook renderer does not specify entry point`);
619
continue;
620
}
621
622
const id = notebookContribution.id;
623
if (!id) {
624
extension.collector.error(`Notebook renderer does not specify id-property`);
625
continue;
626
}
627
628
this._notebookRenderersInfoStore.add(new NotebookOutputRendererInfo({
629
id,
630
extension: extension.description,
631
entrypoint: notebookContribution.entrypoint,
632
displayName: notebookContribution.displayName,
633
mimeTypes: notebookContribution.mimeTypes || [],
634
dependencies: notebookContribution.dependencies,
635
optionalDependencies: notebookContribution.optionalDependencies,
636
requiresMessaging: notebookContribution.requiresMessaging,
637
}));
638
}
639
}
640
641
this._onDidChangeOutputRenderers.fire();
642
});
643
644
notebookPreloadExtensionPoint.setHandler(extensions => {
645
this._notebookStaticPreloadInfoStore.clear();
646
647
for (const extension of extensions) {
648
if (!isProposedApiEnabled(extension.description, 'contribNotebookStaticPreloads')) {
649
continue;
650
}
651
652
for (const notebookContribution of extension.value) {
653
if (!notebookContribution.entrypoint) { // avoid crashing
654
extension.collector.error(`Notebook preload does not specify entry point`);
655
continue;
656
}
657
658
const type = notebookContribution.type;
659
if (!type) {
660
extension.collector.error(`Notebook preload does not specify type-property`);
661
continue;
662
}
663
664
this._notebookStaticPreloadInfoStore.add(new NotebookStaticPreloadInfo({
665
type,
666
extension: extension.description,
667
entrypoint: notebookContribution.entrypoint,
668
localResourceRoots: notebookContribution.localResourceRoots ?? [],
669
}));
670
}
671
}
672
});
673
674
const updateOrder = () => {
675
this._displayOrder = new MimeTypeDisplayOrder(
676
this._configurationService.getValue<string[]>(NotebookSetting.displayOrder) || [],
677
this._accessibilityService.isScreenReaderOptimized()
678
? ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER
679
: NOTEBOOK_DISPLAY_ORDER,
680
);
681
};
682
683
updateOrder();
684
685
this._register(this._configurationService.onDidChangeConfiguration(e => {
686
if (e.affectsConfiguration(NotebookSetting.displayOrder)) {
687
updateOrder();
688
}
689
}));
690
691
this._register(this._accessibilityService.onDidChangeScreenReaderOptimized(() => {
692
updateOrder();
693
}));
694
695
this._memento = new Memento(NotebookService._storageNotebookViewTypeProvider, this._storageService);
696
this._viewTypeCache = this._memento.getMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE);
697
}
698
699
700
getEditorTypes(): IEditorType[] {
701
return [...this.notebookProviderInfoStore].map(info => ({
702
id: info.id,
703
displayName: info.displayName,
704
providerDisplayName: info.providerDisplayName
705
}));
706
}
707
708
clearEditorCache(): void {
709
this.notebookProviderInfoStore.clearEditorCache();
710
}
711
712
private _postDocumentOpenActivation(viewType: string) {
713
// send out activations on notebook text model creation
714
this._extensionService.activateByEvent(`onNotebook:${viewType}`);
715
this._extensionService.activateByEvent(`onNotebook:*`);
716
}
717
718
async canResolve(viewType: string): Promise<boolean> {
719
if (this._notebookProviders.has(viewType)) {
720
return true;
721
}
722
723
await this._extensionService.whenInstalledExtensionsRegistered();
724
await this._extensionService.activateByEvent(`onNotebookSerializer:${viewType}`);
725
726
return this._notebookProviders.has(viewType);
727
}
728
729
registerContributedNotebookType(viewType: string, data: INotebookContributionData): IDisposable {
730
731
const info = new NotebookProviderInfo({
732
extension: data.extension,
733
id: viewType,
734
displayName: data.displayName,
735
providerDisplayName: data.providerDisplayName,
736
priority: data.priority || RegisteredEditorPriority.default,
737
selectors: []
738
});
739
740
info.update({ selectors: data.filenamePattern });
741
742
const reg = this.notebookProviderInfoStore.add(info);
743
this._onDidChangeEditorTypes.fire();
744
745
return toDisposable(() => {
746
reg.dispose();
747
this._onDidChangeEditorTypes.fire();
748
});
749
}
750
751
private _registerProviderData(viewType: string, data: SimpleNotebookProviderInfo): IDisposable {
752
if (this._notebookProviders.has(viewType)) {
753
throw new Error(`notebook provider for viewtype '${viewType}' already exists`);
754
}
755
this._notebookProviders.set(viewType, data);
756
this._onAddViewType.fire(viewType);
757
return toDisposable(() => {
758
this._onWillRemoveViewType.fire(viewType);
759
this._notebookProviders.delete(viewType);
760
});
761
}
762
763
registerNotebookSerializer(viewType: string, extensionData: NotebookExtensionDescription, serializer: INotebookSerializer): IDisposable {
764
this.notebookProviderInfoStore.get(viewType)?.update({ options: serializer.options });
765
this._viewTypeCache[viewType] = extensionData.id.value;
766
this._persistMementos();
767
return this._registerProviderData(viewType, new SimpleNotebookProviderInfo(viewType, serializer, extensionData));
768
}
769
770
async withNotebookDataProvider(viewType: string): Promise<SimpleNotebookProviderInfo> {
771
const selected = this.notebookProviderInfoStore.get(viewType);
772
if (!selected) {
773
const knownProvider = this.getViewTypeProvider(viewType);
774
775
const actions = knownProvider ? [
776
toAction({
777
id: 'workbench.notebook.action.installMissingViewType', label: localize('notebookOpenInstallMissingViewType', "Install extension for '{0}'", viewType), run: async () => {
778
await this._instantiationService.createInstance(InstallRecommendedExtensionAction, knownProvider).run();
779
}
780
})
781
] : [];
782
783
throw createErrorWithActions(`UNKNOWN notebook type '${viewType}'`, actions);
784
}
785
await this.canResolve(selected.id);
786
const result = this._notebookProviders.get(selected.id);
787
if (!result) {
788
throw new Error(`NO provider registered for view type: '${selected.id}'`);
789
}
790
return result;
791
}
792
793
tryGetDataProviderSync(viewType: string): SimpleNotebookProviderInfo | undefined {
794
const selected = this.notebookProviderInfoStore.get(viewType);
795
if (!selected) {
796
return undefined;
797
}
798
return this._notebookProviders.get(selected.id);
799
}
800
801
802
private _persistMementos(): void {
803
this._memento.saveMemento();
804
}
805
806
getViewTypeProvider(viewType: string): string | undefined {
807
return this._viewTypeCache[viewType];
808
}
809
810
getRendererInfo(rendererId: string): INotebookRendererInfo | undefined {
811
return this._notebookRenderersInfoStore.get(rendererId);
812
}
813
814
updateMimePreferredRenderer(viewType: string, mimeType: string, rendererId: string, otherMimetypes: readonly string[]): void {
815
const info = this.notebookProviderInfoStore.get(viewType);
816
if (info) {
817
this._notebookRenderersInfoStore.setPreferred(info, mimeType, rendererId);
818
}
819
820
this._displayOrder.prioritize(mimeType, otherMimetypes);
821
}
822
823
saveMimeDisplayOrder(target: ConfigurationTarget) {
824
this._configurationService.updateValue(NotebookSetting.displayOrder, this._displayOrder.toArray(), target);
825
}
826
827
getRenderers(): INotebookRendererInfo[] {
828
return this._notebookRenderersInfoStore.getAll();
829
}
830
831
*getStaticPreloads(viewType: string): Iterable<INotebookStaticPreloadInfo> {
832
for (const preload of this._notebookStaticPreloadInfoStore) {
833
if (preload.type === viewType) {
834
yield preload;
835
}
836
}
837
}
838
839
// --- notebook documents: create, destory, retrieve, enumerate
840
841
async createNotebookTextModel(viewType: string, uri: URI, stream?: VSBufferReadableStream): Promise<NotebookTextModel> {
842
if (this._models.has(uri)) {
843
throw new Error(`notebook for ${uri} already exists`);
844
}
845
846
const info = await this.withNotebookDataProvider(viewType);
847
if (!(info instanceof SimpleNotebookProviderInfo)) {
848
throw new Error('CANNOT open file notebook with this provider');
849
}
850
851
852
const bytes = stream ? await streamToBuffer(stream) : VSBuffer.fromByteArray([]);
853
const data = await info.serializer.dataToNotebook(bytes);
854
855
856
const notebookModel = this._instantiationService.createInstance(NotebookTextModel, info.viewType, uri, data.cells, data.metadata, info.serializer.options);
857
const modelData = new ModelData(notebookModel, this._onWillDisposeDocument.bind(this));
858
this._models.set(uri, modelData);
859
this._notebookDocumentService.addNotebookDocument(modelData);
860
this._onWillAddNotebookDocument.fire(notebookModel);
861
this._onDidAddNotebookDocument.fire(notebookModel);
862
this._postDocumentOpenActivation(info.viewType);
863
return notebookModel;
864
}
865
866
async createNotebookTextDocumentSnapshot(uri: URI, context: SnapshotContext, token: CancellationToken): Promise<VSBufferReadableStream> {
867
const model = this.getNotebookTextModel(uri);
868
869
if (!model) {
870
throw new Error(`notebook for ${uri} doesn't exist`);
871
}
872
873
const info = await this.withNotebookDataProvider(model.viewType);
874
875
if (!(info instanceof SimpleNotebookProviderInfo)) {
876
throw new Error('CANNOT open file notebook with this provider');
877
}
878
879
const serializer = info.serializer;
880
const outputSizeLimit = this._configurationService.getValue<number>(NotebookSetting.outputBackupSizeLimit) * 1024;
881
const data: NotebookData = model.createSnapshot({ context: context, outputSizeLimit: outputSizeLimit, transientOptions: serializer.options });
882
const indentAmount = model.metadata.indentAmount;
883
if (typeof indentAmount === 'string' && indentAmount) {
884
// This is required for ipynb serializer to preserve the whitespace in the notebook.
885
data.metadata.indentAmount = indentAmount;
886
}
887
const bytes = await serializer.notebookToData(data);
888
889
if (token.isCancellationRequested) {
890
throw new CancellationError();
891
}
892
return bufferToStream(bytes);
893
}
894
895
async restoreNotebookTextModelFromSnapshot(uri: URI, viewType: string, snapshot: VSBufferReadableStream): Promise<NotebookTextModel> {
896
const model = this.getNotebookTextModel(uri);
897
898
if (!model) {
899
throw new Error(`notebook for ${uri} doesn't exist`);
900
}
901
902
const info = await this.withNotebookDataProvider(model.viewType);
903
904
if (!(info instanceof SimpleNotebookProviderInfo)) {
905
throw new Error('CANNOT open file notebook with this provider');
906
}
907
908
const serializer = info.serializer;
909
910
const bytes = await streamToBuffer(snapshot);
911
const data = await info.serializer.dataToNotebook(bytes);
912
model.restoreSnapshot(data, serializer.options);
913
914
return model;
915
}
916
917
getNotebookTextModel(uri: URI): NotebookTextModel | undefined {
918
return this._models.get(uri)?.model;
919
}
920
921
getNotebookTextModels(): Iterable<NotebookTextModel> {
922
return Iterable.map(this._models.values(), data => data.model);
923
}
924
925
listNotebookDocuments(): NotebookTextModel[] {
926
return [...this._models].map(e => e[1].model);
927
}
928
929
private _onWillDisposeDocument(model: INotebookTextModel): void {
930
const modelData = this._models.get(model.uri);
931
if (modelData) {
932
this._onWillRemoveNotebookDocument.fire(modelData.model);
933
this._models.delete(model.uri);
934
this._notebookDocumentService.removeNotebookDocument(modelData);
935
modelData.dispose();
936
this._onDidRemoveNotebookDocument.fire(modelData.model);
937
}
938
}
939
940
getOutputMimeTypeInfo(textModel: NotebookTextModel, kernelProvides: readonly string[] | undefined, output: IOutputDto): readonly IOrderedMimeType[] {
941
const sorted = this._displayOrder.sort(new Set<string>(output.outputs.map(op => op.mime)));
942
const notebookProviderInfo = this.notebookProviderInfoStore.get(textModel.viewType);
943
944
return sorted
945
.flatMap(mimeType => this._notebookRenderersInfoStore.findBestRenderers(notebookProviderInfo, mimeType, kernelProvides))
946
.sort((a, b) => (a.rendererId === RENDERER_NOT_AVAILABLE ? 1 : 0) - (b.rendererId === RENDERER_NOT_AVAILABLE ? 1 : 0));
947
}
948
949
getContributedNotebookTypes(resource?: URI): readonly NotebookProviderInfo[] {
950
if (resource) {
951
return this.notebookProviderInfoStore.getContributedNotebook(resource);
952
}
953
954
return [...this.notebookProviderInfoStore];
955
}
956
957
hasSupportedNotebooks(resource: URI): boolean {
958
if (this._models.has(resource)) {
959
// it might be untitled
960
return true;
961
}
962
963
const contribution = this.notebookProviderInfoStore.getContributedNotebook(resource);
964
if (!contribution.length) {
965
return false;
966
}
967
return contribution.some(info => info.matches(resource) &&
968
(info.priority === RegisteredEditorPriority.default || info.priority === RegisteredEditorPriority.exclusive)
969
);
970
}
971
972
getContributedNotebookType(viewType: string): NotebookProviderInfo | undefined {
973
return this.notebookProviderInfoStore.get(viewType);
974
}
975
976
getNotebookProviderResourceRoots(): URI[] {
977
const ret: URI[] = [];
978
this._notebookProviders.forEach(val => {
979
if (val.extensionData.location) {
980
ret.push(URI.revive(val.extensionData.location));
981
}
982
});
983
984
return ret;
985
}
986
987
// --- copy & paste
988
989
setToCopy(items: NotebookCellTextModel[], isCopy: boolean) {
990
this._cutItems = items;
991
this._lastClipboardIsCopy = isCopy;
992
}
993
994
getToCopy(): { items: NotebookCellTextModel[]; isCopy: boolean } | undefined {
995
if (this._cutItems) {
996
return { items: this._cutItems, isCopy: this._lastClipboardIsCopy };
997
}
998
999
return undefined;
1000
}
1001
1002
}
1003
1004