Path: blob/main/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts
3296 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 { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';6import { URI } from '../../../../base/common/uri.js';7import { CellUri, IResolvedNotebookEditorModel, NotebookEditorModelCreationOptions, NotebookSetting, NotebookWorkingCopyTypeIdentifier } from './notebookCommon.js';8import { NotebookFileWorkingCopyModel, NotebookFileWorkingCopyModelFactory, SimpleNotebookEditorModel } from './notebookEditorModel.js';9import { combinedDisposable, DisposableStore, dispose, IDisposable, IReference, ReferenceCollection, toDisposable } from '../../../../base/common/lifecycle.js';10import { INotebookService } from './notebookService.js';11import { AsyncEmitter, Emitter, Event } from '../../../../base/common/event.js';12import { IExtensionService } from '../../../services/extensions/common/extensions.js';13import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';14import { INotebookConflictEvent, INotebookEditorModelResolverService, IUntitledNotebookResource } from './notebookEditorModelResolverService.js';15import { ResourceMap } from '../../../../base/common/map.js';16import { FileWorkingCopyManager, IFileWorkingCopyManager } from '../../../services/workingCopy/common/fileWorkingCopyManager.js';17import { Schemas } from '../../../../base/common/network.js';18import { NotebookProviderInfo } from './notebookProvider.js';19import { assertReturnsDefined } from '../../../../base/common/types.js';20import { CancellationToken } from '../../../../base/common/cancellation.js';21import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';22import { IFileReadLimits } from '../../../../platform/files/common/files.js';23import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';24import { INotebookLoggingService } from './notebookLoggingService.js';25import { parse } from '../../../services/notebook/common/notebookDocumentService.js';2627class NotebookModelReferenceCollection extends ReferenceCollection<Promise<IResolvedNotebookEditorModel>> {2829private readonly _disposables = new DisposableStore();30private readonly _workingCopyManagers = new Map<string, IFileWorkingCopyManager<NotebookFileWorkingCopyModel, NotebookFileWorkingCopyModel>>();31private readonly _modelListener = new Map<IResolvedNotebookEditorModel, IDisposable>();3233private readonly _onDidSaveNotebook = new Emitter<URI>();34readonly onDidSaveNotebook: Event<URI> = this._onDidSaveNotebook.event;3536private readonly _onDidChangeDirty = new Emitter<IResolvedNotebookEditorModel>();37readonly onDidChangeDirty: Event<IResolvedNotebookEditorModel> = this._onDidChangeDirty.event;3839private readonly _dirtyStates = new ResourceMap<boolean>();4041private readonly modelsToDispose = new Set<string>();42constructor(43@IInstantiationService private readonly _instantiationService: IInstantiationService,44@INotebookService private readonly _notebookService: INotebookService,45@IConfigurationService private readonly _configurationService: IConfigurationService,46@ITelemetryService private readonly _telemetryService: ITelemetryService,47@INotebookLoggingService private readonly _notebookLoggingService: INotebookLoggingService,48) {49super();50}5152dispose(): void {53this._disposables.dispose();54this._onDidSaveNotebook.dispose();55this._onDidChangeDirty.dispose();56dispose(this._modelListener.values());57dispose(this._workingCopyManagers.values());58}5960isDirty(resource: URI): boolean {61return this._dirtyStates.get(resource) ?? false;62}6364isListeningToModel(uri: URI): boolean {65for (const key of this._modelListener.keys()) {66if (key.resource.toString() === uri.toString()) {67return true;68}69}70return false;71}7273protected async createReferencedObject(key: string, notebookType: string, hasAssociatedFilePath: boolean, limits?: IFileReadLimits, isScratchpad?: boolean, viewType?: string): Promise<IResolvedNotebookEditorModel> {74// Untrack as being disposed75this.modelsToDispose.delete(key);7677const uri = URI.parse(key);7879const workingCopyTypeId = NotebookWorkingCopyTypeIdentifier.create(notebookType, viewType);80let workingCopyManager = this._workingCopyManagers.get(workingCopyTypeId);81if (!workingCopyManager) {82const factory = new NotebookFileWorkingCopyModelFactory(notebookType, this._notebookService, this._configurationService, this._telemetryService, this._notebookLoggingService);83workingCopyManager = this._instantiationService.createInstance(84FileWorkingCopyManager<NotebookFileWorkingCopyModel, NotebookFileWorkingCopyModel>,85workingCopyTypeId,86factory,87factory,88);89this._workingCopyManagers.set(workingCopyTypeId, workingCopyManager);90}9192const isScratchpadView = isScratchpad || (notebookType === 'interactive' && this._configurationService.getValue<boolean>(NotebookSetting.InteractiveWindowPromptToSave) !== true);93const model = this._instantiationService.createInstance(SimpleNotebookEditorModel, uri, hasAssociatedFilePath, notebookType, workingCopyManager, isScratchpadView);94const result = await model.load({ limits });959697// Whenever a notebook model is dirty we automatically reference it so that98// we can ensure that at least one reference exists. That guarantees that99// a model with unsaved changes is never disposed.100let onDirtyAutoReference: IReference<any> | undefined;101102this._modelListener.set(result, combinedDisposable(103result.onDidSave(() => this._onDidSaveNotebook.fire(result.resource)),104result.onDidChangeDirty(() => {105const isDirty = result.isDirty();106this._dirtyStates.set(result.resource, isDirty);107108// isDirty -> add reference109// !isDirty -> free reference110if (isDirty && !onDirtyAutoReference) {111onDirtyAutoReference = this.acquire(key, notebookType);112} else if (onDirtyAutoReference) {113onDirtyAutoReference.dispose();114onDirtyAutoReference = undefined;115}116117this._onDidChangeDirty.fire(result);118}),119toDisposable(() => onDirtyAutoReference?.dispose()),120));121return result;122}123124protected destroyReferencedObject(key: string, object: Promise<IResolvedNotebookEditorModel>): void {125this.modelsToDispose.add(key);126127(async () => {128try {129const model = await object;130131if (!this.modelsToDispose.has(key)) {132// return if model has been acquired again meanwhile133return;134}135136if (model instanceof SimpleNotebookEditorModel) {137await model.canDispose();138}139140if (!this.modelsToDispose.has(key)) {141// return if model has been acquired again meanwhile142return;143}144145// Finally we can dispose the model146this._modelListener.get(model)?.dispose();147this._modelListener.delete(model);148model.dispose();149} catch (err) {150this._notebookLoggingService.error('NotebookModelCollection', 'FAILED to destory notebook - ' + err);151} finally {152this.modelsToDispose.delete(key); // Untrack as being disposed153}154})();155}156}157158export class NotebookModelResolverServiceImpl implements INotebookEditorModelResolverService {159160readonly _serviceBrand: undefined;161162private readonly _data: NotebookModelReferenceCollection;163164readonly onDidSaveNotebook: Event<URI>;165readonly onDidChangeDirty: Event<IResolvedNotebookEditorModel>;166167private readonly _onWillFailWithConflict = new AsyncEmitter<INotebookConflictEvent>();168readonly onWillFailWithConflict = this._onWillFailWithConflict.event;169170constructor(171@IInstantiationService instantiationService: IInstantiationService,172@INotebookService private readonly _notebookService: INotebookService,173@IExtensionService private readonly _extensionService: IExtensionService,174@IUriIdentityService private readonly _uriIdentService: IUriIdentityService,175) {176this._data = instantiationService.createInstance(NotebookModelReferenceCollection);177this.onDidSaveNotebook = this._data.onDidSaveNotebook;178this.onDidChangeDirty = this._data.onDidChangeDirty;179}180181dispose() {182this._data.dispose();183}184185isDirty(resource: URI): boolean {186return this._data.isDirty(resource);187}188189private createUntitledUri(notebookType: string) {190const info = this._notebookService.getContributedNotebookType(assertReturnsDefined(notebookType));191if (!info) {192throw new Error('UNKNOWN notebook type: ' + notebookType);193}194195const suffix = NotebookProviderInfo.possibleFileEnding(info.selectors) ?? '';196for (let counter = 1; ; counter++) {197const candidate = URI.from({ scheme: Schemas.untitled, path: `Untitled-${counter}${suffix}`, query: notebookType });198if (!this._notebookService.getNotebookTextModel(candidate) && !this._data.isListeningToModel(candidate)) {199return candidate;200}201}202}203204private async validateResourceViewType(uri: URI | undefined, viewType: string | undefined) {205if (!uri && !viewType) {206throw new Error('Must provide at least one of resource or viewType');207}208209if (uri?.scheme === CellUri.scheme) {210const originalUri = uri;211uri = parse(uri)?.notebook;212if (!uri) {213throw new Error(`CANNOT open a cell-uri as notebook. Tried with ${originalUri.toString()}`);214}215}216217const resource = this._uriIdentService.asCanonicalUri(uri ?? this.createUntitledUri(viewType!));218219const existingNotebook = this._notebookService.getNotebookTextModel(resource);220if (!viewType) {221if (existingNotebook) {222viewType = existingNotebook.viewType;223} else {224await this._extensionService.whenInstalledExtensionsRegistered();225const providers = this._notebookService.getContributedNotebookTypes(resource);226viewType = providers.find(provider => provider.priority === 'exclusive')?.id ??227providers.find(provider => provider.priority === 'default')?.id ??228providers[0]?.id;229}230}231232if (!viewType) {233throw new Error(`Missing viewType for '${resource}'`);234}235236if (existingNotebook && existingNotebook.viewType !== viewType) {237238await this._onWillFailWithConflict.fireAsync({ resource: resource, viewType }, CancellationToken.None);239240// check again, listener should have done cleanup241const existingViewType2 = this._notebookService.getNotebookTextModel(resource)?.viewType;242if (existingViewType2 && existingViewType2 !== viewType) {243throw new Error(`A notebook with view type '${existingViewType2}' already exists for '${resource}', CANNOT create another notebook with view type ${viewType}`);244}245}246return { resource, viewType };247}248249public async createUntitledNotebookTextModel(viewType: string) {250const resource = this._uriIdentService.asCanonicalUri(this.createUntitledUri(viewType));251252return (await this._notebookService.createNotebookTextModel(viewType, resource));253}254255async resolve(resource: URI, viewType?: string, options?: NotebookEditorModelCreationOptions): Promise<IReference<IResolvedNotebookEditorModel>>;256async resolve(resource: IUntitledNotebookResource, viewType: string, options: NotebookEditorModelCreationOptions): Promise<IReference<IResolvedNotebookEditorModel>>;257async resolve(arg0: URI | IUntitledNotebookResource, viewType?: string, options?: NotebookEditorModelCreationOptions): Promise<IReference<IResolvedNotebookEditorModel>> {258let resource: URI | undefined;259let hasAssociatedFilePath;260if (URI.isUri(arg0)) {261resource = arg0;262} else if (arg0.untitledResource) {263if (arg0.untitledResource.scheme === Schemas.untitled) {264resource = arg0.untitledResource;265} else {266resource = arg0.untitledResource.with({ scheme: Schemas.untitled });267hasAssociatedFilePath = true;268}269}270271const validated = await this.validateResourceViewType(resource, viewType);272273const reference = this._data.acquire(validated.resource.toString(), validated.viewType, hasAssociatedFilePath, options?.limits, options?.scratchpad, options?.viewType);274try {275const model = await reference.object;276return {277object: model,278dispose() { reference.dispose(); }279};280} catch (err) {281reference.dispose();282throw err;283}284}285}286287288