Path: blob/main/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts
5250 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { localize } from '../../../../../nls.js';6import { toAction } from '../../../../../base/common/actions.js';7import { createErrorWithActions } from '../../../../../base/common/errorMessage.js';8import { Emitter, Event } from '../../../../../base/common/event.js';9import * as glob from '../../../../../base/common/glob.js';10import { Iterable } from '../../../../../base/common/iterator.js';11import { Lazy } from '../../../../../base/common/lazy.js';12import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js';13import { ResourceMap } from '../../../../../base/common/map.js';14import { Schemas } from '../../../../../base/common/network.js';15import { basename, isEqual } from '../../../../../base/common/resources.js';16import { isDefined } from '../../../../../base/common/types.js';17import { URI } from '../../../../../base/common/uri.js';18import { IAccessibilityService } from '../../../../../platform/accessibility/common/accessibility.js';19import { ConfigurationTarget, IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';20import { IResourceEditorInput } from '../../../../../platform/editor/common/editor.js';21import { IFileService } from '../../../../../platform/files/common/files.js';22import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';23import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js';24import { Memento } from '../../../../common/memento.js';25import { INotebookEditorContribution, notebookPreloadExtensionPoint, notebookRendererExtensionPoint, notebooksExtensionPoint } from '../notebookExtensionPoint.js';26import { INotebookEditorOptions } from '../notebookBrowser.js';27import { NotebookDiffEditorInput } from '../../common/notebookDiffEditorInput.js';28import { NotebookCellTextModel } from '../../common/model/notebookCellTextModel.js';29import { NotebookTextModel } from '../../common/model/notebookTextModel.js';30import { 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';31import { NotebookEditorInput } from '../../common/notebookEditorInput.js';32import { INotebookEditorModelResolverService } from '../../common/notebookEditorModelResolverService.js';33import { NotebookOutputRendererInfo, NotebookStaticPreloadInfo } from '../../common/notebookOutputRenderer.js';34import { NotebookEditorDescriptor, NotebookProviderInfo } from '../../common/notebookProvider.js';35import { INotebookSerializer, INotebookService, SimpleNotebookProviderInfo } from '../../common/notebookService.js';36import { DiffEditorInputFactoryFunction, EditorInputFactoryFunction, EditorInputFactoryObject, IEditorResolverService, IEditorType, RegisteredEditorInfo, RegisteredEditorPriority, UntitledEditorInputFactoryFunction, type MergeEditorInputFactoryFunction } from '../../../../services/editor/common/editorResolverService.js';37import { IExtensionService, isProposedApiEnabled } from '../../../../services/extensions/common/extensions.js';38import { IExtensionPointUser } from '../../../../services/extensions/common/extensionsRegistry.js';39import { InstallRecommendedExtensionAction } from '../../../extensions/browser/extensionsActions.js';40import { IUriIdentityService } from '../../../../../platform/uriIdentity/common/uriIdentity.js';41import { INotebookDocument, INotebookDocumentService } from '../../../../services/notebook/common/notebookDocumentService.js';42import { MergeEditorInput } from '../../../mergeEditor/browser/mergeEditorInput.js';43import type { EditorInputWithOptions, IResourceDiffEditorInput, IResourceMergeEditorInput } from '../../../../common/editor.js';44import { bufferToStream, streamToBuffer, VSBuffer, VSBufferReadableStream } from '../../../../../base/common/buffer.js';45import type { IEditorGroup } from '../../../../services/editor/common/editorGroupsService.js';46import { NotebookMultiDiffEditorInput } from '../diff/notebookMultiDiffEditorInput.js';47import { SnapshotContext } from '../../../../services/workingCopy/common/fileWorkingCopy.js';48import { CancellationToken } from '../../../../../base/common/cancellation.js';49import { CancellationError } from '../../../../../base/common/errors.js';50import { ICellRange } from '../../common/notebookRange.js';5152interface NotebookProviderInfoStoreMemento {53editors: NotebookProviderInfo[];54}5556export class NotebookProviderInfoStore extends Disposable {5758private static readonly CUSTOM_EDITORS_STORAGE_ID = 'notebookEditors';59private static readonly CUSTOM_EDITORS_ENTRY_ID = 'editors';6061private readonly _memento: Memento<NotebookProviderInfoStoreMemento>;62private _handled: boolean = false;6364private readonly _contributedEditors = new Map<string, NotebookProviderInfo>();65private readonly _contributedEditorDisposables = this._register(new DisposableStore());6667constructor(68@IStorageService storageService: IStorageService,69@IExtensionService extensionService: IExtensionService,70@IEditorResolverService private readonly _editorResolverService: IEditorResolverService,71@IConfigurationService private readonly _configurationService: IConfigurationService,72@IAccessibilityService private readonly _accessibilityService: IAccessibilityService,73@IInstantiationService private readonly _instantiationService: IInstantiationService,74@IFileService private readonly _fileService: IFileService,75@INotebookEditorModelResolverService private readonly _notebookEditorModelResolverService: INotebookEditorModelResolverService,76@IUriIdentityService private readonly uriIdentService: IUriIdentityService,77) {78super();7980this._memento = new Memento(NotebookProviderInfoStore.CUSTOM_EDITORS_STORAGE_ID, storageService);8182const mementoObject = this._memento.getMemento(StorageScope.PROFILE, StorageTarget.MACHINE);83// Process the notebook contributions but buffer changes from the resolver84this._editorResolverService.bufferChangeEvents(() => {85for (const info of (mementoObject[NotebookProviderInfoStore.CUSTOM_EDITORS_ENTRY_ID] || []) as NotebookEditorDescriptor[]) {86this.add(new NotebookProviderInfo(info), false);87}88});8990this._register(extensionService.onDidRegisterExtensions(() => {91if (!this._handled) {92// there is no extension point registered for notebook content provider93// clear the memento and cache94this._clear();95mementoObject[NotebookProviderInfoStore.CUSTOM_EDITORS_ENTRY_ID] = [];96this._memento.saveMemento();97}98}));99100notebooksExtensionPoint.setHandler(extensions => this._setupHandler(extensions));101}102103override dispose(): void {104this._clear();105super.dispose();106}107108private _setupHandler(extensions: readonly IExtensionPointUser<INotebookEditorContribution[]>[]) {109this._handled = true;110const builtins: NotebookProviderInfo[] = [...this._contributedEditors.values()].filter(info => !info.extension);111this._clear();112113const builtinProvidersFromCache: Map<string, IDisposable> = new Map();114builtins.forEach(builtin => {115builtinProvidersFromCache.set(builtin.id, this.add(builtin));116});117118for (const extension of extensions) {119for (const notebookContribution of extension.value) {120121if (!notebookContribution.type) {122extension.collector.error(`Notebook does not specify type-property`);123continue;124}125126const existing = this.get(notebookContribution.type);127128if (existing) {129if (!existing.extension && extension.description.isBuiltin && builtins.find(builtin => builtin.id === notebookContribution.type)) {130// we are registering an extension which is using the same view type which is already cached131builtinProvidersFromCache.get(notebookContribution.type)?.dispose();132} else {133extension.collector.error(`Notebook type '${notebookContribution.type}' already used`);134continue;135}136}137138this.add(new NotebookProviderInfo({139extension: extension.description.identifier,140id: notebookContribution.type,141displayName: notebookContribution.displayName,142selectors: notebookContribution.selector || [],143priority: this._convertPriority(notebookContribution.priority),144providerDisplayName: extension.description.displayName ?? extension.description.identifier.value,145}));146}147}148149const mementoObject = this._memento.getMemento(StorageScope.PROFILE, StorageTarget.MACHINE);150mementoObject[NotebookProviderInfoStore.CUSTOM_EDITORS_ENTRY_ID] = Array.from(this._contributedEditors.values());151this._memento.saveMemento();152}153154clearEditorCache() {155const mementoObject = this._memento.getMemento(StorageScope.PROFILE, StorageTarget.MACHINE);156mementoObject[NotebookProviderInfoStore.CUSTOM_EDITORS_ENTRY_ID] = [];157this._memento.saveMemento();158}159160private _convertPriority(priority?: string) {161if (!priority) {162return RegisteredEditorPriority.default;163}164165if (priority === NotebookEditorPriority.default) {166return RegisteredEditorPriority.default;167}168169return RegisteredEditorPriority.option;170171}172173private _registerContributionPoint(notebookProviderInfo: NotebookProviderInfo): IDisposable {174175const disposables = new DisposableStore();176177for (const selector of notebookProviderInfo.selectors) {178const globPattern = (selector as INotebookExclusiveDocumentFilter).include || selector as glob.IRelativePattern | string;179const notebookEditorInfo: RegisteredEditorInfo = {180id: notebookProviderInfo.id,181label: notebookProviderInfo.displayName,182detail: notebookProviderInfo.providerDisplayName,183priority: notebookProviderInfo.priority,184};185const notebookEditorOptions = {186canHandleDiff: () => !!this._configurationService.getValue(NotebookSetting.textDiffEditorPreview) && !this._accessibilityService.isScreenReaderOptimized(),187canSupportResource: (resource: URI) => {188if (resource.scheme === Schemas.vscodeNotebookCellOutput) {189const params = new URLSearchParams(resource.query);190return params.get('openIn') === 'notebook';191}192return resource.scheme === Schemas.untitled || resource.scheme === Schemas.vscodeNotebookCell || this._fileService.hasProvider(resource);193}194};195const notebookEditorInputFactory: EditorInputFactoryFunction = async ({ resource, options }) => {196let data;197if (resource.scheme === Schemas.vscodeNotebookCellOutput) {198const outputUriData = CellUri.parseCellOutputUri(resource);199if (!outputUriData || !outputUriData.notebook || outputUriData.cellHandle === undefined) {200throw new Error('Invalid cell output uri');201}202203data = {204notebook: outputUriData.notebook,205handle: outputUriData.cellHandle206};207208} else {209data = CellUri.parse(resource);210}211212let notebookUri: URI;213214let cellOptions: IResourceEditorInput | undefined;215216if (data) {217// resource is a notebook cell218notebookUri = this.uriIdentService.asCanonicalUri(data.notebook);219cellOptions = { resource, options };220} else {221notebookUri = this.uriIdentService.asCanonicalUri(resource);222}223224if (!cellOptions) {225cellOptions = (options as INotebookEditorOptions | undefined)?.cellOptions;226}227228let notebookOptions: INotebookEditorOptions;229230if (resource.scheme === Schemas.vscodeNotebookCellOutput) {231if (data?.handle === undefined || !data?.notebook) {232throw new Error('Invalid cell handle');233}234235const cellUri = CellUri.generate(data.notebook, data.handle);236237cellOptions = { resource: cellUri, options };238239const cellIndex = await this._notebookEditorModelResolverService.resolve(notebookUri)240.then(model => model.object.notebook.cells.findIndex(cell => cell.handle === data?.handle))241.then(index => index >= 0 ? index : 0);242243const cellIndexesToRanges: ICellRange[] = [{ start: cellIndex, end: cellIndex + 1 }];244245notebookOptions = {246...options,247cellOptions,248viewState: undefined,249cellSelections: cellIndexesToRanges250};251} else {252notebookOptions = {253...options,254cellOptions,255viewState: undefined,256};257}258const preferredResourceParam = cellOptions?.resource;259const editor = NotebookEditorInput.getOrCreate(this._instantiationService, notebookUri, preferredResourceParam, notebookProviderInfo.id);260return { editor, options: notebookOptions };261};262263const notebookUntitledEditorFactory: UntitledEditorInputFactoryFunction = async ({ resource, options }) => {264const ref = await this._notebookEditorModelResolverService.resolve({ untitledResource: resource }, notebookProviderInfo.id);265266// untitled notebooks are disposed when they get saved. we should not hold a reference267// to such a disposed notebook and therefore dispose the reference as well268Event.once(ref.object.notebook.onWillDispose)(() => {269ref.dispose();270});271272return { editor: NotebookEditorInput.getOrCreate(this._instantiationService, ref.object.resource, undefined, notebookProviderInfo.id), options };273};274const notebookDiffEditorInputFactory: DiffEditorInputFactoryFunction = (diffEditorInput: IResourceDiffEditorInput, group: IEditorGroup) => {275const { modified, original, label, description } = diffEditorInput;276277if (this._configurationService.getValue('notebook.experimental.enableNewDiffEditor')) {278return { editor: NotebookMultiDiffEditorInput.create(this._instantiationService, modified.resource!, label, description, original.resource!, notebookProviderInfo.id) };279}280return { editor: NotebookDiffEditorInput.create(this._instantiationService, modified.resource!, label, description, original.resource!, notebookProviderInfo.id) };281};282const mergeEditorInputFactory: MergeEditorInputFactoryFunction = (mergeEditor: IResourceMergeEditorInput): EditorInputWithOptions => {283return {284editor: this._instantiationService.createInstance(285MergeEditorInput,286mergeEditor.base.resource,287{288uri: mergeEditor.input1.resource,289title: mergeEditor.input1.label ?? basename(mergeEditor.input1.resource),290description: mergeEditor.input1.description ?? '',291detail: mergeEditor.input1.detail292},293{294uri: mergeEditor.input2.resource,295title: mergeEditor.input2.label ?? basename(mergeEditor.input2.resource),296description: mergeEditor.input2.description ?? '',297detail: mergeEditor.input2.detail298},299mergeEditor.result.resource300)301};302};303304const notebookFactoryObject: EditorInputFactoryObject = {305createEditorInput: notebookEditorInputFactory,306createDiffEditorInput: notebookDiffEditorInputFactory,307createUntitledEditorInput: notebookUntitledEditorFactory,308createMergeEditorInput: mergeEditorInputFactory309};310const notebookCellFactoryObject: EditorInputFactoryObject = {311createEditorInput: notebookEditorInputFactory,312createDiffEditorInput: notebookDiffEditorInputFactory,313};314315// TODO @lramos15 find a better way to toggle handling diff editors than needing these listeners for every registration316// This is a lot of event listeners especially if there are many notebooks317disposables.add(this._configurationService.onDidChangeConfiguration(e => {318if (e.affectsConfiguration(NotebookSetting.textDiffEditorPreview)) {319const canHandleDiff = !!this._configurationService.getValue(NotebookSetting.textDiffEditorPreview) && !this._accessibilityService.isScreenReaderOptimized();320if (canHandleDiff) {321notebookFactoryObject.createDiffEditorInput = notebookDiffEditorInputFactory;322notebookCellFactoryObject.createDiffEditorInput = notebookDiffEditorInputFactory;323} else {324notebookFactoryObject.createDiffEditorInput = undefined;325notebookCellFactoryObject.createDiffEditorInput = undefined;326}327}328}));329330disposables.add(this._accessibilityService.onDidChangeScreenReaderOptimized(() => {331const canHandleDiff = !!this._configurationService.getValue(NotebookSetting.textDiffEditorPreview) && !this._accessibilityService.isScreenReaderOptimized();332if (canHandleDiff) {333notebookFactoryObject.createDiffEditorInput = notebookDiffEditorInputFactory;334notebookCellFactoryObject.createDiffEditorInput = notebookDiffEditorInputFactory;335} else {336notebookFactoryObject.createDiffEditorInput = undefined;337notebookCellFactoryObject.createDiffEditorInput = undefined;338}339}));340341// Register the notebook editor342disposables.add(this._editorResolverService.registerEditor(343globPattern,344notebookEditorInfo,345notebookEditorOptions,346notebookFactoryObject,347));348// Then register the schema handler as exclusive for that notebook349disposables.add(this._editorResolverService.registerEditor(350`${Schemas.vscodeNotebookCell}:/**/${globPattern}`,351{ ...notebookEditorInfo, priority: RegisteredEditorPriority.exclusive },352notebookEditorOptions,353notebookCellFactoryObject354));355}356357return disposables;358}359360361private _clear(): void {362this._contributedEditors.clear();363this._contributedEditorDisposables.clear();364}365366get(viewType: string): NotebookProviderInfo | undefined {367return this._contributedEditors.get(viewType);368}369370add(info: NotebookProviderInfo, saveMemento = true): IDisposable {371if (this._contributedEditors.has(info.id)) {372throw new Error(`notebook type '${info.id}' ALREADY EXISTS`);373}374this._contributedEditors.set(info.id, info);375let editorRegistration: IDisposable | undefined;376377// built-in notebook providers contribute their own editors378if (info.extension) {379editorRegistration = this._registerContributionPoint(info);380this._contributedEditorDisposables.add(editorRegistration);381}382383if (saveMemento) {384const mementoObject = this._memento.getMemento(StorageScope.PROFILE, StorageTarget.MACHINE);385mementoObject[NotebookProviderInfoStore.CUSTOM_EDITORS_ENTRY_ID] = Array.from(this._contributedEditors.values());386this._memento.saveMemento();387}388389return this._register(toDisposable(() => {390const mementoObject = this._memento.getMemento(StorageScope.PROFILE, StorageTarget.MACHINE);391mementoObject[NotebookProviderInfoStore.CUSTOM_EDITORS_ENTRY_ID] = Array.from(this._contributedEditors.values());392this._memento.saveMemento();393editorRegistration?.dispose();394this._contributedEditors.delete(info.id);395}));396}397398getContributedNotebook(resource: URI): readonly NotebookProviderInfo[] {399const result: NotebookProviderInfo[] = [];400for (const info of this._contributedEditors.values()) {401if (info.matches(resource)) {402result.push(info);403}404}405if (result.length === 0 && resource.scheme === Schemas.untitled) {406// untitled resource and no path-specific match => all providers apply407return Array.from(this._contributedEditors.values());408}409return result;410}411412[Symbol.iterator](): Iterator<NotebookProviderInfo> {413return this._contributedEditors.values();414}415}416417interface NotebookOutputRendererInfoStoreMemento {418[notebookType: string]: { [mimeType: string]: string } | undefined;419}420421export class NotebookOutputRendererInfoStore {422private readonly contributedRenderers = new Map</* rendererId */ string, NotebookOutputRendererInfo>();423private readonly preferredMimetypeMemento: Memento<NotebookOutputRendererInfoStoreMemento>;424private readonly preferredMimetype = new Lazy<NotebookOutputRendererInfoStoreMemento>(425() => this.preferredMimetypeMemento.getMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE));426427constructor(428@IStorageService storageService: IStorageService,429) {430this.preferredMimetypeMemento = new Memento('workbench.editor.notebook.preferredRenderer2', storageService);431}432433clear() {434this.contributedRenderers.clear();435}436437get(rendererId: string): NotebookOutputRendererInfo | undefined {438return this.contributedRenderers.get(rendererId);439}440441getAll(): NotebookOutputRendererInfo[] {442return Array.from(this.contributedRenderers.values());443}444445add(info: NotebookOutputRendererInfo): void {446if (this.contributedRenderers.has(info.id)) {447return;448}449this.contributedRenderers.set(info.id, info);450}451452/** Update and remember the preferred renderer for the given mimetype in this workspace */453setPreferred(notebookProviderInfo: NotebookProviderInfo, mimeType: string, rendererId: string) {454const mementoObj = this.preferredMimetype.value;455const forNotebook = mementoObj[notebookProviderInfo.id];456if (forNotebook) {457forNotebook[mimeType] = rendererId;458} else {459mementoObj[notebookProviderInfo.id] = { [mimeType]: rendererId };460}461462this.preferredMimetypeMemento.saveMemento();463}464465findBestRenderers(notebookProviderInfo: NotebookProviderInfo | undefined, mimeType: string, kernelProvides: readonly string[] | undefined): IOrderedMimeType[] {466467const enum ReuseOrder {468PreviouslySelected = 1 << 8,469SameExtensionAsNotebook = 2 << 8,470OtherRenderer = 3 << 8,471BuiltIn = 4 << 8,472}473474const preferred = notebookProviderInfo && this.preferredMimetype.value[notebookProviderInfo.id]?.[mimeType];475const notebookExtId = notebookProviderInfo?.extension?.value;476const notebookId = notebookProviderInfo?.id;477const renderers: { ordered: IOrderedMimeType; score: number }[] = Array.from(this.contributedRenderers.values())478.map(renderer => {479const ownScore = kernelProvides === undefined480? renderer.matchesWithoutKernel(mimeType)481: renderer.matches(mimeType, kernelProvides);482483if (ownScore === NotebookRendererMatch.Never) {484return undefined;485}486487const rendererExtId = renderer.extensionId.value;488const reuseScore = preferred === renderer.id489? ReuseOrder.PreviouslySelected490: rendererExtId === notebookExtId || RENDERER_EQUIVALENT_EXTENSIONS.get(rendererExtId)?.has(notebookId!)491? ReuseOrder.SameExtensionAsNotebook492: renderer.isBuiltin ? ReuseOrder.BuiltIn : ReuseOrder.OtherRenderer;493return {494ordered: { mimeType, rendererId: renderer.id, isTrusted: true },495score: reuseScore | ownScore,496};497}).filter(isDefined);498499if (renderers.length === 0) {500return [{ mimeType, rendererId: RENDERER_NOT_AVAILABLE, isTrusted: true }];501}502503return renderers.sort((a, b) => a.score - b.score).map(r => r.ordered);504}505}506507class ModelData implements IDisposable, INotebookDocument {508private readonly _modelEventListeners = new DisposableStore();509get uri() { return this.model.uri; }510511constructor(512readonly model: NotebookTextModel,513onWillDispose: (model: INotebookTextModel) => void514) {515this._modelEventListeners.add(model.onWillDispose(() => onWillDispose(model)));516}517518getCellIndex(cellUri: URI): number | undefined {519return this.model.cells.findIndex(cell => isEqual(cell.uri, cellUri));520}521522dispose(): void {523this._modelEventListeners.dispose();524}525}526527interface NotebookServiceMemento {528[viewType: string]: string | undefined;529}530531export class NotebookService extends Disposable implements INotebookService {532533declare readonly _serviceBrand: undefined;534private static _storageNotebookViewTypeProvider = 'notebook.viewTypeProvider';535private readonly _memento: Memento<NotebookServiceMemento>;536private readonly _viewTypeCache: NotebookServiceMemento;537538private readonly _notebookProviders;539private _notebookProviderInfoStore: NotebookProviderInfoStore | undefined;540private get notebookProviderInfoStore(): NotebookProviderInfoStore {541if (!this._notebookProviderInfoStore) {542this._notebookProviderInfoStore = this._register(this._instantiationService.createInstance(NotebookProviderInfoStore));543}544545return this._notebookProviderInfoStore;546}547private readonly _notebookRenderersInfoStore;548private readonly _onDidChangeOutputRenderers;549readonly onDidChangeOutputRenderers;550551private readonly _notebookStaticPreloadInfoStore;552553private readonly _models;554555private readonly _onWillAddNotebookDocument;556private readonly _onDidAddNotebookDocument;557private readonly _onWillRemoveNotebookDocument;558private readonly _onDidRemoveNotebookDocument;559560readonly onWillAddNotebookDocument;561readonly onDidAddNotebookDocument;562readonly onDidRemoveNotebookDocument;563readonly onWillRemoveNotebookDocument;564565private readonly _onAddViewType;566readonly onAddViewType;567568private readonly _onWillRemoveViewType;569readonly onWillRemoveViewType;570571private readonly _onDidChangeEditorTypes;572readonly onDidChangeEditorTypes: Event<void>;573574private _cutItems: NotebookCellTextModel[] | undefined;575private _lastClipboardIsCopy: boolean;576577private _displayOrder!: MimeTypeDisplayOrder;578579constructor(580@IExtensionService private readonly _extensionService: IExtensionService,581@IConfigurationService private readonly _configurationService: IConfigurationService,582@IAccessibilityService private readonly _accessibilityService: IAccessibilityService,583@IInstantiationService private readonly _instantiationService: IInstantiationService,584@IStorageService private readonly _storageService: IStorageService,585@INotebookDocumentService private readonly _notebookDocumentService: INotebookDocumentService586) {587super();588this._notebookProviders = new Map<string, SimpleNotebookProviderInfo>();589this._notebookProviderInfoStore = undefined;590this._notebookRenderersInfoStore = this._instantiationService.createInstance(NotebookOutputRendererInfoStore);591this._onDidChangeOutputRenderers = this._register(new Emitter<void>());592this.onDidChangeOutputRenderers = this._onDidChangeOutputRenderers.event;593this._notebookStaticPreloadInfoStore = new Set<NotebookStaticPreloadInfo>();594this._models = new ResourceMap<ModelData>();595this._onWillAddNotebookDocument = this._register(new Emitter<NotebookTextModel>());596this._onDidAddNotebookDocument = this._register(new Emitter<NotebookTextModel>());597this._onWillRemoveNotebookDocument = this._register(new Emitter<NotebookTextModel>());598this._onDidRemoveNotebookDocument = this._register(new Emitter<NotebookTextModel>());599this.onWillAddNotebookDocument = this._onWillAddNotebookDocument.event;600this.onDidAddNotebookDocument = this._onDidAddNotebookDocument.event;601this.onDidRemoveNotebookDocument = this._onDidRemoveNotebookDocument.event;602this.onWillRemoveNotebookDocument = this._onWillRemoveNotebookDocument.event;603this._onAddViewType = this._register(new Emitter<string>());604this.onAddViewType = this._onAddViewType.event;605this._onWillRemoveViewType = this._register(new Emitter<string>());606this.onWillRemoveViewType = this._onWillRemoveViewType.event;607this._onDidChangeEditorTypes = this._register(new Emitter<void>());608this.onDidChangeEditorTypes = this._onDidChangeEditorTypes.event;609this._lastClipboardIsCopy = true;610611notebookRendererExtensionPoint.setHandler((renderers) => {612this._notebookRenderersInfoStore.clear();613614for (const extension of renderers) {615for (const notebookContribution of extension.value) {616if (!notebookContribution.entrypoint) { // avoid crashing617extension.collector.error(`Notebook renderer does not specify entry point`);618continue;619}620621const id = notebookContribution.id;622if (!id) {623extension.collector.error(`Notebook renderer does not specify id-property`);624continue;625}626627this._notebookRenderersInfoStore.add(new NotebookOutputRendererInfo({628id,629extension: extension.description,630entrypoint: notebookContribution.entrypoint,631displayName: notebookContribution.displayName,632mimeTypes: notebookContribution.mimeTypes || [],633dependencies: notebookContribution.dependencies,634optionalDependencies: notebookContribution.optionalDependencies,635requiresMessaging: notebookContribution.requiresMessaging,636}));637}638}639640this._onDidChangeOutputRenderers.fire();641});642643notebookPreloadExtensionPoint.setHandler(extensions => {644this._notebookStaticPreloadInfoStore.clear();645646for (const extension of extensions) {647if (!isProposedApiEnabled(extension.description, 'contribNotebookStaticPreloads')) {648continue;649}650651for (const notebookContribution of extension.value) {652if (!notebookContribution.entrypoint) { // avoid crashing653extension.collector.error(`Notebook preload does not specify entry point`);654continue;655}656657const type = notebookContribution.type;658if (!type) {659extension.collector.error(`Notebook preload does not specify type-property`);660continue;661}662663this._notebookStaticPreloadInfoStore.add(new NotebookStaticPreloadInfo({664type,665extension: extension.description,666entrypoint: notebookContribution.entrypoint,667localResourceRoots: notebookContribution.localResourceRoots ?? [],668}));669}670}671});672673const updateOrder = () => {674this._displayOrder = new MimeTypeDisplayOrder(675this._configurationService.getValue<string[]>(NotebookSetting.displayOrder) || [],676this._accessibilityService.isScreenReaderOptimized()677? ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER678: NOTEBOOK_DISPLAY_ORDER,679);680};681682updateOrder();683684this._register(this._configurationService.onDidChangeConfiguration(e => {685if (e.affectsConfiguration(NotebookSetting.displayOrder)) {686updateOrder();687}688}));689690this._register(this._accessibilityService.onDidChangeScreenReaderOptimized(() => {691updateOrder();692}));693694this._memento = new Memento(NotebookService._storageNotebookViewTypeProvider, this._storageService);695this._viewTypeCache = this._memento.getMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE);696}697698699getEditorTypes(): IEditorType[] {700return [...this.notebookProviderInfoStore].map(info => ({701id: info.id,702displayName: info.displayName,703providerDisplayName: info.providerDisplayName704}));705}706707clearEditorCache(): void {708this.notebookProviderInfoStore.clearEditorCache();709}710711private _postDocumentOpenActivation(viewType: string) {712// send out activations on notebook text model creation713this._extensionService.activateByEvent(`onNotebook:${viewType}`);714this._extensionService.activateByEvent(`onNotebook:*`);715}716717async canResolve(viewType: string): Promise<boolean> {718if (this._notebookProviders.has(viewType)) {719return true;720}721722await this._extensionService.whenInstalledExtensionsRegistered();723await this._extensionService.activateByEvent(`onNotebookSerializer:${viewType}`);724725return this._notebookProviders.has(viewType);726}727728registerContributedNotebookType(viewType: string, data: INotebookContributionData): IDisposable {729730const info = new NotebookProviderInfo({731extension: data.extension,732id: viewType,733displayName: data.displayName,734providerDisplayName: data.providerDisplayName,735priority: data.priority || RegisteredEditorPriority.default,736selectors: []737});738739info.update({ selectors: data.filenamePattern });740741const reg = this.notebookProviderInfoStore.add(info);742this._onDidChangeEditorTypes.fire();743744return toDisposable(() => {745reg.dispose();746this._onDidChangeEditorTypes.fire();747});748}749750private _registerProviderData(viewType: string, data: SimpleNotebookProviderInfo): IDisposable {751if (this._notebookProviders.has(viewType)) {752throw new Error(`notebook provider for viewtype '${viewType}' already exists`);753}754this._notebookProviders.set(viewType, data);755this._onAddViewType.fire(viewType);756return toDisposable(() => {757this._onWillRemoveViewType.fire(viewType);758this._notebookProviders.delete(viewType);759});760}761762registerNotebookSerializer(viewType: string, extensionData: NotebookExtensionDescription, serializer: INotebookSerializer): IDisposable {763this.notebookProviderInfoStore.get(viewType)?.update({ options: serializer.options });764this._viewTypeCache[viewType] = extensionData.id.value;765this._persistMementos();766return this._registerProviderData(viewType, new SimpleNotebookProviderInfo(viewType, serializer, extensionData));767}768769async withNotebookDataProvider(viewType: string): Promise<SimpleNotebookProviderInfo> {770const selected = this.notebookProviderInfoStore.get(viewType);771if (!selected) {772const knownProvider = this.getViewTypeProvider(viewType);773774const actions = knownProvider ? [775toAction({776id: 'workbench.notebook.action.installMissingViewType', label: localize('notebookOpenInstallMissingViewType', "Install extension for '{0}'", viewType), run: async () => {777await this._instantiationService.createInstance(InstallRecommendedExtensionAction, knownProvider).run();778}779})780] : [];781782throw createErrorWithActions(`UNKNOWN notebook type '${viewType}'`, actions);783}784await this.canResolve(selected.id);785const result = this._notebookProviders.get(selected.id);786if (!result) {787throw new Error(`NO provider registered for view type: '${selected.id}'`);788}789return result;790}791792tryGetDataProviderSync(viewType: string): SimpleNotebookProviderInfo | undefined {793const selected = this.notebookProviderInfoStore.get(viewType);794if (!selected) {795return undefined;796}797return this._notebookProviders.get(selected.id);798}799800801private _persistMementos(): void {802this._memento.saveMemento();803}804805getViewTypeProvider(viewType: string): string | undefined {806return this._viewTypeCache[viewType];807}808809getRendererInfo(rendererId: string): INotebookRendererInfo | undefined {810return this._notebookRenderersInfoStore.get(rendererId);811}812813updateMimePreferredRenderer(viewType: string, mimeType: string, rendererId: string, otherMimetypes: readonly string[]): void {814const info = this.notebookProviderInfoStore.get(viewType);815if (info) {816this._notebookRenderersInfoStore.setPreferred(info, mimeType, rendererId);817}818819this._displayOrder.prioritize(mimeType, otherMimetypes);820}821822saveMimeDisplayOrder(target: ConfigurationTarget) {823this._configurationService.updateValue(NotebookSetting.displayOrder, this._displayOrder.toArray(), target);824}825826getRenderers(): INotebookRendererInfo[] {827return this._notebookRenderersInfoStore.getAll();828}829830*getStaticPreloads(viewType: string): Iterable<INotebookStaticPreloadInfo> {831for (const preload of this._notebookStaticPreloadInfoStore) {832if (preload.type === viewType) {833yield preload;834}835}836}837838// --- notebook documents: create, destory, retrieve, enumerate839840async createNotebookTextModel(viewType: string, uri: URI, stream?: VSBufferReadableStream): Promise<NotebookTextModel> {841if (this._models.has(uri)) {842throw new Error(`notebook for ${uri} already exists`);843}844845const info = await this.withNotebookDataProvider(viewType);846if (!(info instanceof SimpleNotebookProviderInfo)) {847throw new Error('CANNOT open file notebook with this provider');848}849850851const bytes = stream ? await streamToBuffer(stream) : VSBuffer.fromByteArray([]);852const data = await info.serializer.dataToNotebook(bytes);853854855const notebookModel = this._instantiationService.createInstance(NotebookTextModel, info.viewType, uri, data.cells, data.metadata, info.serializer.options);856const modelData = new ModelData(notebookModel, this._onWillDisposeDocument.bind(this));857this._models.set(uri, modelData);858this._notebookDocumentService.addNotebookDocument(modelData);859this._onWillAddNotebookDocument.fire(notebookModel);860this._onDidAddNotebookDocument.fire(notebookModel);861this._postDocumentOpenActivation(info.viewType);862return notebookModel;863}864865async createNotebookTextDocumentSnapshot(uri: URI, context: SnapshotContext, token: CancellationToken): Promise<VSBufferReadableStream> {866const model = this.getNotebookTextModel(uri);867868if (!model) {869throw new Error(`notebook for ${uri} doesn't exist`);870}871872const info = await this.withNotebookDataProvider(model.viewType);873874if (!(info instanceof SimpleNotebookProviderInfo)) {875throw new Error('CANNOT open file notebook with this provider');876}877878const serializer = info.serializer;879const outputSizeLimit = this._configurationService.getValue<number>(NotebookSetting.outputBackupSizeLimit) * 1024;880const data: NotebookData = model.createSnapshot({ context: context, outputSizeLimit: outputSizeLimit, transientOptions: serializer.options });881const indentAmount = model.metadata.indentAmount;882if (typeof indentAmount === 'string' && indentAmount) {883// This is required for ipynb serializer to preserve the whitespace in the notebook.884data.metadata.indentAmount = indentAmount;885}886const bytes = await serializer.notebookToData(data);887888if (token.isCancellationRequested) {889throw new CancellationError();890}891return bufferToStream(bytes);892}893894async restoreNotebookTextModelFromSnapshot(uri: URI, viewType: string, snapshot: VSBufferReadableStream): Promise<NotebookTextModel> {895const model = this.getNotebookTextModel(uri);896897if (!model) {898throw new Error(`notebook for ${uri} doesn't exist`);899}900901const info = await this.withNotebookDataProvider(model.viewType);902903if (!(info instanceof SimpleNotebookProviderInfo)) {904throw new Error('CANNOT open file notebook with this provider');905}906907const serializer = info.serializer;908909const bytes = await streamToBuffer(snapshot);910const data = await info.serializer.dataToNotebook(bytes);911model.restoreSnapshot(data, serializer.options);912913return model;914}915916getNotebookTextModel(uri: URI): NotebookTextModel | undefined {917return this._models.get(uri)?.model;918}919920getNotebookTextModels(): Iterable<NotebookTextModel> {921return Iterable.map(this._models.values(), data => data.model);922}923924listNotebookDocuments(): NotebookTextModel[] {925return [...this._models].map(e => e[1].model);926}927928private _onWillDisposeDocument(model: INotebookTextModel): void {929const modelData = this._models.get(model.uri);930if (modelData) {931this._onWillRemoveNotebookDocument.fire(modelData.model);932this._models.delete(model.uri);933this._notebookDocumentService.removeNotebookDocument(modelData);934modelData.dispose();935this._onDidRemoveNotebookDocument.fire(modelData.model);936}937}938939getOutputMimeTypeInfo(textModel: NotebookTextModel, kernelProvides: readonly string[] | undefined, output: IOutputDto): readonly IOrderedMimeType[] {940const sorted = this._displayOrder.sort(new Set<string>(output.outputs.map(op => op.mime)));941const notebookProviderInfo = this.notebookProviderInfoStore.get(textModel.viewType);942943return sorted944.flatMap(mimeType => this._notebookRenderersInfoStore.findBestRenderers(notebookProviderInfo, mimeType, kernelProvides))945.sort((a, b) => (a.rendererId === RENDERER_NOT_AVAILABLE ? 1 : 0) - (b.rendererId === RENDERER_NOT_AVAILABLE ? 1 : 0));946}947948getContributedNotebookTypes(resource?: URI): readonly NotebookProviderInfo[] {949if (resource) {950return this.notebookProviderInfoStore.getContributedNotebook(resource);951}952953return [...this.notebookProviderInfoStore];954}955956hasSupportedNotebooks(resource: URI): boolean {957if (this._models.has(resource)) {958// it might be untitled959return true;960}961962const contribution = this.notebookProviderInfoStore.getContributedNotebook(resource);963if (!contribution.length) {964return false;965}966return contribution.some(info => info.matches(resource) &&967(info.priority === RegisteredEditorPriority.default || info.priority === RegisteredEditorPriority.exclusive)968);969}970971getContributedNotebookType(viewType: string): NotebookProviderInfo | undefined {972return this.notebookProviderInfoStore.get(viewType);973}974975getNotebookProviderResourceRoots(): URI[] {976const ret: URI[] = [];977this._notebookProviders.forEach(val => {978if (val.extensionData.location) {979ret.push(URI.revive(val.extensionData.location));980}981});982983return ret;984}985986// --- copy & paste987988setToCopy(items: NotebookCellTextModel[], isCopy: boolean) {989this._cutItems = items;990this._lastClipboardIsCopy = isCopy;991}992993getToCopy(): { items: NotebookCellTextModel[]; isCopy: boolean } | undefined {994if (this._cutItems) {995return { items: this._cutItems, isCopy: this._lastClipboardIsCopy };996}997998return undefined;999}10001001}100210031004