/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { URI } from 'vs/base/common/uri'; import { CellUri, IResolvedNotebookEditorModel, NotebookWorkingCopyTypeIdentifier } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ComplexNotebookEditorModel, NotebookFileWorkingCopyModel, NotebookFileWorkingCopyModelFactory, SimpleNotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookEditorModel'; import { combinedDisposable, DisposableStore, dispose, IDisposable, IReference, ReferenceCollection, toDisposable } from 'vs/base/common/lifecycle'; import { ComplexNotebookProviderInfo, INotebookService, SimpleNotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookService'; import { ILogService } from 'vs/platform/log/common/log'; import { AsyncEmitter, Emitter, Event } from 'vs/base/common/event'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { INotebookConflictEvent, INotebookEditorModelResolverService, IUntitledNotebookResource } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService'; import { ResourceMap } from 'vs/base/common/map'; import { FileWorkingCopyManager, IFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager'; import { Schemas } from 'vs/base/common/network'; import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider'; import { assertIsDefined } from 'vs/base/common/types'; import { CancellationToken } from 'vs/base/common/cancellation'; class NotebookModelReferenceCollection extends ReferenceCollection> { private readonly _disposables = new DisposableStore(); private readonly _workingCopyManagers = new Map>(); private readonly _modelListener = new Map(); private readonly _onDidSaveNotebook = new Emitter(); readonly onDidSaveNotebook: Event = this._onDidSaveNotebook.event; private readonly _onDidChangeDirty = new Emitter(); readonly onDidChangeDirty: Event = this._onDidChangeDirty.event; private readonly _dirtyStates = new ResourceMap(); constructor( @IInstantiationService readonly _instantiationService: IInstantiationService, @INotebookService private readonly _notebookService: INotebookService, @ILogService private readonly _logService: ILogService, ) { super(); this._disposables.add(_notebookService.onWillRemoveViewType(viewType => { const manager = this._workingCopyManagers.get(NotebookWorkingCopyTypeIdentifier.create(viewType)); manager?.destroy().catch(err => _logService.error(err)); })); } dispose(): void { this._disposables.dispose(); this._onDidSaveNotebook.dispose(); this._onDidChangeDirty.dispose(); dispose(this._modelListener.values()); dispose(this._workingCopyManagers.values()); } isDirty(resource: URI): boolean { return this._dirtyStates.get(resource) ?? false; } protected async createReferencedObject(key: string, viewType: string, hasAssociatedFilePath: boolean): Promise { const uri = URI.parse(key); const info = await this._notebookService.withNotebookDataProvider(viewType); let result: IResolvedNotebookEditorModel; if (info instanceof ComplexNotebookProviderInfo) { const model = this._instantiationService.createInstance(ComplexNotebookEditorModel, uri, viewType, info.controller); result = await model.load(); } else if (info instanceof SimpleNotebookProviderInfo) { const workingCopyTypeId = NotebookWorkingCopyTypeIdentifier.create(viewType); let workingCopyManager = this._workingCopyManagers.get(workingCopyTypeId); if (!workingCopyManager) { const factory = new NotebookFileWorkingCopyModelFactory(viewType, this._notebookService); workingCopyManager = >this._instantiationService.createInstance( FileWorkingCopyManager, workingCopyTypeId, factory, factory, ); this._workingCopyManagers.set(workingCopyTypeId, workingCopyManager); } const model = this._instantiationService.createInstance(SimpleNotebookEditorModel, uri, hasAssociatedFilePath, viewType, workingCopyManager); result = await model.load(); } else { throw new Error(`CANNOT open ${key}, no provider found`); } // Whenever a notebook model is dirty we automatically reference it so that // we can ensure that at least one reference exists. That guarantees that // a model with unsaved changes is never disposed. let onDirtyAutoReference: IReference | undefined; this._modelListener.set(result, combinedDisposable( result.onDidSave(() => this._onDidSaveNotebook.fire(result.resource)), result.onDidChangeDirty(() => { const isDirty = result.isDirty(); this._dirtyStates.set(result.resource, isDirty); // isDirty -> add reference // !isDirty -> free reference if (isDirty && !onDirtyAutoReference) { onDirtyAutoReference = this.acquire(key, viewType); } else if (onDirtyAutoReference) { onDirtyAutoReference.dispose(); onDirtyAutoReference = undefined; } this._onDidChangeDirty.fire(result); }), toDisposable(() => onDirtyAutoReference?.dispose()), )); return result; } protected destroyReferencedObject(_key: string, object: Promise): void { object.then(model => { this._modelListener.get(model)?.dispose(); this._modelListener.delete(model); model.dispose(); }).catch(err => { this._logService.critical('FAILED to destory notebook', err); }); } } export class NotebookModelResolverServiceImpl implements INotebookEditorModelResolverService { readonly _serviceBrand: undefined; private readonly _data: NotebookModelReferenceCollection; readonly onDidSaveNotebook: Event; readonly onDidChangeDirty: Event; private readonly _onWillFailWithConflict = new AsyncEmitter(); readonly onWillFailWithConflict = this._onWillFailWithConflict.event; constructor( @IInstantiationService instantiationService: IInstantiationService, @INotebookService private readonly _notebookService: INotebookService, @IExtensionService private readonly _extensionService: IExtensionService, @IUriIdentityService private readonly _uriIdentService: IUriIdentityService, ) { this._data = instantiationService.createInstance(NotebookModelReferenceCollection); this.onDidSaveNotebook = this._data.onDidSaveNotebook; this.onDidChangeDirty = this._data.onDidChangeDirty; } dispose() { this._data.dispose(); } isDirty(resource: URI): boolean { return this._data.isDirty(resource); } async resolve(resource: URI, viewType?: string): Promise>; async resolve(resource: IUntitledNotebookResource, viewType: string): Promise>; async resolve(arg0: URI | IUntitledNotebookResource, viewType?: string): Promise> { let resource: URI; let hasAssociatedFilePath = false; if (URI.isUri(arg0)) { resource = arg0; } else { if (!arg0.untitledResource) { const info = this._notebookService.getContributedNotebookType(assertIsDefined(viewType)); if (!info) { throw new Error('UNKNOWN view type: ' + viewType); } const suffix = NotebookProviderInfo.possibleFileEnding(info.selectors) ?? ''; for (let counter = 1; ; counter++) { const candidate = URI.from({ scheme: Schemas.untitled, path: `Untitled-${counter}${suffix}`, query: viewType }); if (!this._notebookService.getNotebookTextModel(candidate)) { resource = candidate; break; } } } else if (arg0.untitledResource.scheme === Schemas.untitled) { resource = arg0.untitledResource; } else { resource = arg0.untitledResource.with({ scheme: Schemas.untitled }); hasAssociatedFilePath = true; } } if (resource.scheme === CellUri.scheme) { throw new Error(`CANNOT open a cell-uri as notebook. Tried with ${resource.toString()}`); } resource = this._uriIdentService.asCanonicalUri(resource); const existingViewType = this._notebookService.getNotebookTextModel(resource)?.viewType; if (!viewType) { if (existingViewType) { viewType = existingViewType; } else { await this._extensionService.whenInstalledExtensionsRegistered(); const providers = this._notebookService.getContributedNotebookTypes(resource); const exclusiveProvider = providers.find(provider => provider.exclusive); viewType = exclusiveProvider?.id || providers[0]?.id; } } if (!viewType) { throw new Error(`Missing viewType for '${resource}'`); } if (existingViewType && existingViewType !== viewType) { await this._onWillFailWithConflict.fireAsync({ resource, viewType }, CancellationToken.None); // check again, listener should have done cleanup const existingViewType2 = this._notebookService.getNotebookTextModel(resource)?.viewType; if (existingViewType2 && existingViewType2 !== viewType) { throw new Error(`A notebook with view type '${existingViewType2}' already exists for '${resource}', CANNOT create another notebook with view type ${viewType}`); } } const reference = this._data.acquire(resource.toString(), viewType, hasAssociatedFilePath); try { const model = await reference.object; return { object: model, dispose() { reference.dispose(); } }; } catch (err) { reference.dispose(); throw err; } } }