diff --git a/extensions/vscode-api-tests/package.json b/extensions/vscode-api-tests/package.json index 0abdff1acbe..bce3f10fa67 100644 --- a/extensions/vscode-api-tests/package.json +++ b/extensions/vscode-api-tests/package.json @@ -135,6 +135,15 @@ "filenamePattern": "**/*.nbdtest" } ] + }, + { + "viewType": "notebook.nbdserializer", + "displayName": "notebook.nbdserializer", + "selector": [ + { + "filenamePattern": "**/*.nbdserializer" + } + ] } ] }, diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.document.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.document.test.ts index b611a83953e..79697de48d8 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.document.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.document.test.ts @@ -9,7 +9,19 @@ import * as utils from '../utils'; suite('Notebook Document', function () { - const contentProvider = new class implements vscode.NotebookContentProvider { + const simpleContentProvider = new class implements vscode.NotebookSerializer { + dataToNotebook(_data: Uint8Array): vscode.NotebookData | Thenable { + return new vscode.NotebookData( + [new vscode.NotebookCellData(vscode.NotebookCellKind.Code, '// SIMPLE', 'javascript')], + new vscode.NotebookDocumentMetadata() + ); + } + notebookToData(_data: vscode.NotebookData): Uint8Array | Thenable { + return new Uint8Array(); + } + }; + + const complexContentProvider = new class implements vscode.NotebookContentProvider { async openNotebook(uri: vscode.Uri, _openContext: vscode.NotebookDocumentOpenContext): Promise { return new vscode.NotebookData( [new vscode.NotebookCellData(vscode.NotebookCellKind.Code, uri.toString(), 'javascript')], @@ -42,13 +54,17 @@ suite('Notebook Document', function () { }); suiteSetup(function () { - disposables.push(vscode.notebook.registerNotebookContentProvider('notebook.nbdtest', contentProvider)); + disposables.push(vscode.notebook.registerNotebookContentProvider('notebook.nbdtest', complexContentProvider)); + disposables.push(vscode.notebook.registerNotebookSerializer('notebook.nbdserializer', simpleContentProvider)); }); test('cannot register sample provider multiple times', function () { assert.throws(() => { - vscode.notebook.registerNotebookContentProvider('notebook.nbdtest', contentProvider); + vscode.notebook.registerNotebookContentProvider('notebook.nbdtest', complexContentProvider); }); + // assert.throws(() => { + // vscode.notebook.registerNotebookSerializer('notebook.nbdserializer', simpleContentProvider); + // }); }); test('cannot open unknown types', async function () { @@ -385,4 +401,34 @@ suite('Notebook Document', function () { assert.deepStrictEqual(document.cells[0].outputs[1].metadata, { outputType: 'stream', streamName: 'stderr' }); assert.deepStrictEqual(document.cells[0].outputs[1].outputs[0].metadata, { outputType: 'stream', streamName: 'stderr' }); }); + + test('dirty state - complex', async function () { + const resource = await utils.createRandomFile(undefined, undefined, '.nbdtest'); + const document = await vscode.notebook.openNotebookDocument(resource); + assert.strictEqual(document.isDirty, false); + + const edit = new vscode.WorkspaceEdit(); + edit.replaceNotebookCells(document.uri, 0, document.getCells().length, []); + assert.ok(await vscode.workspace.applyEdit(edit)); + + assert.strictEqual(document.isDirty, true); + + await document.save(); + assert.strictEqual(document.isDirty, false); + }); + + test('dirty state - serializer', async function () { + const resource = await utils.createRandomFile(undefined, undefined, '.nbdserializer'); + const document = await vscode.notebook.openNotebookDocument(resource); + assert.strictEqual(document.isDirty, false); + + const edit = new vscode.WorkspaceEdit(); + edit.replaceNotebookCells(document.uri, 0, document.getCells().length, []); + assert.ok(await vscode.workspace.applyEdit(edit)); + + assert.strictEqual(document.isDirty, true); + + await document.save(); + assert.strictEqual(document.isDirty, false); + }); }); diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts index ef913f0cf42..8b994acf80e 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -10,8 +10,6 @@ import { Emitter } from 'vs/base/common/event'; import { IRelativePattern } from 'vs/base/common/glob'; import { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; import { ResourceMap } from 'vs/base/common/map'; -import { Schemas } from 'vs/base/common/network'; -import { isEqual } from 'vs/base/common/resources'; import { URI, UriComponents } from 'vs/base/common/uri'; import { EditorActivation, EditorOverride } from 'vs/platform/editor/common/editor'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -29,7 +27,6 @@ import { INotebookEditorModelResolverService } from 'vs/workbench/contrib/notebo import { IMainNotebookController, INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; -import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { ExtHostContext, ExtHostNotebookShape, IExtHostContext, INotebookCellStatusBarEntryDto, INotebookDocumentsAndEditorsDelta, INotebookDocumentShowOptions, INotebookEditorAddData, INotebookModelAddedData, MainContext, MainThreadNotebookShape, NotebookEditorRevealType, NotebookExtensionDescription } from '../common/extHost.protocol'; class NotebookAndEditorState { @@ -119,7 +116,6 @@ export class MainThreadNotebooks implements MainThreadNotebookShape { constructor( extHostContext: IExtHostContext, @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IWorkingCopyService private readonly _workingCopyService: IWorkingCopyService, @INotebookService private readonly _notebookService: INotebookService, @INotebookEditorService private readonly _notebookEditorService: INotebookEditorService, @IEditorService private readonly _editorService: IEditorService, @@ -176,21 +172,13 @@ export class MainThreadNotebooks implements MainThreadNotebookShape { private _registerListeners(): void { - // forward changes to dirty state - // todo@rebornix todo@mjbvz this seem way too complicated... is there an easy way to - // the actual resource from a working copy? - this._disposables.add(this._workingCopyService.onDidChangeDirty(e => { - if (e.resource.scheme !== Schemas.vscodeNotebook) { - return; - } - for (const notebook of this._notebookService.getNotebookTextModels()) { - if (isEqual(notebook.uri.with({ scheme: Schemas.vscodeNotebook }), e.resource)) { - this._proxy.$acceptDirtyStateChanged(notebook.uri, e.isDirty()); - break; - } - } - })); + this._notebookEditorModelResolverService.onDidChangeDirty(model => { + this._proxy.$acceptDirtyStateChanged(model.resource, model.isDirty); + }); + this._disposables.add(this._notebookEditorModelResolverService.onDidSaveNotebook(e => { + this._proxy.$acceptModelSaved(e); + })); this._disposables.add(this._editorService.onDidActiveEditorChange(e => { this._updateState(); @@ -334,9 +322,6 @@ export class MainThreadNotebooks implements MainThreadNotebookShape { this._proxy.$acceptNotebookActiveKernelChange(e); })); - this._disposables.add(this._notebookEditorModelResolverService.onDidSaveNotebook(e => { - this._proxy.$acceptModelSaved(e); - })); const notebookEditor = getNotebookEditorFromEditorPane(this._editorService.activeEditorPane); this._updateState(notebookEditor); diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverService.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverService.ts index 8d433169c1d..52c13ae3031 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverService.ts @@ -14,7 +14,8 @@ export const INotebookEditorModelResolverService = createDecorator; + readonly onDidSaveNotebook: Event; + readonly onDidChangeDirty: Event<{ resource: URI, isDirty: boolean }>; resolve(resource: URI, viewType?: string): Promise>; } diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts index 71a006f6a81..1daf24a0f39 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts @@ -7,7 +7,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { URI } from 'vs/base/common/uri'; import { CellUri, IResolvedNotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ComplexNotebookEditorModel, NotebookFileWorkingCopyModel, NotebookFileWorkingCopyModelFactory, SimpleNotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookEditorModel'; -import { combinedDisposable, DisposableStore, IDisposable, IReference, ReferenceCollection } from 'vs/base/common/lifecycle'; +import { combinedDisposable, DisposableStore, dispose, IDisposable, IReference, ReferenceCollection } 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 { Emitter, Event } from 'vs/base/common/event'; @@ -25,6 +25,9 @@ class NotebookModelReferenceCollection extends ReferenceCollection(); readonly onDidSaveNotebook: Event = this._onDidSaveNotebook.event; + private readonly _onDidChangeDirty = new Emitter<{ resource: URI, isDirty: boolean }>(); + readonly onDidChangeDirty: Event<{ resource: URI, isDirty: boolean }> = this._onDidChangeDirty.event; + constructor( @IInstantiationService readonly _instantiationService: IInstantiationService, @INotebookService private readonly _notebookService: INotebookService, @@ -38,6 +41,13 @@ class NotebookModelReferenceCollection extends ReferenceCollection { const uri = URI.parse(key); const info = await this._notebookService.withNotebookDataProvider(uri, viewType); @@ -56,7 +66,10 @@ class NotebookModelReferenceCollection extends ReferenceCollection this._onDidSaveNotebook.fire(result.resource))); + this._modelListener.set(result, combinedDisposable( + result.onDidSave(() => this._onDidSaveNotebook.fire(result.resource)), + result.onDidChangeDirty(() => this._onDidChangeDirty.fire({ resource: result.resource, isDirty: result.isDirty() })), + )); return result; } @@ -78,6 +91,7 @@ export class NotebookModelResolverServiceImpl implements INotebookEditorModelRes private readonly _data: NotebookModelReferenceCollection; readonly onDidSaveNotebook: Event; + readonly onDidChangeDirty: Event<{ resource: URI, isDirty: boolean }>; constructor( @IInstantiationService instantiationService: IInstantiationService, @@ -87,6 +101,11 @@ export class NotebookModelResolverServiceImpl implements INotebookEditorModelRes ) { this._data = instantiationService.createInstance(NotebookModelReferenceCollection); this.onDidSaveNotebook = this._data.onDidSaveNotebook; + this.onDidChangeDirty = this._data.onDidChangeDirty; + } + + dispose() { + this._data.dispose(); } async resolve(resource: URI, viewType?: string): Promise> {