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