Code change:
<ins> /*--------------------------------------------------------------------------------------------- *  Copyright (c) Microsoft Corporation. All rights reserved. *  Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; import * as sinon from 'sinon'; import { CancellationTokenSource, Disposable, EventEmitter, ExtensionContext, NotebookCellKind, NotebookDocumentChangeEvent, NotebookDocumentWillSaveEvent, NotebookEdit, NotebookRange, TextDocumentSaveReason, workspace, type CancellationToken, type NotebookCell, type NotebookDocument, type WorkspaceEdit, type WorkspaceEditMetadata } from 'vscode'; import { activate } from '../notebookModelStoreSync'; suite('Notebook Model Store Sync', () => { let disposables: Disposable[] = []; let onDidChangeNotebookDocument: EventEmitter<NotebookDocumentChangeEvent>; let onWillSaveNotebookDocument: AsyncEmitter<NotebookDocumentWillSaveEvent>; let notebook: NotebookDocument; let token: CancellationTokenSource; let editsApplied: WorkspaceEdit[] = []; let pendingPromises: Promise<void>[] = []; let cellMetadataUpdates: NotebookEdit[] = []; let applyEditStub: sinon.SinonStub<[edit: WorkspaceEdit, metadata?: WorkspaceEditMetadata | undefined], Thenable<boolean>>; setup(() => { disposables = []; notebook = { notebookType: '', metadata: {} } as NotebookDocument; token = new CancellationTokenSource(); disposables.push(token); sinon.stub(notebook, 'notebookType').get(() => 'jupyter-notebook'); applyEditStub = sinon.stub(workspace, 'applyEdit').callsFake((edit: WorkspaceEdit) => { editsApplied.push(edit); return Promise.resolve(true); }); const context = { subscriptions: [] as Disposable[] } as ExtensionContext; onDidChangeNotebookDocument = new EventEmitter<NotebookDocumentChangeEvent>(); disposables.push(onDidChangeNotebookDocument); onWillSaveNotebookDocument = new AsyncEmitter<NotebookDocumentWillSaveEvent>(); sinon.stub(NotebookEdit, 'updateCellMetadata').callsFake((index, metadata) => { const edit = (NotebookEdit.updateCellMetadata as any).wrappedMethod.call(NotebookEdit, index, metadata); cellMetadataUpdates.push(edit); return edit; } ); sinon.stub(workspace, 'onDidChangeNotebookDocument').callsFake(cb => onDidChangeNotebookDocument.event(cb) ); sinon.stub(workspace, 'onWillSaveNotebookDocument').callsFake(cb => onWillSaveNotebookDocument.event(cb) ); activate(context); }); teardown(async () => { await Promise.allSettled(pendingPromises); editsApplied = []; pendingPromises = []; cellMetadataUpdates = []; disposables.forEach(d => d.dispose()); disposables = []; sinon.restore(); }); test('Empty cell will not result in any updates', async () => { const e: NotebookDocumentChangeEvent = { notebook, metadata: undefined, contentChanges: [], cellChanges: [] }; onDidChangeNotebookDocument.fire(e); assert.strictEqual(editsApplied.length, 0); }); test('Adding cell for non Jupyter Notebook will not result in any updates', async () => { sinon.stub(notebook, 'notebookType').get(() => 'some-other-type'); const cell: NotebookCell = { document: {} as any, executionSummary: {}, index: 0, kind: NotebookCellKind.Code, metadata: {}, notebook, outputs: [] }; const e: NotebookDocumentChangeEvent = { notebook, metadata: undefined, contentChanges: [ { range: new NotebookRange(0, 0), removedCells: [], addedCells: [cell] } ], cellChanges: [] }; onDidChangeNotebookDocument.fire(e); assert.strictEqual(editsApplied.length, 0); assert.strictEqual(cellMetadataUpdates.length, 0); }); test('Adding cell will result in an update to the metadata', async () => { const cell: NotebookCell = { document: {} as any, executionSummary: {}, index: 0, kind: NotebookCellKind.Code, metadata: {}, notebook, outputs: [] }; const e: NotebookDocumentChangeEvent = { notebook, metadata: undefined, contentChanges: [ { range: new NotebookRange(0, 0), removedCells: [], addedCells: [cell] } ], cellChanges: [] }; onDidChangeNotebookDocument.fire(e); assert.strictEqual(editsApplied.length, 1); assert.strictEqual(cellMetadataUpdates.length, 1); const newMetadata = cellMetadataUpdates[0].newCellMetadata; assert.deepStrictEqual(newMetadata, { custom: { metadata: {} } }); }); test('Add cell id if nbformat is 4.5', async () => { sinon.stub(notebook, 'metadata').get(() => ({ custom: { nbformat: 4, nbformat_minor: 5 } })); const cell: NotebookCell = { document: {} as any, executionSummary: {}, index: 0, kind: NotebookCellKind.Code, metadata: {}, notebook, outputs: [] }; const e: NotebookDocumentChangeEvent = { notebook, metadata: undefined, contentChanges: [ { range: new NotebookRange(0, 0), removedCells: [], addedCells: [cell] } ], cellChanges: [] }; onDidChangeNotebookDocument.fire(e); assert.strictEqual(editsApplied.length, 1); assert.strictEqual(cellMetadataUpdates.length, 1); const newMetadata = cellMetadataUpdates[0].newCellMetadata || {}; assert.strictEqual(Object.keys(newMetadata).length, 1); assert.strictEqual(Object.keys(newMetadata.custom).length, 2); assert.deepStrictEqual(newMetadata.custom.metadata, {}); assert.ok(newMetadata.custom.id); }); test('Do not add cell id if one already exists', async () => { sinon.stub(notebook, 'metadata').get(() => ({ custom: { nbformat: 4, nbformat_minor: 5 } })); const cell: NotebookCell = { document: {} as any, executionSummary: {}, index: 0, kind: NotebookCellKind.Code, metadata: { custom: { id: '1234' } }, notebook, outputs: [] }; const e: NotebookDocumentChangeEvent = { notebook, metadata: undefined, contentChanges: [ { range: new NotebookRange(0, 0), removedCells: [], addedCells: [cell] } ], cellChanges: [] }; onDidChangeNotebookDocument.fire(e); assert.strictEqual(editsApplied.length, 1); assert.strictEqual(cellMetadataUpdates.length, 1); const newMetadata = cellMetadataUpdates[0].newCellMetadata || {}; assert.strictEqual(Object.keys(newMetadata).length, 1); assert.strictEqual(Object.keys(newMetadata.custom).length, 2); assert.deepStrictEqual(newMetadata.custom.metadata, {}); assert.strictEqual(newMetadata.custom.id, '1234'); }); test('Do not perform any updates if cell id and metadata exists', async () => { sinon.stub(notebook, 'metadata').get(() => ({ custom: { nbformat: 4, nbformat_minor: 5 } })); const cell: NotebookCell = { document: {} as any, executionSummary: {}, index: 0, kind: NotebookCellKind.Code, metadata: { custom: { id: '1234', metadata: {} } }, notebook, outputs: [] }; const e: NotebookDocumentChangeEvent = { notebook, metadata: undefined, contentChanges: [ { range: new NotebookRange(0, 0), removedCells: [], addedCells: [cell] } ], cellChanges: [] }; onDidChangeNotebookDocument.fire(e); assert.strictEqual(editsApplied.length, 0); assert.strictEqual(cellMetadataUpdates.length, 0); }); test('Store language id in custom metadata, whilst preserving existing metadata', async () => { sinon.stub(notebook, 'metadata').get(() => ({ custom: { nbformat: 4, nbformat_minor: 5, metadata: { language_info: { name: 'python' } } } })); const cell: NotebookCell = { document: { languageId: 'javascript' } as any, executionSummary: {}, index: 0, kind: NotebookCellKind.Code, metadata: { custom: { id: '1234', metadata: { collapsed: true, scrolled: true } } }, notebook, outputs: [] }; const e: NotebookDocumentChangeEvent = { notebook, metadata: undefined, contentChanges: [], cellChanges: [ { cell, document: undefined, metadata: undefined, outputs: undefined, executionSummary: undefined } ] }; onDidChangeNotebookDocument.fire(e); assert.strictEqual(editsApplied.length, 1); assert.strictEqual(cellMetadataUpdates.length, 1); const newMetadata = cellMetadataUpdates[0].newCellMetadata || {}; assert.strictEqual(Object.keys(newMetadata).length, 1); assert.strictEqual(Object.keys(newMetadata.custom).length, 2); assert.deepStrictEqual(newMetadata.custom.metadata, { collapsed: true, scrolled: true, vscode: { languageId: 'javascript' } }); assert.strictEqual(newMetadata.custom.id, '1234'); }); test('No changes when language is javascript', async () => { sinon.stub(notebook, 'metadata').get(() => ({ custom: { nbformat: 4, nbformat_minor: 5, metadata: { language_info: { name: 'javascript' } } } })); const cell: NotebookCell = { document: { languageId: 'javascript' } as any, executionSummary: {}, index: 0, kind: NotebookCellKind.Code, metadata: { custom: { id: '1234', metadata: { collapsed: true, scrolled: true } } }, notebook, outputs: [] }; const e: NotebookDocumentChangeEvent = { notebook, metadata: undefined, contentChanges: [], cellChanges: [ { cell, document: undefined, metadata: undefined, outputs: undefined, executionSummary: undefined } ] }; onDidChangeNotebookDocument.fire(e); assert.strictEqual(editsApplied.length, 0); assert.strictEqual(cellMetadataUpdates.length, 0); }); test('Remove language from metadata when cell language matches kernel language', async () => { sinon.stub(notebook, 'metadata').get(() => ({ custom: { nbformat: 4, nbformat_minor: 5, metadata: { language_info: { name: 'javascript' } } } })); const cell: NotebookCell = { document: { languageId: 'javascript' } as any, executionSummary: {}, index: 0, kind: NotebookCellKind.Code, metadata: { custom: { id: '1234', metadata: { vscode: { languageId: 'python' }, collapsed: true, scrolled: true } } }, notebook, outputs: [] }; const e: NotebookDocumentChangeEvent = { notebook, metadata: undefined, contentChanges: [], cellChanges: [ { cell, document: undefined, metadata: undefined, outputs: undefined, executionSummary: undefined } ] }; onDidChangeNotebookDocument.fire(e); assert.strictEqual(editsApplied.length, 1); assert.strictEqual(cellMetadataUpdates.length, 1); const newMetadata = cellMetadataUpdates[0].newCellMetadata || {}; assert.strictEqual(Object.keys(newMetadata).length, 1); assert.strictEqual(Object.keys(newMetadata.custom).length, 2); assert.deepStrictEqual(newMetadata.custom.metadata, { collapsed: true, scrolled: true }); assert.strictEqual(newMetadata.custom.id, '1234'); }); test('Update language in metadata', async () => { sinon.stub(notebook, 'metadata').get(() => ({ custom: { nbformat: 4, nbformat_minor: 5, metadata: { language_info: { name: 'javascript' } } } })); const cell: NotebookCell = { document: { languageId: 'powershell' } as any, executionSummary: {}, index: 0, kind: NotebookCellKind.Code, metadata: { custom: { id: '1234', metadata: { vscode: { languageId: 'python' }, collapsed: true, scrolled: true } } }, notebook, outputs: [] }; const e: NotebookDocumentChangeEvent = { notebook, metadata: undefined, contentChanges: [], cellChanges: [ { cell, document: undefined, metadata: undefined, outputs: undefined, executionSummary: undefined } ] }; onDidChangeNotebookDocument.fire(e); assert.strictEqual(editsApplied.length, 1); assert.strictEqual(cellMetadataUpdates.length, 1); const newMetadata = cellMetadataUpdates[0].newCellMetadata || {}; assert.strictEqual(Object.keys(newMetadata).length, 1); assert.strictEqual(Object.keys(newMetadata.custom).length, 2); assert.deepStrictEqual(newMetadata.custom.metadata, { collapsed: true, scrolled: true, vscode: { languageId: 'powershell' } }); assert.strictEqual(newMetadata.custom.id, '1234'); }); test('Will save event without any changes', async () => { await onWillSaveNotebookDocument.fireAsync({ notebook, reason: TextDocumentSaveReason.Manual }, token.token); }); test('Wait for pending updates to complete when saving', async () => { let resolveApplyEditPromise: (value: boolean) => void; const promise = new Promise<boolean>((resolve) => resolveApplyEditPromise = resolve); applyEditStub.restore(); sinon.stub(workspace, 'applyEdit').callsFake((edit: WorkspaceEdit) => { editsApplied.push(edit); return promise; }); const cell: NotebookCell = { document: {} as any, executionSummary: {}, index: 0, kind: NotebookCellKind.Code, metadata: {}, notebook, outputs: [] }; const e: NotebookDocumentChangeEvent = { notebook, metadata: undefined, contentChanges: [ { range: new NotebookRange(0, 0), removedCells: [], addedCells: [cell] } ], cellChanges: [] }; onDidChangeNotebookDocument.fire(e); assert.strictEqual(editsApplied.length, 1); assert.strictEqual(cellMetadataUpdates.length, 1); // Try to save. let saveCompleted = false; const saved = onWillSaveNotebookDocument.fireAsync({ notebook, reason: TextDocumentSaveReason.Manual }, token.token); saved.finally(() => saveCompleted = true); await new Promise((resolve) => setTimeout(resolve, 10)); // Verify we have not yet completed saving. assert.strictEqual(saveCompleted, false); resolveApplyEditPromise!(true); await new Promise((resolve) => setTimeout(resolve, 1)); // Should have completed saving. saved.finally(() => saveCompleted = true); }); interface IWaitUntil { token: CancellationToken; waitUntil(thenable: Promise<unknown>): void; } interface IWaitUntil { token: CancellationToken; waitUntil(thenable: Promise<unknown>): void; } type IWaitUntilData<T> = Omit<Omit<T, 'waitUntil'>, 'token'>; class AsyncEmitter<T extends IWaitUntil> { private listeners: ((d: T) => void)[] = []; get event(): (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]) => Disposable { return (listener, thisArgs, _disposables) => { this.listeners.push(listener.bind(thisArgs)); return { dispose: () => { // } }; }; } dispose() { this.listeners = []; } async fireAsync(data: IWaitUntilData<T>, token: CancellationToken): Promise<void> { if (!this.listeners.length) { return; } const promises: Promise<unknown>[] = []; this.listeners.forEach(cb => { const event = { ...data, token, waitUntil: (thenable: Promise<WorkspaceEdit>) => { promises.push(thenable); } } as T; cb(event); }); await Promise.all(promises); } } }); </ins>

Related discussion:
text: <details<summaryInternal Notes</summaryHere's the work flow Add a cell A change event is triggered, undo redo stack is updated Last undo redo is User hits save button Working copy starts to handle the save operation Save participants start We add a save participant and update the model via a work space edit Now a new change event is triggered This results in a whole new undo redo stack The latest (last) undo redo item is Save participants done Working copy keeps track of the last as the entry for Problems Time line does not include , time line shows only I guess this is correct, However from a users perspective, the labels are never going to be meaningful as we'd always say . </details