Path: blob/main/src/vs/workbench/contrib/notebook/common/notebookEditorModel.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 { VSBufferReadableStream, streamToBuffer } from '../../../../base/common/buffer.js';6import { CancellationToken } from '../../../../base/common/cancellation.js';7import { CancellationError } from '../../../../base/common/errors.js';8import { Emitter, Event } from '../../../../base/common/event.js';9import { IMarkdownString } from '../../../../base/common/htmlContent.js';10import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';11import { Schemas } from '../../../../base/common/network.js';12import { assertType } from '../../../../base/common/types.js';13import { URI } from '../../../../base/common/uri.js';14import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';15import { IWriteFileOptions, IFileStatWithMetadata, FileOperationError, FileOperationResult } from '../../../../platform/files/common/files.js';16import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';17import { IRevertOptions, ISaveOptions, IUntypedEditorInput } from '../../../common/editor.js';18import { EditorModel } from '../../../common/editor/editorModel.js';19import { NotebookTextModel } from './model/notebookTextModel.js';20import { INotebookEditorModel, INotebookLoadOptions, IResolvedNotebookEditorModel, NotebookCellsChangeType, NotebookSetting } from './notebookCommon.js';21import { INotebookLoggingService } from './notebookLoggingService.js';22import { INotebookSerializer, INotebookService, SimpleNotebookProviderInfo } from './notebookService.js';23import { IFilesConfigurationService } from '../../../services/filesConfiguration/common/filesConfigurationService.js';24import { IFileWorkingCopyModelConfiguration, SnapshotContext } from '../../../services/workingCopy/common/fileWorkingCopy.js';25import { IFileWorkingCopyManager } from '../../../services/workingCopy/common/fileWorkingCopyManager.js';26import { IStoredFileWorkingCopy, IStoredFileWorkingCopyModel, IStoredFileWorkingCopyModelContentChangedEvent, IStoredFileWorkingCopyModelFactory, IStoredFileWorkingCopySaveEvent, StoredFileWorkingCopyState } from '../../../services/workingCopy/common/storedFileWorkingCopy.js';27import { IUntitledFileWorkingCopy, IUntitledFileWorkingCopyModel, IUntitledFileWorkingCopyModelContentChangedEvent, IUntitledFileWorkingCopyModelFactory } from '../../../services/workingCopy/common/untitledFileWorkingCopy.js';28import { WorkingCopyCapabilities } from '../../../services/workingCopy/common/workingCopy.js';2930//#region --- simple content provider3132export class SimpleNotebookEditorModel extends EditorModel implements INotebookEditorModel {3334private readonly _onDidChangeDirty = this._register(new Emitter<void>());35private readonly _onDidSave = this._register(new Emitter<IStoredFileWorkingCopySaveEvent>());36private readonly _onDidChangeOrphaned = this._register(new Emitter<void>());37private readonly _onDidChangeReadonly = this._register(new Emitter<void>());38private readonly _onDidRevertUntitled = this._register(new Emitter<void>());3940readonly onDidChangeDirty: Event<void> = this._onDidChangeDirty.event;41readonly onDidSave: Event<IStoredFileWorkingCopySaveEvent> = this._onDidSave.event;42readonly onDidChangeOrphaned: Event<void> = this._onDidChangeOrphaned.event;43readonly onDidChangeReadonly: Event<void> = this._onDidChangeReadonly.event;44readonly onDidRevertUntitled: Event<void> = this._onDidRevertUntitled.event;4546private _workingCopy?: IStoredFileWorkingCopy<NotebookFileWorkingCopyModel> | IUntitledFileWorkingCopy<NotebookFileWorkingCopyModel>;47private readonly _workingCopyListeners = this._register(new DisposableStore());48private readonly scratchPad: boolean;4950constructor(51readonly resource: URI,52private readonly _hasAssociatedFilePath: boolean,53readonly viewType: string,54private readonly _workingCopyManager: IFileWorkingCopyManager<NotebookFileWorkingCopyModel, NotebookFileWorkingCopyModel>,55scratchpad: boolean,56@IFilesConfigurationService private readonly _filesConfigurationService: IFilesConfigurationService,57) {58super();5960this.scratchPad = scratchpad;61}6263override dispose(): void {64this._workingCopy?.dispose();65super.dispose();66}6768get notebook(): NotebookTextModel | undefined {69return this._workingCopy?.model?.notebookModel;70}7172override isResolved(): this is IResolvedNotebookEditorModel {73return Boolean(this._workingCopy?.model?.notebookModel);74}7576async canDispose(): Promise<boolean> {77if (!this._workingCopy) {78return true;79}8081if (SimpleNotebookEditorModel._isStoredFileWorkingCopy(this._workingCopy)) {82return this._workingCopyManager.stored.canDispose(this._workingCopy);83} else {84return true;85}86}8788isDirty(): boolean {89return this._workingCopy?.isDirty() ?? false;90}9192isModified(): boolean {93return this._workingCopy?.isModified() ?? false;94}9596isOrphaned(): boolean {97return SimpleNotebookEditorModel._isStoredFileWorkingCopy(this._workingCopy) && this._workingCopy.hasState(StoredFileWorkingCopyState.ORPHAN);98}99100hasAssociatedFilePath(): boolean {101return !SimpleNotebookEditorModel._isStoredFileWorkingCopy(this._workingCopy) && !!this._workingCopy?.hasAssociatedFilePath;102}103104isReadonly(): boolean | IMarkdownString {105if (SimpleNotebookEditorModel._isStoredFileWorkingCopy(this._workingCopy)) {106return this._workingCopy?.isReadonly();107} else {108return this._filesConfigurationService.isReadonly(this.resource);109}110}111112get hasErrorState(): boolean {113if (this._workingCopy && 'hasState' in this._workingCopy) {114return this._workingCopy.hasState(StoredFileWorkingCopyState.ERROR);115}116117return false;118}119120async revert(options?: IRevertOptions): Promise<void> {121assertType(this.isResolved());122return this._workingCopy!.revert(options);123}124125async save(options?: ISaveOptions): Promise<boolean> {126assertType(this.isResolved());127return this._workingCopy!.save(options);128}129130async load(options?: INotebookLoadOptions): Promise<IResolvedNotebookEditorModel> {131if (!this._workingCopy || !this._workingCopy.model) {132if (this.resource.scheme === Schemas.untitled) {133if (this._hasAssociatedFilePath) {134this._workingCopy = await this._workingCopyManager.resolve({ associatedResource: this.resource });135} else {136this._workingCopy = await this._workingCopyManager.resolve({ untitledResource: this.resource, isScratchpad: this.scratchPad });137}138this._register(this._workingCopy.onDidRevert(() => this._onDidRevertUntitled.fire()));139} else {140this._workingCopy = await this._workingCopyManager.resolve(this.resource, {141limits: options?.limits,142reload: options?.forceReadFromFile ? { async: false, force: true } : undefined143});144this._workingCopyListeners.add(this._workingCopy.onDidSave(e => this._onDidSave.fire(e)));145this._workingCopyListeners.add(this._workingCopy.onDidChangeOrphaned(() => this._onDidChangeOrphaned.fire()));146this._workingCopyListeners.add(this._workingCopy.onDidChangeReadonly(() => this._onDidChangeReadonly.fire()));147}148this._workingCopyListeners.add(this._workingCopy.onDidChangeDirty(() => this._onDidChangeDirty.fire(), undefined));149150this._workingCopyListeners.add(this._workingCopy.onWillDispose(() => {151this._workingCopyListeners.clear();152this._workingCopy?.model?.dispose();153}));154} else {155await this._workingCopyManager.resolve(this.resource, {156reload: {157async: !options?.forceReadFromFile,158force: options?.forceReadFromFile159},160limits: options?.limits161});162}163164assertType(this.isResolved());165return this;166}167168async saveAs(target: URI): Promise<IUntypedEditorInput | undefined> {169const newWorkingCopy = await this._workingCopyManager.saveAs(this.resource, target);170if (!newWorkingCopy) {171return undefined;172}173// this is a little hacky because we leave the new working copy alone. BUT174// the newly created editor input will pick it up and claim ownership of it.175return { resource: newWorkingCopy.resource };176}177178private static _isStoredFileWorkingCopy(candidate?: IStoredFileWorkingCopy<NotebookFileWorkingCopyModel> | IUntitledFileWorkingCopy<NotebookFileWorkingCopyModel>): candidate is IStoredFileWorkingCopy<NotebookFileWorkingCopyModel> {179const isUntitled = candidate && candidate.capabilities & WorkingCopyCapabilities.Untitled;180181return !isUntitled;182}183}184185export class NotebookFileWorkingCopyModel extends Disposable implements IStoredFileWorkingCopyModel, IUntitledFileWorkingCopyModel {186187private readonly _onDidChangeContent = this._register(new Emitter<IStoredFileWorkingCopyModelContentChangedEvent & IUntitledFileWorkingCopyModelContentChangedEvent>());188readonly onDidChangeContent = this._onDidChangeContent.event;189190readonly onWillDispose: Event<void>;191192readonly configuration: IFileWorkingCopyModelConfiguration | undefined = undefined;193save: ((options: IWriteFileOptions, token: CancellationToken) => Promise<IFileStatWithMetadata>) | undefined;194195constructor(196private readonly _notebookModel: NotebookTextModel,197private readonly _notebookService: INotebookService,198private readonly _configurationService: IConfigurationService,199private readonly _telemetryService: ITelemetryService,200private readonly _notebookLogService: INotebookLoggingService,201) {202super();203204this.onWillDispose = _notebookModel.onWillDispose.bind(_notebookModel);205206this._register(_notebookModel.onDidChangeContent(e => {207for (const rawEvent of e.rawEvents) {208if (rawEvent.kind === NotebookCellsChangeType.Initialize) {209continue;210}211if (rawEvent.transient) {212continue;213}214this._onDidChangeContent.fire({215isRedoing: false, //todo@rebornix forward this information from notebook model216isUndoing: false,217isInitial: false, //_notebookModel.cells.length === 0 // todo@jrieken non transient metadata?218});219break;220}221}));222223const saveWithReducedCommunication = this._configurationService.getValue(NotebookSetting.remoteSaving);224225if (saveWithReducedCommunication || _notebookModel.uri.scheme === Schemas.vscodeRemote) {226this.configuration = {227// Intentionally pick a larger delay for triggering backups to allow auto-save228// to complete first on the optimized save path229backupDelay: 10000230};231}232233// Override save behavior to avoid transferring the buffer across the wire 3 times234if (saveWithReducedCommunication) {235this.setSaveDelegate().catch(error => this._notebookLogService.error('WorkingCopyModel', `Failed to set save delegate: ${error}`));236}237}238239private async setSaveDelegate() {240// make sure we wait for a serializer to resolve before we try to handle saves in the EH241await this.getNotebookSerializer();242243this.save = async (options: IWriteFileOptions, token: CancellationToken) => {244try {245let serializer = this._notebookService.tryGetDataProviderSync(this.notebookModel.viewType)?.serializer;246247if (!serializer) {248this._notebookLogService.info('WorkingCopyModel', 'No serializer found for notebook model, checking if provider still needs to be resolved');249serializer = await this.getNotebookSerializer().catch(error => {250this._notebookLogService.error('WorkingCopyModel', `Failed to get notebook serializer: ${error}`);251// The serializer was set initially but somehow is no longer available252this.save = undefined;253throw new NotebookSaveError('Failed to get notebook serializer');254});255}256257if (token.isCancellationRequested) {258throw new CancellationError();259}260261const stat = await serializer.save(this._notebookModel.uri, this._notebookModel.versionId, options, token);262return stat;263} catch (error) {264if (!token.isCancellationRequested && error.name !== 'Canceled') {265type notebookSaveErrorData = {266isRemote: boolean;267isIPyNbWorkerSerializer: boolean;268error: string;269};270type notebookSaveErrorClassification = {271owner: 'amunger';272comment: 'Detect if we are having issues saving a notebook on the Extension Host';273isRemote: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Whether the save is happening on a remote file system' };274isIPyNbWorkerSerializer: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Whether the IPynb files are serialized in workers' };275error: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Info about the error that occurred' };276};277const isIPynb = this._notebookModel.viewType === 'jupyter-notebook' || this._notebookModel.viewType === 'interactive';278const errorMessage = getSaveErrorMessage(error);279this._telemetryService.publicLogError2<notebookSaveErrorData, notebookSaveErrorClassification>('notebook/SaveError', {280isRemote: this._notebookModel.uri.scheme === Schemas.vscodeRemote,281isIPyNbWorkerSerializer: isIPynb && this._configurationService.getValue<boolean>('ipynb.experimental.serialization'),282error: errorMessage283});284}285286throw error;287}288};289}290291override dispose(): void {292this._notebookModel.dispose();293super.dispose();294}295296get notebookModel() {297return this._notebookModel;298}299300async snapshot(context: SnapshotContext, token: CancellationToken): Promise<VSBufferReadableStream> {301return this._notebookService.createNotebookTextDocumentSnapshot(this._notebookModel.uri, context, token);302}303304async update(stream: VSBufferReadableStream, token: CancellationToken): Promise<void> {305const serializer = await this.getNotebookSerializer();306307const bytes = await streamToBuffer(stream);308const data = await serializer.dataToNotebook(bytes);309310if (token.isCancellationRequested) {311throw new CancellationError();312}313314this._notebookLogService.info('WorkingCopyModel', 'Notebook content updated from file system - ' + this._notebookModel.uri.toString());315this._notebookModel.reset(data.cells, data.metadata, serializer.options);316}317318async getNotebookSerializer(): Promise<INotebookSerializer> {319const info = await this._notebookService.withNotebookDataProvider(this.notebookModel.viewType);320if (!(info instanceof SimpleNotebookProviderInfo)) {321const message = 'CANNOT open notebook with this provider';322throw new NotebookSaveError(message);323}324325return info.serializer;326}327328get versionId() {329return this._notebookModel.alternativeVersionId;330}331332pushStackElement(): void {333this._notebookModel.pushStackElement();334}335}336337export class NotebookFileWorkingCopyModelFactory implements IStoredFileWorkingCopyModelFactory<NotebookFileWorkingCopyModel>, IUntitledFileWorkingCopyModelFactory<NotebookFileWorkingCopyModel> {338339constructor(340private readonly _viewType: string,341@INotebookService private readonly _notebookService: INotebookService,342@IConfigurationService private readonly _configurationService: IConfigurationService,343@ITelemetryService private readonly _telemetryService: ITelemetryService,344@INotebookLoggingService private readonly _notebookLogService: INotebookLoggingService345) { }346347async createModel(resource: URI, stream: VSBufferReadableStream, token: CancellationToken): Promise<NotebookFileWorkingCopyModel> {348349const notebookModel = this._notebookService.getNotebookTextModel(resource) ??350await this._notebookService.createNotebookTextModel(this._viewType, resource, stream);351352return new NotebookFileWorkingCopyModel(notebookModel, this._notebookService, this._configurationService, this._telemetryService, this._notebookLogService);353}354}355356//#endregion357358class NotebookSaveError extends Error {359constructor(message: string) {360super(message);361this.name = 'NotebookSaveError';362}363}364365function getSaveErrorMessage(error: Error): string {366if (error.name === 'NotebookSaveError') {367return error.message;368} else if (error instanceof FileOperationError) {369switch (error.fileOperationResult) {370case FileOperationResult.FILE_IS_DIRECTORY:371return 'File is a directory';372case FileOperationResult.FILE_NOT_FOUND:373return 'File not found';374case FileOperationResult.FILE_NOT_MODIFIED_SINCE:375return 'File not modified since';376case FileOperationResult.FILE_MODIFIED_SINCE:377return 'File modified since';378case FileOperationResult.FILE_MOVE_CONFLICT:379return 'File move conflict';380case FileOperationResult.FILE_WRITE_LOCKED:381return 'File write locked';382case FileOperationResult.FILE_PERMISSION_DENIED:383return 'File permission denied';384case FileOperationResult.FILE_TOO_LARGE:385return 'File too large';386case FileOperationResult.FILE_INVALID_PATH:387return 'File invalid path';388case FileOperationResult.FILE_NOT_DIRECTORY:389return 'File not directory';390case FileOperationResult.FILE_OTHER_ERROR:391return 'File other error';392}393}394return 'Unknown error';395}396397398