diff --git a/extensions/vscode-notebook-tests/src/notebook.test.ts b/extensions/vscode-notebook-tests/src/notebook.test.ts index a9369e1ffbb..a5e4bb198fa 100644 --- a/extensions/vscode-notebook-tests/src/notebook.test.ts +++ b/extensions/vscode-notebook-tests/src/notebook.test.ts @@ -568,6 +568,70 @@ suite('notebook undo redo', () => { await vscode.commands.executeCommand('workbench.action.files.save'); await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); }); + + test('execute and then undo redo', async function () { + const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + + const cellsChangeEvent = getEventOncePromise(vscode.notebook.onDidChangeNotebookCells); + await vscode.commands.executeCommand('notebook.cell.insertCodeCellBelow'); + const cellChangeEventRet = await cellsChangeEvent; + assert.equal(cellChangeEventRet.document, vscode.notebook.activeNotebookEditor?.document); + assert.equal(cellChangeEventRet.changes.length, 1); + assert.deepEqual(cellChangeEventRet.changes[0], { + start: 1, + deletedCount: 0, + deletedItems: [], + items: [ + vscode.notebook.activeNotebookEditor!.document.cells[1] + ] + }); + + const secondCell = vscode.notebook.activeNotebookEditor!.document.cells[1]; + + const moveCellEvent = getEventOncePromise(vscode.notebook.onDidChangeNotebookCells); + await vscode.commands.executeCommand('notebook.cell.moveUp'); + const moveCellEventRet = await moveCellEvent; + assert.deepEqual(moveCellEventRet, { + document: vscode.notebook.activeNotebookEditor!.document, + changes: [ + { + start: 1, + deletedCount: 1, + deletedItems: [secondCell], + items: [] + }, + { + start: 0, + deletedCount: 0, + deletedItems: [], + items: [vscode.notebook.activeNotebookEditor?.document.cells[0]] + } + ] + }); + + const cellOutputChange = getEventOncePromise(vscode.notebook.onDidChangeCellOutputs); + await vscode.commands.executeCommand('notebook.cell.execute'); + const cellOutputsAddedRet = await cellOutputChange; + assert.deepEqual(cellOutputsAddedRet, { + document: vscode.notebook.activeNotebookEditor!.document, + cells: [vscode.notebook.activeNotebookEditor!.document.cells[0]] + }); + assert.equal(cellOutputsAddedRet.cells[0].outputs.length, 1); + + const cellOutputClear = getEventOncePromise(vscode.notebook.onDidChangeCellOutputs); + await vscode.commands.executeCommand('notebook.undo'); + const cellOutputsCleardRet = await cellOutputClear; + assert.deepEqual(cellOutputsCleardRet, { + document: vscode.notebook.activeNotebookEditor!.document, + cells: [vscode.notebook.activeNotebookEditor!.document.cells[0]] + }); + assert.equal(cellOutputsAddedRet.cells[0].outputs.length, 0); + + await vscode.commands.executeCommand('workbench.action.files.save'); + await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + }); + }); suite('notebook working copy', () => { diff --git a/extensions/vscode-notebook-tests/src/notebookTestMain.ts b/extensions/vscode-notebook-tests/src/notebookTestMain.ts index ac7e35ce8d4..33dd2960fda 100644 --- a/extensions/vscode-notebook-tests/src/notebookTestMain.ts +++ b/extensions/vscode-notebook-tests/src/notebookTestMain.ts @@ -10,8 +10,10 @@ import { smokeTestActivate } from './notebookSmokeTestMain'; export function activate(context: vscode.ExtensionContext): any { smokeTestActivate(context); + const _onDidChangeNotebook = new vscode.EventEmitter(); + context.subscriptions.push(_onDidChangeNotebook); context.subscriptions.push(vscode.notebook.registerNotebookContentProvider('notebookCoreTest', { - onDidChangeNotebook: new vscode.EventEmitter().event, + onDidChangeNotebook: _onDidChangeNotebook.event, openNotebook: async (_resource: vscode.Uri) => { if (_resource.path.endsWith('empty.vsctestnb')) { return { @@ -71,13 +73,13 @@ export function activate(context: vscode.ExtensionContext): any { }]; return; }, - executeCell: async (_document: vscode.NotebookDocument, _cell: vscode.NotebookCell | undefined, _token: vscode.CancellationToken) => { - if (!_cell) { - _cell = _document.cells[0]; + executeCell: async (document: vscode.NotebookDocument, cell: vscode.NotebookCell | undefined, _token: vscode.CancellationToken) => { + if (!cell) { + cell = document.cells[0]; } - if (_document.uri.path.endsWith('customRenderer.vsctestnb')) { - _cell.outputs = [{ + if (document.uri.path.endsWith('customRenderer.vsctestnb')) { + cell.outputs = [{ outputKind: vscode.CellOutputKind.Rich, data: { 'text/custom': 'test' @@ -87,13 +89,29 @@ export function activate(context: vscode.ExtensionContext): any { return; } - _cell.outputs = [{ + const previousOutputs = cell.outputs; + const newOutputs: vscode.CellOutput[] = [{ outputKind: vscode.CellOutputKind.Rich, data: { 'text/plain': ['my output'] } }]; + cell.outputs = newOutputs; + + _onDidChangeNotebook.fire({ + document: document, + undo: () => { + if (cell) { + cell.outputs = previousOutputs; + } + }, + redo: () => { + if (cell) { + cell.outputs = newOutputs; + } + } + }); return; } })); diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 4321568d6a7..ab820b18917 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -1604,12 +1604,45 @@ declare module 'vscode' { readonly metadata: NotebookDocumentMetadata; } + interface NotebookDocumentContentChangeEvent { + + /** + * The document that the edit is for. + */ + readonly document: NotebookDocument; + } + interface NotebookDocumentEditEvent { /** * The document that the edit is for. */ readonly document: NotebookDocument; + + /** + * Undo the edit operation. + * + * This is invoked by VS Code when the user undoes this edit. To implement `undo`, your + * extension should restore the document and editor to the state they were in just before this + * edit was added to VS Code's internal edit stack by `onDidChangeCustomDocument`. + */ + undo(): Thenable | void; + + /** + * Redo the edit operation. + * + * This is invoked by VS Code when the user redoes this edit. To implement `redo`, your + * extension should restore the document and editor to the state they were in just after this + * edit was added to VS Code's internal edit stack by `onDidChangeCustomDocument`. + */ + redo(): Thenable | void; + + /** + * Display name describing the edit. + * + * This will be shown to users in the UI for undo/redo operations. + */ + readonly label?: string; } interface NotebookDocumentBackup { @@ -1660,7 +1693,7 @@ declare module 'vscode' { }): Promise; saveNotebook(document: NotebookDocument, cancellation: CancellationToken): Promise; saveNotebookAs(targetResource: Uri, document: NotebookDocument, cancellation: CancellationToken): Promise; - readonly onDidChangeNotebook: Event; + readonly onDidChangeNotebook: Event; backupNotebook(document: NotebookDocument, context: NotebookDocumentBackupContext, cancellation: CancellationToken): Promise; kernel?: NotebookKernel; diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts index fefa481e06d..d78dc231e8d 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as nls from 'vs/nls'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { MainContext, MainThreadNotebookShape, NotebookExtensionDescription, IExtHostContext, ExtHostNotebookShape, ExtHostContext, INotebookDocumentsAndEditorsDelta, INotebookModelAddedData } from '../common/extHost.protocol'; import { Disposable, IDisposable, combinedDisposable } from 'vs/base/common/lifecycle'; @@ -17,6 +18,8 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { IRelativePattern } from 'vs/base/common/glob'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IUndoRedoService, UndoRedoElementType } from 'vs/platform/undoRedo/common/undoRedo'; export class MainThreadNotebookDocument extends Disposable { private _textModel: NotebookTextModel; @@ -31,9 +34,12 @@ export class MainThreadNotebookDocument extends Disposable { public viewType: string, public supportBackup: boolean, public uri: URI, - readonly notebookService: INotebookService + @INotebookService readonly notebookService: INotebookService, + @IUndoRedoService readonly undoRedoService: IUndoRedoService + ) { super(); + this._textModel = new NotebookTextModel(handle, viewType, supportBackup, uri); this._register(this._textModel.onDidModelChangeProxy(e => { this._proxy.$acceptModelChanged(this.uri, e); @@ -54,6 +60,22 @@ export class MainThreadNotebookDocument extends Disposable { await this.notebookService.transformSpliceOutputs(this.textModel, splices); this._textModel.$spliceNotebookCellOutputs(cellHandle, splices); } + + handleEdit(editId: number, label: string | undefined): void { + this.undoRedoService.pushElement({ + type: UndoRedoElementType.Resource, + resource: this._textModel.uri, + label: label ?? nls.localize('defaultEditLabel', "Edit"), + undo: async () => { + await this._proxy.$undoNotebook(this._textModel.viewType, this._textModel.uri, editId, this._textModel.isDirty); + }, + redo: async () => { + await this._proxy.$redoNotebook(this._textModel.viewType, this._textModel.uri, editId, this._textModel.isDirty); + }, + }); + this._textModel.setDirty(true); + } + dispose() { this._textModel.dispose(); super.dispose(); @@ -179,7 +201,8 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo @INotebookService private _notebookService: INotebookService, @IConfigurationService private readonly configurationService: IConfigurationService, @IEditorService private readonly editorService: IEditorService, - @IAccessibilityService private readonly accessibilityService: IAccessibilityService + @IAccessibilityService private readonly accessibilityService: IAccessibilityService, + @IInstantiationService private readonly _instantiationService: IInstantiationService ) { super(); @@ -388,7 +411,7 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo } async $registerNotebookProvider(extension: NotebookExtensionDescription, viewType: string, supportBackup: boolean, kernel: INotebookKernelInfoDto | undefined): Promise { - let controller = new MainThreadNotebookController(this._proxy, this, viewType, supportBackup, kernel, this._notebookService); + let controller = new MainThreadNotebookController(this._proxy, this, viewType, supportBackup, kernel, this._notebookService, this._instantiationService); this._notebookProviders.set(viewType, controller); this._notebookService.registerNotebookController(viewType, extension, controller); return; @@ -467,6 +490,16 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo return false; } + + $onDidEdit(resource: UriComponents, viewType: string, editId: number, label: string | undefined): void { + let controller = this._notebookProviders.get(viewType); + controller?.handleEdit(resource, editId, label); + } + + $onContentChange(resource: UriComponents, viewType: string): void { + let controller = this._notebookProviders.get(viewType); + controller?.handleNotebookChange(resource); + } } export class MainThreadNotebookController implements IMainNotebookController { @@ -480,6 +513,7 @@ export class MainThreadNotebookController implements IMainNotebookController { private _supportBackup: boolean, readonly kernel: INotebookKernelInfoDto | undefined, readonly notebookService: INotebookService, + readonly _instantiationService: IInstantiationService ) { } @@ -504,7 +538,7 @@ export class MainThreadNotebookController implements IMainNotebookController { return mainthreadNotebook.textModel; } - let document = new MainThreadNotebookDocument(this._proxy, MainThreadNotebookController.documentHandle++, viewType, this._supportBackup, uri, this.notebookService); + let document = this._instantiationService.createInstance(MainThreadNotebookDocument, this._proxy, MainThreadNotebookController.documentHandle++, viewType, this._supportBackup, uri); this._mapping.set(document.uri.toString(), document); if (backup) { @@ -635,6 +669,11 @@ export class MainThreadNotebookController implements IMainNotebookController { document?.textModel.handleUnknownChange(); } + handleEdit(resource: UriComponents, editId: number, label: string | undefined): void { + let document = this._mapping.get(URI.from(resource).toString()); + document?.handleEdit(editId, label); + } + updateLanguages(resource: UriComponents, languages: string[]) { let document = this._mapping.get(URI.from(resource).toString()); document?.textModel.updateLanguages(languages); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index f35f7b80b9d..ec458233f6a 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -710,6 +710,9 @@ export interface MainThreadNotebookShape extends IDisposable { $updateNotebookCellMetadata(viewType: string, resource: UriComponents, handle: number, metadata: NotebookCellMetadata | undefined): Promise; $spliceNotebookCellOutputs(viewType: string, resource: UriComponents, cellHandle: number, splices: NotebookCellOutputsSplice[], renderers: number[]): Promise; $postMessage(handle: number, value: any): Promise; + + $onDidEdit(resource: UriComponents, viewType: string, editId: number, label: string | undefined): void; + $onContentChange(resource: UriComponents, viewType: string): void; } export interface MainThreadUrlsShape extends IDisposable { @@ -1596,6 +1599,9 @@ export interface ExtHostNotebookShape { $acceptModelChanged(uriComponents: UriComponents, event: NotebookCellsChangedEvent): void; $acceptEditorPropertiesChanged(uriComponents: UriComponents, data: INotebookEditorPropertiesChangeData): void; $acceptDocumentAndEditorsDelta(delta: INotebookDocumentsAndEditorsDelta): Promise; + $undoNotebook(viewType: string, uri: UriComponents, editId: number, isDirty: boolean): Promise; + $redoNotebook(viewType: string, uri: UriComponents, editId: number, isDirty: boolean): Promise; + } export interface ExtHostStorageShape { diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index d2d4cc47c3d..d688ccf2536 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -25,6 +25,7 @@ import { joinPath } from 'vs/base/common/resources'; import { Schemas } from 'vs/base/common/network'; import { hash } from 'vs/base/common/hash'; import { generateUuid } from 'vs/base/common/uuid'; +import { Cache } from './cache'; interface IObservable { proxy: T; @@ -250,6 +251,43 @@ export class ExtHostNotebookDocument extends Disposable implements vscode.Notebo private _backup?: vscode.NotebookDocumentBackup; + + private readonly _edits = new Cache('notebook documents'); + + + addEdit(item: vscode.NotebookDocumentEditEvent): number { + return this._edits.add([item]); + } + + async undo(editId: number, isDirty: boolean): Promise { + await this.getEdit(editId).undo(); + // if (!isDirty) { + // this.disposeBackup(); + // } + } + + async redo(editId: number, isDirty: boolean): Promise { + await this.getEdit(editId).redo(); + // if (!isDirty) { + // this.disposeBackup(); + // } + } + + private getEdit(editId: number): vscode.NotebookDocumentEditEvent { + const edit = this._edits.get(editId, 0); + if (!edit) { + throw new Error('No edit found'); + } + + return edit; + } + + disposeEdits(editIds: number[]): void { + for (const id of editIds) { + this._edits.delete(id); + } + } + private _disposed = false; constructor( @@ -906,8 +944,25 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN const supportBackup = !!provider.backupNotebook; this._proxy.$registerNotebookProvider({ id: extension.identifier, location: extension.extensionLocation }, viewType, supportBackup, provider.kernel ? { id: viewType, label: provider.kernel.label, extensionLocation: extension.extensionLocation, preloads: provider.kernel.preloads } : undefined); + + const contentChangeListener = provider.onDidChangeNotebook(e => { + const document = this._documents.get(URI.revive(e.document.uri).toString()); + + if (!document) { + throw new Error(`Notebook document ${e.document.uri.toString()} not found`); + } + + if (isEditEvent(e)) { + const editId = document.addEdit(e); + this._proxy.$onDidEdit(e.document.uri, viewType, editId, e.label); + } else { + this._proxy.$onContentChange(e.document.uri, viewType); + } + }); + return new extHostTypes.Disposable(() => { listener.dispose(); + contentChangeListener.dispose(); this._notebookContentProviders.delete(viewType); this._proxy.$unregisterNotebookProvider(viewType); }); @@ -1080,6 +1135,26 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN return false; } + async $undoNotebook(viewType: string, uri: UriComponents, editId: number, isDirty: boolean): Promise { + const document = this._documents.get(URI.revive(uri).toString()); + if (!document) { + return; + } + + document.undo(editId, isDirty); + + } + + async $redoNotebook(viewType: string, uri: UriComponents, editId: number, isDirty: boolean): Promise { + const document = this._documents.get(URI.revive(uri).toString()); + if (!document) { + return; + } + + document.redo(editId, isDirty); + } + + async $backup(viewType: string, uri: UriComponents, cancellation: CancellationToken): Promise { const document = this._documents.get(URI.revive(uri).toString()); const provider = this._notebookContentProviders.get(viewType); @@ -1353,3 +1428,8 @@ function hashPath(resource: URI): string { const str = resource.scheme === Schemas.file || resource.scheme === Schemas.untitled ? resource.fsPath : resource.toString(); return hash(str) + ''; } + +function isEditEvent(e: vscode.NotebookDocumentEditEvent | vscode.NotebookDocumentContentChangeEvent): e is vscode.NotebookDocumentEditEvent { + return typeof (e as vscode.NotebookDocumentEditEvent).undo === 'function' + && typeof (e as vscode.NotebookDocumentEditEvent).redo === 'function'; +} diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts index 291acd822a6..c3070a89d7c 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts @@ -78,8 +78,6 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel onDidChangeContent: Event = this._onDidChangeContent.event; private _onDidChangeMetadata = new Emitter(); onDidChangeMetadata: Event = this._onDidChangeMetadata.event; - private readonly _onDidChangeUnknown = new Emitter(); - readonly onDidChangeUnknown: Event = this._onDidChangeUnknown.event; private _mapping: Map = new Map(); private _cellListeners: Map = new Map(); cells: NotebookCellTextModel[]; @@ -104,6 +102,10 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel this._onDidSelectionChangeProxy.fire(this._selections); } + private _dirty = false; + protected readonly _onDidChangeDirty = this._register(new Emitter()); + readonly onDidChangeDirty = this._onDidChangeDirty.event; + constructor( public handle: number, public viewType: string, @@ -114,6 +116,17 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel this.cells = []; } + get isDirty() { + return this._dirty; + } + + setDirty(newState: boolean) { + if (this._dirty !== newState) { + this._dirty = newState; + this._onDidChangeDirty.fire(); + } + } + createCellTextModel( source: string | string[], language: string, @@ -135,7 +148,21 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel const cellUri = CellUri.generate(this.uri, cellHandle); return new NotebookCellTextModel(cellUri, cellHandle, cell.source, cell.language, cell.cellKind, cell.outputs || [], cell.metadata); }); - this.insertNewCell(0, mainCells, false); + + this._isUntitled = false; + + for (let i = 0; i < mainCells.length; i++) { + this._mapping.set(mainCells[i].handle, mainCells[i]); + let dirtyStateListener = mainCells[i].onDidChangeContent(() => { + this.setDirty(true); + this._onDidChangeContent.fire(); + }); + + this._cellListeners.set(mainCells[i].handle, dirtyStateListener); + } + + this.cells.splice(0, 0, ...mainCells); + this._increaseVersionId(); } $applyEdit(modelVersionId: number, rawEdits: ICellEditOperation[], emitToExtHost: boolean = true): boolean { @@ -228,7 +255,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel } handleUnknownChange() { - this._onDidChangeUnknown.fire(); + this.setDirty(true); } updateLanguages(languages: string[]) { @@ -270,10 +297,12 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel let dirtyStateListener = cell.onDidChangeContent(() => { this._isUntitled = false; + this.setDirty(true); this._onDidChangeContent.fire(); }); this._cellListeners.set(cell.handle, dirtyStateListener); + this.setDirty(true); this._onDidChangeContent.fire(); this._onDidModelChangeProxy.fire({ @@ -303,6 +332,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel for (let i = 0; i < cells.length; i++) { this._mapping.set(cells[i].handle, cells[i]); let dirtyStateListener = cells[i].onDidChangeContent(() => { + this.setDirty(true); this._onDidChangeContent.fire(); }); @@ -310,7 +340,9 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel } this.cells.splice(index, 0, ...cells); + this.setDirty(true); this._onDidChangeContent.fire(); + this._increaseVersionId(); if (emitToExtHost) { @@ -345,6 +377,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel this._cellListeners.delete(cell.handle); } this.cells.splice(index, count); + this.setDirty(true); this._onDidChangeContent.fire(); this._increaseVersionId(); @@ -359,6 +392,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel const cells = this.cells.splice(index, 1); this.cells.splice(newIdx, 0, ...cells); + this.setDirty(true); this._onDidChangeContent.fire(); this._increaseVersionId(); diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index b1419f3b6d2..e69fb76a155 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -271,7 +271,6 @@ export interface INotebookTextModel { renderers: Set; onDidChangeCells?: Event; onDidChangeContent: Event; - onDidChangeUnknown: Event; onWillDispose(listener: () => void): IDisposable; } diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts index b6c4289bff7..3104d96ab9f 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts @@ -35,7 +35,6 @@ export interface INotebookLoadOptions { export class NotebookEditorModel extends EditorModel implements IWorkingCopy, INotebookEditorModel { - private _dirty = false; protected readonly _onDidChangeDirty = this._register(new Emitter()); readonly onDidChangeDirty = this._onDidChangeDirty.event; private readonly _onDidChangeContent = this._register(new Emitter()); @@ -113,7 +112,7 @@ export class NotebookEditorModel extends EditorModel implements IWorkingCopy, IN await this.load({ forceReadFromDisk: true }); - this._dirty = false; + this._notebook.setDirty(false); this._onDidChangeDirty.fire(); } @@ -153,15 +152,14 @@ export class NotebookEditorModel extends EditorModel implements IWorkingCopy, IN this._name = basename(this._notebook!.uri); this._register(this._notebook.onDidChangeContent(() => { - this.setDirty(true); this._onDidChangeContent.fire(); })); - this._register(this._notebook.onDidChangeUnknown(() => { - this.setDirty(true); + this._register(this._notebook.onDidChangeDirty(() => { + this._onDidChangeDirty.fire(); })); await this._backupFileService.discardBackup(this._workingCopyResource); - this.setDirty(true); + this._notebook.setDirty(true); return this; } @@ -173,16 +171,15 @@ export class NotebookEditorModel extends EditorModel implements IWorkingCopy, IN this._name = basename(this._notebook!.uri); this._register(this._notebook.onDidChangeContent(() => { - this.setDirty(true); this._onDidChangeContent.fire(); })); - this._register(this._notebook.onDidChangeUnknown(() => { - this.setDirty(true); + this._register(this._notebook.onDidChangeDirty(() => { + this._onDidChangeDirty.fire(); })); if (backupId) { await this._backupFileService.discardBackup(this._workingCopyResource); - this.setDirty(true); + this._notebook.setDirty(true); } return this; @@ -192,15 +189,8 @@ export class NotebookEditorModel extends EditorModel implements IWorkingCopy, IN return !!this._notebook; } - setDirty(newState: boolean) { - if (this._dirty !== newState) { - this._dirty = newState; - this._onDidChangeDirty.fire(); - } - } - isDirty() { - return this._dirty; + return this._notebook?.isDirty; } isUntitled() { @@ -210,16 +200,14 @@ export class NotebookEditorModel extends EditorModel implements IWorkingCopy, IN async save(): Promise { const tokenSource = new CancellationTokenSource(); await this._notebookService.save(this.notebook.viewType, this.notebook.uri, tokenSource.token); - this._dirty = false; - this._onDidChangeDirty.fire(); + this._notebook.setDirty(false); return true; } async saveAs(targetResource: URI): Promise { const tokenSource = new CancellationTokenSource(); await this._notebookService.saveAs(this.notebook.viewType, this.notebook.uri, targetResource, tokenSource.token); - this._dirty = false; - this._onDidChangeDirty.fire(); + this._notebook.setDirty(false); return true; } }