From 7b96cc4c8baecded235d96262a4ea5d10017f4a7 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Mon, 22 Mar 2021 11:27:00 -0700 Subject: [PATCH] Implement Execution API (#116416) Implement new execution task API #106744 Fix #105847 --- .../src/singlefolder-tests/notebook.test.ts | 463 ++++++++++++++---- .../vscode-notebook-tests/src/extension.ts | 30 +- src/vs/vscode.proposed.d.ts | 152 ++++-- .../api/browser/mainThreadNotebook.ts | 24 +- .../workbench/api/common/extHost.api.impl.ts | 11 +- .../workbench/api/common/extHost.protocol.ts | 8 +- .../workbench/api/common/extHostNotebook.ts | 259 ++++++++-- .../api/common/extHostNotebookDocument.ts | 34 +- .../api/common/extHostNotebookEditor.ts | 6 +- .../api/common/extHostTypeConverters.ts | 29 +- src/vs/workbench/api/common/extHostTypes.ts | 69 +-- .../notebook/browser/contrib/coreActions.ts | 33 +- .../notebook/browser/diff/diffComponents.ts | 15 - .../notebook/browser/notebookBrowser.ts | 6 +- .../browser/notebookEditorKernelManager.ts | 97 +++- .../notebook/browser/notebookEditorWidget.ts | 3 +- .../browser/view/renderers/cellContextKeys.ts | 24 +- .../browser/view/renderers/cellRenderer.ts | 42 +- .../common/model/notebookTextModel.ts | 78 ++- .../contrib/notebook/common/notebookCommon.ts | 58 ++- .../test/notebookEditorKernelManager.test.ts | 18 +- .../notebook/test/notebookTextModel.test.ts | 34 ++ .../test/browser/api/extHostNotebook.test.ts | 28 +- .../test/browser/api/extHostTypes.test.ts | 19 - 24 files changed, 1100 insertions(+), 440 deletions(-) diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.test.ts index 027370243b0..ab2739099dd 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.test.ts @@ -53,79 +53,106 @@ async function withEvent(event: vscode.Event, callback: (e: Promise) => await callback(e); } -const kernel1: vscode.NotebookKernel = { - id: 'mainKernel', - label: 'Notebook Test Kernel', - isPreferred: true, - supportedLanguages: ['typescript', 'javascript'], - executeAllCells: async (_document: vscode.NotebookDocument) => { - const edit = new vscode.WorkspaceEdit(); +const kernel1 = new class implements vscode.NotebookKernel { + readonly id = 'mainKernel'; + readonly label = 'Notebook Test Kernel'; + readonly isPreferred = true; + readonly supportedLanguages = ['typescript', 'javascript']; - edit.replaceNotebookCellOutput(_document.uri, 0, [new vscode.NotebookCellOutput([ - new vscode.NotebookCellOutputItem('text/plain', ['my output'], undefined) - ])]); - return vscode.workspace.applyEdit(edit); - }, - cancelAllCellsExecution: async (_document: vscode.NotebookDocument) => { }, - executeCell: async (document: vscode.NotebookDocument, cell: vscode.NotebookCell | undefined) => { - if (!cell) { - cell = document.cells[0]; + async executeCellsRequest(document: vscode.NotebookDocument, ranges: vscode.NotebookCellRange[]) { + if (ranges.length > 1 || ranges[0].start + 1 < ranges[0].end) { + // Keeping same behavior... if the full notebook is executed, just execute the first cell + const task = vscode.notebook.createNotebookCellExecutionTask(document.uri, 0, 'mainKernel'); + if (!task) { + return; + } + + task.start(); + await task.replaceOutput([new vscode.NotebookCellOutput([ + new vscode.NotebookCellOutputItem('text/plain', ['my output'], undefined) + ])]); + task.end({ success: true }); + return; } + for (let range of ranges) { + for (let i = range.start; i < range.end; i++) { + await this.runCell(document, i); + } + } + } + + private async runCell(document: vscode.NotebookDocument, idx: number) { + const task = vscode.notebook.createNotebookCellExecutionTask(document.uri, idx, 'mainKernel'); + if (!task) { + return; + } + + task.start(); + task.executionOrder = 1; if (document.uri.path.endsWith('customRenderer.vsctestnb')) { - const edit = new vscode.WorkspaceEdit(); - edit.replaceNotebookCellOutput(document.uri, cell.index, [new vscode.NotebookCellOutput([ + await task.replaceOutput([new vscode.NotebookCellOutput([ new vscode.NotebookCellOutputItem('text/custom', ['test'], undefined) ])]); - - return vscode.workspace.applyEdit(edit); + return; } - const edit = new vscode.WorkspaceEdit(); - // const previousOutputs = cell.outputs; - edit.replaceNotebookCellOutput(document.uri, cell.index, [new vscode.NotebookCellOutput([ + await task.replaceOutput([new vscode.NotebookCellOutput([ new vscode.NotebookCellOutputItem('text/plain', ['my output'], undefined) ])]); - - return vscode.workspace.applyEdit(edit); - }, - cancelCellExecution: async (_document: vscode.NotebookDocument, _cell: vscode.NotebookCell) => { } + task.end({ success: true }); + } }; -const kernel2: vscode.NotebookKernel = { - id: 'secondaryKernel', - label: 'Notebook Secondary Test Kernel', - isPreferred: false, - supportedLanguages: ['typescript', 'javascript'], - executeAllCells: async (_document: vscode.NotebookDocument) => { - const edit = new vscode.WorkspaceEdit(); - edit.replaceNotebookCellOutput(_document.uri, 0, [new vscode.NotebookCellOutput([ - new vscode.NotebookCellOutputItem('text/plain', ['my second output'], undefined) - ])]); +const kernel2 = new class implements vscode.NotebookKernel { + readonly id = 'secondaryKernel'; + readonly label = 'Notebook Secondary Test Kernel'; + readonly isPreferred = false; + readonly supportedLanguages = ['typescript', 'javascript']; - return vscode.workspace.applyEdit(edit); - }, - cancelAllCellsExecution: async (_document: vscode.NotebookDocument) => { }, - executeCell: async (document: vscode.NotebookDocument, cell: vscode.NotebookCell | undefined) => { - if (!cell) { - cell = document.cells[0]; - } + async executeCellsRequest(document: vscode.NotebookDocument, ranges: vscode.NotebookCellRange[]) { + if (ranges.length > 1 || ranges[0].start + 1 < ranges[0].end) { + // Keeping same behavior... if the full notebook is executed, just execute the first cell + const task = vscode.notebook.createNotebookCellExecutionTask(document.uri, 0, 'secondaryKernel'); + if (!task) { + return; + } - const edit = new vscode.WorkspaceEdit(); - - if (document.uri.path.endsWith('customRenderer.vsctestnb')) { - edit.replaceNotebookCellOutput(document.uri, cell.index, [new vscode.NotebookCellOutput([ - new vscode.NotebookCellOutputItem('text/custom', ['test 2'], undefined) - ])]); - } else { - edit.replaceNotebookCellOutput(document.uri, cell.index, [new vscode.NotebookCellOutput([ + task.start(); + await task.replaceOutput([new vscode.NotebookCellOutput([ new vscode.NotebookCellOutputItem('text/plain', ['my second output'], undefined) ])]); + task.end({ success: true }); + return; } - return vscode.workspace.applyEdit(edit); - }, - cancelCellExecution: async (_document: vscode.NotebookDocument, _cell: vscode.NotebookCell) => { } + for (let range of ranges) { + for (let i = range.start; i < range.end; i++) { + await this.runCell(document, i); + } + } + } + + private async runCell(document: vscode.NotebookDocument, idx: number) { + const task = vscode.notebook.createNotebookCellExecutionTask(document.uri, idx, 'mainKernel'); + if (!task) { + return; + } + + task.start(); + if (document.uri.path.endsWith('customRenderer.vsctestnb')) { + task.replaceOutput([new vscode.NotebookCellOutput([ + new vscode.NotebookCellOutputItem('text/custom', ['test 2'], undefined) + ])]); + task.end({ success: true }); + return; + } + + await task.replaceOutput([new vscode.NotebookCellOutput([ + new vscode.NotebookCellOutputItem('text/plain', ['my second output'], undefined) + ])]); + task.end({ success: true }); + } }; class KernelProvider implements vscode.NotebookKernelProvider { @@ -133,7 +160,12 @@ class KernelProvider implements vscode.NotebookKernelProvider { onDidChangeKernels = this._onDidChangeKernels.event; private _hasKernels = true; - private readonly _kernels = [kernel1, kernel2]; + private readonly _kernels: vscode.NotebookKernel[] = [kernel1, kernel2]; + + addKernel(kernel: vscode.NotebookKernel): void { + this._kernels.push(kernel); + this._onDidChangeKernels.fire(undefined); + } provideKernels(): vscode.ProviderResult { return this._hasKernels ? this._kernels : []; @@ -144,11 +176,13 @@ class KernelProvider implements vscode.NotebookKernelProvider { this._onDidChangeKernels.fire(undefined); } } -let currentKernerProvider: KernelProvider; + +let currentKernelProvider: KernelProvider; suite('Notebook API tests', function () { - const disposables: vscode.Disposable[] = []; + const testDisposables: vscode.Disposable[] = []; + const suiteDisposables: vscode.Disposable[] = []; suiteTeardown(async function () { @@ -157,12 +191,12 @@ suite('Notebook API tests', function () { await revertAllDirty(); await closeAllEditors(); - disposeAll(disposables); - disposables.length = 0; + disposeAll(suiteDisposables); + suiteDisposables.length = 0; }); suiteSetup(function () { - disposables.push(vscode.notebook.registerNotebookContentProvider('notebookCoreTest', { + suiteDisposables.push(vscode.notebook.registerNotebookContentProvider('notebookCoreTest', { openNotebook: async (_resource: vscode.Uri): Promise => { if (/.*empty\-.*\.vsctestnb$/.test(_resource.path)) { return { @@ -191,6 +225,7 @@ suite('Notebook API tests', function () { ], { testOutputMetadata: true }) ], + previousResult: { executionOrder: 5, success: true }, metadata: new vscode.NotebookCellMetadata().with({ custom: { testCellMetadata: 456 } }) } ] @@ -213,9 +248,16 @@ suite('Notebook API tests', function () { }; } })); + }); - currentKernerProvider = new KernelProvider(); - disposables.push(vscode.notebook.registerNotebookKernelProvider({ filenamePattern: '*.vsctestnb' }, currentKernerProvider)); + setup(() => { + currentKernelProvider = new KernelProvider(); + testDisposables.push(vscode.notebook.registerNotebookKernelProvider({ filenamePattern: '*.vsctestnb' }, currentKernelProvider)); + }); + + teardown(() => { + disposeAll(testDisposables); + testDisposables.length = 0; }); test('shared document in notebook editors', async function () { @@ -391,12 +433,11 @@ suite('Notebook API tests', function () { await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); await vscode.window.activeNotebookEditor!.edit(editBuilder => { - editBuilder.replaceCellMetadata(0, new vscode.NotebookCellMetadata().with({ inputCollapsed: true, executionOrder: 17 })); + editBuilder.replaceCellMetadata(0, new vscode.NotebookCellMetadata().with({ inputCollapsed: true })); }); const document = vscode.window.activeNotebookEditor?.document!; assert.strictEqual(document.cells.length, 2); - assert.strictEqual(document.cells[0].metadata.executionOrder, 17); assert.strictEqual(document.cells[0].metadata.inputCollapsed, true); assert.strictEqual(document.isDirty, true); @@ -410,12 +451,11 @@ suite('Notebook API tests', function () { const event = asPromise(vscode.notebook.onDidChangeCellMetadata); await vscode.window.activeNotebookEditor!.edit(editBuilder => { - editBuilder.replaceCellMetadata(0, new vscode.NotebookCellMetadata().with({ inputCollapsed: true, executionOrder: 17 })); + editBuilder.replaceCellMetadata(0, new vscode.NotebookCellMetadata().with({ inputCollapsed: true })); }); const data = await event; assert.strictEqual(data.document, vscode.window.activeNotebookEditor?.document); - assert.strictEqual(data.cell.metadata.executionOrder, 17); assert.strictEqual(data.cell.metadata.inputCollapsed, true); assert.strictEqual(data.document.isDirty, true); @@ -499,6 +539,8 @@ suite('Notebook API tests', function () { assert.strictEqual(secondCell!.outputs[0].outputs[0].mime, 'text/plain'); assert.strictEqual(secondCell!.outputs[0].outputs[0].value, 'Hello World'); assert.deepStrictEqual(secondCell!.outputs[0].outputs[0].metadata, { testOutputItemMetadata: true }); + assert.strictEqual(secondCell!.previousResult?.executionOrder, 5); + assert.strictEqual(secondCell!.previousResult?.success, true); await vscode.commands.executeCommand('notebook.cell.insertCodeCellBelow'); assert.strictEqual(vscode.window.activeNotebookEditor!.selection?.document.getText(), ''); @@ -637,11 +679,11 @@ suite('Notebook API tests', function () { const cell = editor.document.cells[0]; assert.strictEqual(cell.outputs.length, 0); - currentKernerProvider.setHasKernels(false); + currentKernelProvider.setHasKernels(false); await vscode.commands.executeCommand('notebook.execute'); assert.strictEqual(cell.outputs.length, 0, 'should not execute'); // not runnable, didn't work - currentKernerProvider.setHasKernels(true); + currentKernelProvider.setHasKernels(true); await withEvent(vscode.notebook.onDidChangeCellOutputs, async (event) => { await vscode.commands.executeCommand('notebook.execute'); @@ -737,26 +779,158 @@ suite('Notebook API tests', function () { const editor = vscode.window.activeNotebookEditor!; const cell = editor.document.cells[0]; - await vscode.commands.executeCommand('notebook.cell.execute'); - assert.strictEqual(cell.outputs.length, 1, 'should execute'); // runnable, it worked - assert.strictEqual(cell.outputs[0].outputs.length, 1); - assert.strictEqual(cell.outputs[0].outputs[0].mime, 'text/plain'); - assert.deepStrictEqual(cell.outputs[0].outputs[0].value, [ - 'my output' - ]); + vscode.commands.executeCommand('notebook.cell.execute'); + await withEvent(vscode.notebook.onDidChangeCellOutputs, async (event) => { + await event; + assert.strictEqual(cell.outputs.length, 1, 'should execute'); // runnable, it worked + assert.strictEqual(cell.outputs[0].outputs.length, 1); + assert.strictEqual(cell.outputs[0].outputs[0].mime, 'text/plain'); + assert.deepStrictEqual(cell.outputs[0].outputs[0].value, [ + 'my output' + ]); + }); await vscode.commands.executeCommand('notebook.selectKernel', { extension: 'vscode.vscode-api-tests', id: 'secondaryKernel' }); - await vscode.commands.executeCommand('notebook.cell.execute'); - assert.strictEqual(cell.outputs.length, 1, 'should execute'); // runnable, it worked - assert.strictEqual(cell.outputs[0].outputs.length, 1); - assert.strictEqual(cell.outputs[0].outputs[0].mime, 'text/plain'); - assert.deepStrictEqual(cell.outputs[0].outputs[0].value, [ - 'my second output' - ]); + vscode.commands.executeCommand('notebook.cell.execute'); + await withEvent(vscode.notebook.onDidChangeCellOutputs, async (event) => { + await event; + assert.strictEqual(cell.outputs.length, 1, 'should execute'); // runnable, it worked + assert.strictEqual(cell.outputs[0].outputs.length, 1); + assert.strictEqual(cell.outputs[0].outputs[0].mime, 'text/plain'); + assert.deepStrictEqual(cell.outputs[0].outputs[0].value, [ + 'my second output' + ]); + }); await saveAllFilesAndCloseAll(undefined); }); - // }); + + test('set outputs on cancel', async () => { + const cancelableKernel = new class implements vscode.NotebookKernel { + readonly id = 'cancelableKernel'; + readonly label = 'Notebook Cancelable Test Kernel'; + readonly isPreferred = false; + readonly supportedLanguages = ['typescript', 'javascript']; + + async executeCellsRequest(document: vscode.NotebookDocument, ranges: vscode.NotebookCellRange[]) { + const idx = ranges[0].start; + + const task = vscode.notebook.createNotebookCellExecutionTask(document.uri, idx, 'cancelableKernel'); + if (!task) { + return; + } + + task.start(); + task.token.onCancellationRequested(async () => { + await task.replaceOutput([new vscode.NotebookCellOutput([ + new vscode.NotebookCellOutputItem('text/plain', ['Canceled'], undefined) + ])]); + task.end({}); + }); + } + }; + + currentKernelProvider.addKernel(cancelableKernel); + const resource = await createRandomFile('', undefined, '.vsctestnb'); + await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + const editor = vscode.window.activeNotebookEditor!; + const cell = editor.document.cells[0]; + + await vscode.commands.executeCommand('notebook.selectKernel', { extension: 'vscode.vscode-api-tests', id: cancelableKernel.id }); + await withEvent(vscode.notebook.onDidChangeCellOutputs, async (event) => { + await vscode.commands.executeCommand('notebook.cell.execute'); + await vscode.commands.executeCommand('notebook.cell.cancelExecution'); + await event; + assert.strictEqual(cell.outputs.length, 1, 'should execute'); // runnable, it worked + assert.strictEqual(cell.outputs[0].outputs.length, 1); + assert.strictEqual(cell.outputs[0].outputs[0].mime, 'text/plain'); + assert.deepStrictEqual(cell.outputs[0].outputs[0].value, [ + 'Canceled' + ]); + }); + + await saveAllFilesAndCloseAll(undefined); + }); + + test('set outputs on interrupt', async () => { + const interruptableKernel = new class implements vscode.NotebookKernel { + readonly id = 'interruptableKernel'; + readonly label = 'Notebook Interruptable Test Kernel'; + readonly isPreferred = false; + readonly supportedLanguages = ['typescript', 'javascript']; + + private _task: vscode.NotebookCellExecutionTask | undefined; + + async executeCellsRequest(document: vscode.NotebookDocument, ranges: vscode.NotebookCellRange[]) { + const idx = ranges[0].start; + + this._task = vscode.notebook.createNotebookCellExecutionTask(document.uri, idx, 'interruptableKernel'); + if (!this._task) { + return; + } + + this._task.start(); + } + + async interrupt(_document: vscode.NotebookDocument, _ranges: vscode.NotebookCellRange[]) { + await this._task!.replaceOutput([new vscode.NotebookCellOutput([ + new vscode.NotebookCellOutputItem('text/plain', ['Interrupted'], undefined) + ])]); + this._task!.end({}); + } + }; + + currentKernelProvider.addKernel(interruptableKernel); + const resource = await createRandomFile('', undefined, '.vsctestnb'); + await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + const editor = vscode.window.activeNotebookEditor!; + const cell = editor.document.cells[0]; + + await vscode.commands.executeCommand('notebook.selectKernel', { extension: 'vscode.vscode-api-tests', id: interruptableKernel.id }); + await withEvent(vscode.notebook.onDidChangeCellOutputs, async (event) => { + await vscode.commands.executeCommand('notebook.cell.execute'); + await vscode.commands.executeCommand('notebook.cell.cancelExecution'); + await event; + assert.strictEqual(cell.outputs.length, 1, 'should execute'); // runnable, it worked + assert.strictEqual(cell.outputs[0].outputs.length, 1); + assert.strictEqual(cell.outputs[0].outputs[0].mime, 'text/plain'); + assert.deepStrictEqual(cell.outputs[0].outputs[0].value, [ + 'Interrupted' + ]); + }); + + await saveAllFilesAndCloseAll(undefined); + }); + + test('onDidChangeCellExecutionState is fired', async () => { + const resource = await createRandomFile('', undefined, '.vsctestnb'); + await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + const editor = vscode.window.activeNotebookEditor!; + const cell = editor.document.cells[0]; + + vscode.commands.executeCommand('notebook.cell.execute'); + let eventCount = 0; + let resolve: () => void; + const p = new Promise(r => resolve = r); + const listener = vscode.notebook.onDidChangeCellExecutionState(e => { + if (eventCount === 0) { + assert.strictEqual(e.executionState, vscode.NotebookCellExecutionState.Pending, 'should be set to Pending'); + } else if (eventCount === 1) { + assert.strictEqual(e.executionState, vscode.NotebookCellExecutionState.Executing, 'should be set to Executing'); + assert.strictEqual(cell.outputs.length, 0, 'no outputs yet: ' + JSON.stringify(cell.outputs[0])); + } else if (eventCount === 2) { + assert.strictEqual(e.executionState, vscode.NotebookCellExecutionState.Idle, 'should be set to Idle'); + assert.strictEqual(cell.outputs.length, 1, 'should have an output'); + resolve(); + } + + eventCount++; + }); + + await p; + listener.dispose(); + await saveAllFilesAndCloseAll(undefined); + }); // suite('notebook dirty state', () => { test('notebook open', async function () { @@ -1094,7 +1268,6 @@ suite('Notebook API tests', function () { await saveAllFilesAndCloseAll(resource); }); - test('#116808, active kernel should not be undefined', async function () { const resource = await createRandomFile('', undefined, '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); @@ -1108,32 +1281,110 @@ suite('Notebook API tests', function () { await saveAllFilesAndCloseAll(resource); }); - test('Numeric metadata should get updated correctly', async function () { + test('Output changes are applied once the promise resolves', async function () { + const verifyOutputSyncKernel = new class implements vscode.NotebookKernel { + readonly id = 'verifyOutputSyncKernel'; + readonly label = ''; + readonly isPreferred = false; + readonly supportedLanguages = ['typescript', 'javascript']; + + async executeCellsRequest(document: vscode.NotebookDocument, ranges: vscode.NotebookCellRange[]) { + const idx = ranges[0].start; + + const task = vscode.notebook.createNotebookCellExecutionTask(document.uri, idx, this.id); + if (!task) { + return; + } + + task.start(); + await task.replaceOutput([new vscode.NotebookCellOutput([ + new vscode.NotebookCellOutputItem('text/plain', ['Some output'], undefined) + ])]); + assert.strictEqual(document.cells[0].outputs.length, 1); + assert.deepStrictEqual(document.cells[0].outputs[0].outputs[0].value, ['Some output']); + task.end({}); + } + }; + + currentKernelProvider.addKernel(verifyOutputSyncKernel); + const resource = await createRandomFile('', undefined, '.vsctestnb'); - const document = await vscode.notebook.openNotebookDocument(resource); + await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + await vscode.commands.executeCommand('notebook.selectKernel', { extension: 'vscode.vscode-api-tests', id: verifyOutputSyncKernel.id }); + await vscode.commands.executeCommand('notebook.cell.execute'); - const edit = new vscode.WorkspaceEdit(); - const runStartTime = Date.now(); - const lastRunDuration = Date.now() + 1000; - const runState = vscode.NotebookCellRunState.Success; - const executionOrder = 1234; - const metadata = document.cells[0].metadata.with({ - ...document.cells[0].metadata, - runStartTime, - runState, - lastRunDuration, - executionOrder - }); - edit.replaceNotebookCellMetadata(document.uri, 0, metadata); - await vscode.workspace.applyEdit(edit); - - assert.strictEqual(document.cells[0].metadata.runStartTime, runStartTime); - assert.strictEqual(document.cells[0].metadata.lastRunDuration, lastRunDuration); - assert.strictEqual(document.cells[0].metadata.executionOrder, executionOrder); - assert.strictEqual(document.cells[0].metadata.runState, vscode.NotebookCellRunState.Success); + await saveAllFilesAndCloseAll(undefined); }); + test('previousResult', async () => { + const resource = await createRandomFile('', undefined, '.vsctestnb'); + await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + const editor = vscode.window.activeNotebookEditor!; + const cell = editor.document.cells[0]; + assert.strictEqual(cell.previousResult?.success, undefined); + assert.strictEqual(cell.previousResult?.executionOrder, undefined); + await vscode.commands.executeCommand('notebook.cell.execute'); + assert.strictEqual(cell.outputs.length, 1, 'should execute'); + assert.ok(cell.previousResult); + assert.strictEqual(cell.previousResult!.success, true); + assert.strictEqual(typeof cell.previousResult!.executionOrder, 'number'); + + await saveAllFilesAndCloseAll(undefined); + }); + + test('initialize previousResult', async () => { + const resource = await createRandomFile('', undefined, '.vsctestnb'); + await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + const editor = vscode.window.activeNotebookEditor!; + const cell = editor.document.cells[0]; + + assert.strictEqual(cell.previousResult?.success, undefined); + assert.strictEqual(cell.previousResult?.executionOrder, undefined); + await vscode.commands.executeCommand('notebook.cell.execute'); + assert.strictEqual(cell.outputs.length, 1, 'should execute'); + assert.ok(cell.previousResult); + assert.strictEqual(cell.previousResult!.success, true); + assert.strictEqual(typeof cell.previousResult!.executionOrder, 'number'); + + await saveAllFilesAndCloseAll(undefined); + }); + + test('Throws errors for invalid execution tasks', async function () { + let missedError: string | undefined; + + const invalidKernel = new class implements vscode.NotebookKernel { + readonly id = 'invalidKernel'; + readonly label = ''; + readonly isPreferred = false; + readonly supportedLanguages = ['typescript', 'javascript']; + + async executeCellsRequest(document: vscode.NotebookDocument, _ranges: vscode.NotebookCellRange[]) { + try { + vscode.notebook.createNotebookCellExecutionTask(document.uri, 1000, this.id); + missedError = 'Expected to throw for invalid index'; + return; + } catch (e) { } + + try { + vscode.notebook.createNotebookCellExecutionTask(vscode.Uri.file('slkdf'), 0, this.id); + missedError = 'Expected to throw for invalid uri'; + return; + } catch (e) { } + } + }; + + currentKernelProvider.addKernel(invalidKernel); + + const resource = await createRandomFile('', undefined, '.vsctestnb'); + await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + await vscode.commands.executeCommand('notebook.selectKernel', { extension: 'vscode.vscode-api-tests', id: invalidKernel.id }); + await vscode.commands.executeCommand('notebook.cell.execute'); + + assert.strictEqual(missedError, undefined, missedError); + + await saveAllFilesAndCloseAll(undefined); + }); // }); diff --git a/extensions/vscode-notebook-tests/src/extension.ts b/extensions/vscode-notebook-tests/src/extension.ts index 038b87fc44e..3338813d5f9 100644 --- a/extensions/vscode-notebook-tests/src/extension.ts +++ b/extensions/vscode-notebook-tests/src/extension.ts @@ -62,32 +62,22 @@ export function activate(context: vscode.ExtensionContext): any { })); const kernel: vscode.NotebookKernel = { + id: 'notebookSmokeTest', label: 'notebookSmokeTest', isPreferred: true, - executeAllCells: async (_document: vscode.NotebookDocument) => { - const edit = new vscode.WorkspaceEdit(); - for (let i = 0; i < _document.cells.length; i++) { - edit.replaceNotebookCellOutput(_document.uri, i, [new vscode.NotebookCellOutput([ - new vscode.NotebookCellOutputItem('text/html', ['test output'], undefined) - ])]); + executeCellsRequest: async (document: vscode.NotebookDocument, ranges: vscode.NotebookCellRange[]) => { + const idx = ranges[0].start; + const task = vscode.notebook.createNotebookCellExecutionTask(document.uri, idx, 'notebookSmokeTest'); + if (!task) { + return; } - await vscode.workspace.applyEdit(edit); - }, - cancelAllCellsExecution: async () => { }, - executeCell: async (_document: vscode.NotebookDocument, _cell: vscode.NotebookCell | undefined) => { - if (!_cell) { - _cell = _document.cells[0]; - } - - const edit = new vscode.WorkspaceEdit(); - edit.replaceNotebookCellOutput(_document.uri, _cell.index, [new vscode.NotebookCellOutput([ + task.start(); + task.replaceOutput([new vscode.NotebookCellOutput([ new vscode.NotebookCellOutputItem('text/html', ['test output'], undefined) ])]); - await vscode.workspace.applyEdit(edit); - return; - }, - cancelCellExecution: async () => { } + task.end({ success: true }); + } }; context.subscriptions.push(vscode.notebook.registerNotebookKernelProvider({ filenamePattern: '*.smoke-nb' }, { diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 17e0ec366d9..0b90f75a65f 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -963,18 +963,6 @@ declare module 'vscode' { Code = 2 } - export enum NotebookCellRunState { - Running = 1, - Idle = 2, - Success = 3, - Error = 4 - } - - export enum NotebookRunState { - Running = 1, - Idle = 2 - } - export class NotebookCellMetadata { /** * Controls whether a cell's editor is editable/readonly. @@ -1003,14 +991,16 @@ declare module 'vscode' { // run related API, will be removed readonly hasExecutionOrder?: boolean; - readonly executionOrder?: number; - readonly runState?: NotebookCellRunState; - readonly runStartTime?: number; - readonly lastRunDuration?: number; - constructor(editable?: boolean, breakpointMargin?: boolean, hasExecutionOrder?: boolean, executionOrder?: number, runState?: NotebookCellRunState, runStartTime?: number, statusMessage?: string, lastRunDuration?: number, inputCollapsed?: boolean, outputCollapsed?: boolean, custom?: Record) + constructor(editable?: boolean, breakpointMargin?: boolean, hasExecutionOrder?: boolean, statusMessage?: string, lastRunDuration?: number, inputCollapsed?: boolean, outputCollapsed?: boolean, custom?: Record) - with(change: { editable?: boolean | null, breakpointMargin?: boolean | null, hasExecutionOrder?: boolean | null, executionOrder?: number | null, runState?: NotebookCellRunState | null, runStartTime?: number | null, statusMessage?: string | null, lastRunDuration?: number | null, inputCollapsed?: boolean | null, outputCollapsed?: boolean | null, custom?: Record | null, }): NotebookCellMetadata; + with(change: { editable?: boolean | null, breakpointMargin?: boolean | null, hasExecutionOrder?: boolean | null, statusMessage?: string | null, lastRunDuration?: number | null, inputCollapsed?: boolean | null, outputCollapsed?: boolean | null, custom?: Record | null, }): NotebookCellMetadata; + } + + export interface NotebookCellExecutionSummary { + executionOrder?: number; + success?: boolean; + duration?: number; } // todo@API support ids https://github.com/jupyter/enhancement-proposals/blob/master/62-cell-id/cell-id.md @@ -1021,6 +1011,7 @@ declare module 'vscode' { readonly document: TextDocument; readonly metadata: NotebookCellMetadata readonly outputs: ReadonlyArray; + readonly latestExecutionSummary: NotebookCellExecutionSummary | undefined; } export class NotebookDocumentMetadata { @@ -1048,12 +1039,9 @@ declare module 'vscode' { // todo@API is this a kernel property? readonly cellHasExecutionOrder: boolean; - // todo@API remove - readonly runState: NotebookRunState; + constructor(editable?: boolean, cellEditable?: boolean, cellHasExecutionOrder?: boolean, custom?: { [key: string]: any; }, trusted?: boolean); - constructor(editable?: boolean, cellEditable?: boolean, cellHasExecutionOrder?: boolean, custom?: { [key: string]: any; }, runState?: NotebookRunState, trusted?: boolean); - - with(change: { editable?: boolean | null, cellEditable?: boolean | null, cellHasExecutionOrder?: boolean | null, custom?: { [key: string]: any; } | null, runState?: NotebookRunState | null, trusted?: boolean | null, }): NotebookDocumentMetadata + with(change: { editable?: boolean | null, cellEditable?: boolean | null, cellHasExecutionOrder?: boolean | null, custom?: { [key: string]: any; } | null, trusted?: boolean | null, }): NotebookDocumentMetadata } export interface NotebookDocumentContentOptions { @@ -1227,6 +1215,12 @@ declare module 'vscode' { readonly visibleRanges: ReadonlyArray; } + export interface NotebookCellExecutionStateChangeEvent { + readonly document: NotebookDocument; + readonly cell: NotebookCell; + readonly executionState: NotebookCellExecutionState; + } + // todo@API support ids https://github.com/jupyter/enhancement-proposals/blob/master/62-cell-id/cell-id.md export class NotebookCellData { kind: NotebookCellKind; @@ -1236,7 +1230,8 @@ declare module 'vscode' { language: string; outputs?: NotebookCellOutput[]; metadata?: NotebookCellMetadata; - constructor(kind: NotebookCellKind, source: string, language: string, outputs?: NotebookCellOutput[], metadata?: NotebookCellMetadata) + latestExecutionSummary?: NotebookCellExecutionSummary; + constructor(kind: NotebookCellKind, source: string, language: string, outputs?: NotebookCellOutput[], metadata?: NotebookCellMetadata, latestExecutionSummary?: NotebookCellExecutionSummary); } export class NotebookData { @@ -1496,27 +1491,6 @@ declare module 'vscode' { //#region https://github.com/microsoft/vscode/issues/106744, NotebookKernel - // todo@API use the NotebookCellExecution-object as a container to model and enforce - // the flow of a cell execution - - // kernel -> execute_info - // ext -> createNotebookCellExecution(cell) - // kernel -> done - // exec.dispose(); - - // export interface NotebookCellExecution { - // dispose(): void; - // clearOutput(): void; - // appendOutput(out: NotebookCellOutput): void; - // replaceOutput(out: NotebookCellOutput): void; - // appendOutputItems(output:string, items: NotebookCellOutputItem[]):void; - // replaceOutputItems(output:string, items: NotebookCellOutputItem[]):void; - // } - - // export function createNotebookCellExecution(cell: NotebookCell, startTime?: number): NotebookCellExecution; - // export const onDidStartNotebookCellExecution: Event; - // export const onDidStopNotebookCellExecution: Event; - export interface NotebookKernel { // todo@API make this mandatory? @@ -1541,14 +1515,86 @@ declare module 'vscode' { // fired when properties like the supported languages etc change // onDidChangeProperties?: Event - // @roblourens - // todo@API change to `executeCells(document: NotebookDocument, cells: NotebookCellRange[], context:{isWholeNotebooke: boolean}, token: CancelationToken): void;` - // todo@API interrupt vs cancellation, https://github.com/microsoft/vscode/issues/106741 - // interrupt?():void; - executeCell(document: NotebookDocument, cell: NotebookCell): void; - cancelCellExecution(document: NotebookDocument, cell: NotebookCell): void; - executeAllCells(document: NotebookDocument): void; - cancelAllCellsExecution(document: NotebookDocument): void; + /** + * A kernel can optionally implement this which will be called when any "cancel" button is clicked in the document. + */ + interrupt?(document: NotebookDocument): void; + + /** + * Called when the user triggers execution of a cell by clicking the run button for a cell, multiple cells, + * or full notebook. The cell will be put into the Pending state when this method is called. If + * createNotebookCellExecutionTask has not been called by the time the promise returned by this method is + * resolved, the cell will be put back into the Idle state. + */ + executeCellsRequest(document: NotebookDocument, ranges: NotebookCellRange[]): Thenable; + } + + export interface NotebookCellExecuteStartContext { + // TODO@roblou are we concerned about clock issues with this absolute time? + /** + * The time that execution began, in milliseconds in the Unix epoch. Used to drive the clock + * that shows for how long a cell has been running. If not given, the clock won't be shown. + */ + startTime?: number; + } + + export interface NotebookCellExecuteEndContext { + /** + * If true, a green check is shown on the cell status bar. + * If false, a red X is shown. + */ + success?: boolean; + + /** + * The total execution time in milliseconds. + */ + duration?: number; + } + + /** + * A NotebookCellExecutionTask is how the kernel modifies a notebook cell as it is executing. When + * [`createNotebookCellExecutionTask`](#notebook.createNotebookCellExecutionTask) is called, the cell + * enters the Pending state. When `start()` is called on the execution task, it enters the Executing state. When + * `end()` is called, it enters the Idle state. While in the Executing state, cell outputs can be + * modified with the methods on the run task. + * + * All outputs methods operate on this NotebookCellExecutionTask's cell by default. They optionally take + * a cellIndex parameter that allows them to modify the outputs of other cells. `appendOutputItems` and + * `replaceOutputItems` operate on the output with the given ID, which can be an output on any cell. They + * all resolve once the output edit has been applied. + */ + export interface NotebookCellExecutionTask { + readonly document: NotebookDocument; + readonly cell: NotebookCell; + + start(context?: NotebookCellExecuteStartContext): void; + executionOrder: number | undefined; + end(result?: NotebookCellExecuteEndContext): void; + readonly token: CancellationToken; + + clearOutput(cellIndex?: number): Thenable; + appendOutput(out: NotebookCellOutput[], cellIndex?: number): Thenable; + replaceOutput(out: NotebookCellOutput[], cellIndex?: number): Thenable; + appendOutputItems(items: NotebookCellOutputItem[], outputId: string): Thenable; + replaceOutputItems(items: NotebookCellOutputItem[], outputId: string): Thenable; + } + + export enum NotebookCellExecutionState { + Idle = 1, + Pending = 2, + Executing = 3, + } + + export namespace notebook { + /** + * Creates a [`NotebookCellExecutionTask`](#NotebookCellExecutionTask). Should only be called by a kernel. Returns undefined unless requested by the active kernel. + * @param uri The [uri](#Uri) of the notebook document. + * @param index The index of the cell. + * @param kernelId The id of the kernel requesting this run task. If this kernel is not the current active kernel, `undefined` is returned. + */ + export function createNotebookCellExecutionTask(uri: Uri, index: number, kernelId: string): NotebookCellExecutionTask | undefined; + + export const onDidChangeCellExecutionState: Event; } export type NotebookFilenamePattern = GlobPattern | { include: GlobPattern; exclude: GlobPattern; }; diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts index 87fb882781e..f287b19b1eb 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -24,7 +24,7 @@ import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/no import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { INotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/common/notebookCellStatusBarService'; -import { ICellEditOperation, ICellRange, IMainCellDto, INotebookDecorationRenderOptions, INotebookDocumentFilter, INotebookExclusiveDocumentFilter, INotebookKernel, NotebookCellsChangeType, NotebookDataDto, TransientMetadata, TransientOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { ICellEditOperation, ICellRange, IImmediateCellEditOperation, IMainCellDto, INotebookDecorationRenderOptions, INotebookDocumentFilter, INotebookExclusiveDocumentFilter, INotebookKernel, NotebookCellsChangeType, NotebookDataDto, TransientMetadata, TransientOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookEditorModelResolverService } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService'; import { IMainNotebookController, INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -165,6 +165,15 @@ export class MainThreadNotebooks implements MainThreadNotebookShape { return textModel.applyEdits(cellEdits, true, undefined, () => undefined, undefined); } + async $applyEdits(resource: UriComponents, cellEdits: IImmediateCellEditOperation[], computeUndoRedo = true): Promise { + const textModel = this._notebookService.getNotebookTextModel(URI.from(resource)); + if (!textModel) { + throw new Error(`Can't apply edits to unknown notebook model: ${resource}`); + } + + textModel.applyEdits(cellEdits, true, undefined, () => undefined, undefined, computeUndoRedo); + } + private _registerListeners(): void { // forward changes to dirty state @@ -497,17 +506,18 @@ export class MainThreadNotebooks implements MainThreadNotebookShape { isPreferred: dto.isPreferred, preloads: dto.preloads?.map(u => URI.revive(u)), supportedLanguages: dto.supportedLanguages, + implementsInterrupt: dto.implementsInterrupt, resolve: (uri: URI, editorId: string, token: CancellationToken): Promise => { this._logService.debug('MainthreadNotebooks.resolveNotebookKernel', uri.path, dto.friendlyId); return this._proxy.$resolveNotebookKernel(handle, editorId, uri, dto.friendlyId, token); }, - executeNotebookCell: (uri: URI, cellHandle: number | undefined): Promise => { - this._logService.debug('MainthreadNotebooks.executeNotebookCell', uri.path, dto.friendlyId, cellHandle); - return this._proxy.$executeNotebookKernelFromProvider(handle, uri, dto.friendlyId, cellHandle); + executeNotebookCellsRequest: (uri: URI, cellRanges: ICellRange[]): Promise => { + this._logService.debug('MainthreadNotebooks.executeNotebookCell', uri.path, dto.friendlyId, cellRanges); + return this._proxy.$executeNotebookKernelFromProvider(handle, uri, dto.friendlyId, cellRanges); }, - cancelNotebookCell: (uri: URI, cellHandle: number | undefined): Promise => { - this._logService.debug('MainthreadNotebooks.cancelNotebookCell', uri.path, dto.friendlyId, cellHandle); - return this._proxy.$cancelNotebookKernelFromProvider(handle, uri, dto.friendlyId, cellHandle); + cancelNotebookCellExecution: (uri: URI, cellRanges: ICellRange[]): Promise => { + this._logService.debug('MainthreadNotebooks.cancelNotebookCellExecution', uri.path, dto.friendlyId, cellRanges); + return this._proxy.$cancelNotebookCellExecution(handle, uri, dto.friendlyId, cellRanges); } }); } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 58a5fd98ccc..b1285f37734 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1078,6 +1078,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension); return extHostNotebook.onDidChangeNotebookCells(listener, thisArgs, disposables); }, + onDidChangeCellExecutionState(listener, thisArgs?, disposables?) { + checkProposedApiEnabled(extension); + return extHostNotebook.onDidChangeNotebookCellExecutionState(listener, thisArgs, disposables); + }, onDidChangeCellOutputs(listener, thisArgs?, disposables?) { checkProposedApiEnabled(extension); return extHostNotebook.onDidChangeCellOutputs(listener, thisArgs, disposables); @@ -1093,6 +1097,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I createCellStatusBarItem(cell: vscode.NotebookCell, alignment?: vscode.NotebookCellStatusBarAlignment, priority?: number): vscode.NotebookCellStatusBarItem { checkProposedApiEnabled(extension); return extHostNotebook.createNotebookCellStatusBarItemInternal(cell, alignment, priority); + }, + createNotebookCellExecutionTask(uri: vscode.Uri, index: number, kernelId: string): vscode.NotebookCellExecutionTask | undefined { + checkProposedApiEnabled(extension); + return extHostNotebook.createNotebookCellExecution(uri, index, kernelId); } }; @@ -1231,12 +1239,11 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I TimelineItem: extHostTypes.TimelineItem, NotebookCellRange: extHostTypes.NotebookCellRange, NotebookCellKind: extHostTypes.NotebookCellKind, - NotebookCellRunState: extHostTypes.NotebookCellRunState, + NotebookCellExecutionState: extHostTypes.NotebookCellExecutionState, NotebookDocumentMetadata: extHostTypes.NotebookDocumentMetadata, NotebookCellMetadata: extHostTypes.NotebookCellMetadata, NotebookCellData: extHostTypes.NotebookCellData, NotebookData: extHostTypes.NotebookData, - NotebookRunState: extHostTypes.NotebookRunState, NotebookCellStatusBarAlignment: extHostTypes.NotebookCellStatusBarAlignment, NotebookEditorRevealType: extHostTypes.NotebookEditorRevealType, NotebookCellOutput: extHostTypes.NotebookCellOutput, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 664ad417000..59f972d8493 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -50,7 +50,7 @@ import { TunnelDto } from 'vs/workbench/api/common/extHostTunnelService'; import { TunnelCreationOptions, TunnelProviderFeatures, TunnelOptions, ProvidedPortAttributes } from 'vs/platform/remote/common/tunnel'; import { Timeline, TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor, InternalTimelineOptions } from 'vs/workbench/contrib/timeline/common/timeline'; import { revive } from 'vs/base/common/marshalling'; -import { NotebookCellMetadata, NotebookDocumentMetadata, ICellEditOperation, NotebookCellsChangedEventDto, NotebookDataDto, IMainCellDto, INotebookDocumentFilter, TransientMetadata, INotebookCellStatusBarEntry, ICellRange, INotebookDecorationRenderOptions, INotebookExclusiveDocumentFilter, IOutputDto, TransientOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NotebookCellMetadata, NotebookDocumentMetadata, ICellEditOperation, NotebookCellsChangedEventDto, NotebookDataDto, IMainCellDto, INotebookDocumentFilter, TransientMetadata, INotebookCellStatusBarEntry, ICellRange, INotebookDecorationRenderOptions, INotebookExclusiveDocumentFilter, IOutputDto, TransientOptions, IImmediateCellEditOperation } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; import { Dto } from 'vs/base/common/types'; import { DebugConfigurationProviderTriggerKind, WorkspaceTrustState } from 'vs/workbench/api/common/extHostTypes'; @@ -849,6 +849,7 @@ export interface MainThreadNotebookShape extends IDisposable { $onNotebookKernelChange(handle: number, uri: UriComponents | undefined): void; $trySaveDocument(uri: UriComponents): Promise; $tryApplyEdits(viewType: string, resource: UriComponents, modelVersionId: number, edits: ICellEditOperation[]): Promise; + $applyEdits(resource: UriComponents, edits: IImmediateCellEditOperation[], computeUndoRedo?: boolean): Promise; $postMessage(editorId: string, forRendererId: string | undefined, value: any): Promise; $setStatusBarEntry(id: number, statusBarEntry: INotebookCellStatusBarEntryDto): Promise; $tryOpenDocument(uriComponents: UriComponents): Promise; @@ -1857,6 +1858,7 @@ export interface INotebookKernelInfoDto2 { isPreferred?: boolean; preloads?: UriComponents[]; supportedLanguages?: string[] + implementsInterrupt?: boolean; } export interface ExtHostNotebookShape { @@ -1864,8 +1866,8 @@ export interface ExtHostNotebookShape { $acceptNotebookActiveKernelChange(event: { uri: UriComponents, providerHandle: number | undefined, kernelFriendlyId: string | undefined }): void; $provideNotebookKernels(handle: number, uri: UriComponents, token: CancellationToken): Promise; $resolveNotebookKernel(handle: number, editorId: string, uri: UriComponents, kernelId: string, token: CancellationToken): Promise; - $executeNotebookKernelFromProvider(handle: number, uri: UriComponents, kernelId: string, cellHandle: number | undefined): Promise; - $cancelNotebookKernelFromProvider(handle: number, uri: UriComponents, kernelId: string, cellHandle: number | undefined): Promise; + $executeNotebookKernelFromProvider(handle: number, uri: UriComponents, kernelId: string, cellRanges: ICellRange[]): Promise; + $cancelNotebookCellExecution(handle: number, uri: UriComponents, kernelId: string, cellRange: ICellRange[]): Promise; $onDidReceiveMessage(editorId: string, rendererId: string | undefined, message: unknown): void; $openNotebook(viewType: string, uri: UriComponents, backupId: string | undefined, untitledDocumentData: VSBuffer | undefined, token: CancellationToken): Promise; diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index c6b8ed36d73..d06967d37ea 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -17,7 +17,7 @@ import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePa import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; import { asWebviewUri, WebviewInitData } from 'vs/workbench/api/common/shared/webview'; -import { CellStatusbarAlignment, CellUri, INotebookCellStatusBarEntry, INotebookExclusiveDocumentFilter, NotebookCellMetadata, NotebookCellsChangedEventDto, NotebookCellsChangeType, NotebookDataDto, TransientOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellEditType, CellStatusbarAlignment, CellUri, ICellRange, INotebookCellStatusBarEntry, INotebookExclusiveDocumentFilter, NotebookCellMetadata, NotebookCellExecutionState, NotebookCellsChangedEventDto, NotebookCellsChangeType, NotebookDataDto, TransientOptions, NullablePartialNotebookCellMetadata, IImmediateCellEditOperation } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import * as vscode from 'vscode'; import { ResourceMap } from 'vs/base/common/map'; import { ExtHostCell, ExtHostNotebookDocument } from './extHostNotebookDocument'; @@ -125,7 +125,8 @@ export class ExtHostNotebookKernelProviderAdapter extends Disposable { detail: kernel.detail, isPreferred: kernel.isPreferred, preloads: kernel.preloads, - supportedLanguages: kernel.supportedLanguages + supportedLanguages: kernel.supportedLanguages, + implementsInterrupt: !!kernel.interrupt }; }); @@ -151,42 +152,25 @@ export class ExtHostNotebookKernelProviderAdapter extends Disposable { } } - async executeNotebook(kernelId: string, document: ExtHostNotebookDocument, cell: ExtHostCell | undefined) { + async executeNotebook(kernelId: string, document: ExtHostNotebookDocument, cellRange: ICellRange[]): Promise { const kernel = this._friendlyIdToKernel.get(document.uri)?.get(kernelId); if (!kernel) { return; } - if (cell) { - return withToken(token => (kernel.executeCell as any)(document.notebookDocument, cell.cell, token)); - } else { - return withToken(token => (kernel.executeAllCells as any)(document.notebookDocument, token)); - } + const extCellRange = cellRange.map(c => typeConverters.NotebookCellRange.to(c)); + return kernel.executeCellsRequest(document.notebookDocument, extCellRange); } - async cancelNotebook(kernelId: string, document: ExtHostNotebookDocument, cell: ExtHostCell | undefined) { + async interruptNotebookExecution(kernelId: string, document: ExtHostNotebookDocument): Promise { const kernel = this._friendlyIdToKernel.get(document.uri)?.get(kernelId); - if (!kernel) { + if (!kernel || !kernel.interrupt) { return; } - if (cell) { - return kernel.cancelCellExecution(document.notebookDocument, cell.cell); - } else { - return kernel.cancelAllCellsExecution(document.notebookDocument); - } - } -} - -// TODO@roblou remove 'token' passed to all execute APIs once extensions are updated -async function withToken(cb: (token: CancellationToken) => any) { - const source = new CancellationTokenSource(); - try { - await cb(source.token); - } finally { - source.dispose(); + return kernel.interrupt(document.notebookDocument); } } @@ -239,6 +223,8 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { readonly onDidChangeCellMetadata = this._onDidChangeCellMetadata.event; private readonly _onDidChangeActiveNotebookEditor = new Emitter(); readonly onDidChangeActiveNotebookEditor = this._onDidChangeActiveNotebookEditor.event; + private readonly _onDidChangeCellExecutionState = new Emitter(); + readonly onDidChangeNotebookCellExecutionState = this._onDidChangeCellExecutionState.event; private _activeNotebookEditor: ExtHostNotebookEditor | undefined; get activeNotebookEditor(): vscode.NotebookEditor | undefined { @@ -260,6 +246,8 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { private _onDidChangeVisibleNotebookEditors = new Emitter(); onDidChangeVisibleNotebookEditors = this._onDidChangeVisibleNotebookEditors.event; + private _activeExecutions = new ResourceMap(); + constructor( mainContext: IMainContext, commands: ExtHostCommands, @@ -481,19 +469,9 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { await provider.provider.resolveNotebook(document.notebookDocument, webComm.contentProviderComm); } - async $executeNotebookKernelFromProvider(handle: number, uri: UriComponents, kernelId: string, cellHandle: number | undefined): Promise { + async $executeNotebookKernelFromProvider(handle: number, uri: UriComponents, kernelId: string, cellRange: ICellRange[]): Promise { await this._withAdapter(handle, uri, async (adapter, document) => { - const cell = cellHandle !== undefined ? document.getCell(cellHandle) : undefined; - - return adapter.executeNotebook(kernelId, document, cell); - }); - } - - async $cancelNotebookKernelFromProvider(handle: number, uri: UriComponents, kernelId: string, cellHandle: number | undefined): Promise { - await this._withAdapter(handle, uri, async (adapter, document) => { - const cell = cellHandle !== undefined ? document.getCell(cellHandle) : undefined; - - return adapter.cancelNotebook(kernelId, document, cell); + return adapter.executeNotebook(kernelId, document, cellRange); }); } @@ -540,6 +518,31 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { return VSBuffer.wrap(bytes); } + async $cancelNotebookCellExecution(handle: number, uri: UriComponents, kernelId: string, cellRange: ICellRange[]): Promise { + await this._withAdapter(handle, uri, async (adapter, document) => { + return adapter.interruptNotebookExecution(kernelId, document); + }); + + const document = this._documents.get(URI.revive(uri)); + if (!document) { + return; + } + + for (let range of cellRange) { + for (let i = range.start; i < range.end; i++) { + const cell = document.getCellFromIndex(i); + if (cell) { + this.cancelOneNotebookCellExecution(cell); + } + } + } + } + + private cancelOneNotebookCellExecution(cell: ExtHostCell): void { + const execution = this._activeExecutions.get(cell.uri); + execution?.cancel(); + } + // --- open, save, saveAs, backup async $openNotebook(viewType: string, uri: UriComponents, backupId: string | undefined, untitledDocumentData: VSBuffer | undefined, token: CancellationToken): Promise { @@ -729,6 +732,9 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { emitCellMetadataChange(event: vscode.NotebookCellMetadataChangeEvent): void { that._onDidChangeCellMetadata.fire(event); }, + emitCellExecutionStateChange(event: vscode.NotebookCellExecutionStateChangeEvent): void { + that._onDidChangeCellExecutionState.fire(event); + } }, viewType, modelData.metadata ? typeConverters.NotebookDocumentMetadata.to(modelData.metadata) : new extHostTypes.NotebookDocumentMetadata(), @@ -837,6 +843,35 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { return statusBarItem; } + + createNotebookCellExecution(docUri: vscode.Uri, index: number, kernelId: string): vscode.NotebookCellExecutionTask | undefined { + const document = this.lookupNotebookDocument(docUri); + if (!document) { + throw new Error(`Invalid cell uri/index: ${docUri}, ${index}`); + } + + const cell = document.getCellFromIndex(index); + if (!cell) { + throw new Error(`Invalid cell uri/index: ${docUri}, ${index}`); + } + + // TODO@roblou also validate kernelId, once kernel has moved from editor to document + if (this._activeExecutions.has(cell.uri)) { + return; + } + + const execution = new NotebookCellExecutionTask(docUri, document, cell, this._proxy); + this._activeExecutions.set(cell.uri, execution); + const listener = execution.onDidChangeState(() => { + if (execution.state === NotebookCellExecutionTaskState.Resolved) { + execution.dispose(); + listener.dispose(); + this._activeExecutions.delete(cell.uri); + } + }); + + return execution.asApiObject(); + } } export class NotebookCellStatusBarItemInternal extends Disposable { @@ -1013,3 +1048,153 @@ function createNotebookCellStatusBarApiItem(internalItem: NotebookCellStatusBarI dispose() { internalItem.dispose(); } }); } + +enum NotebookCellExecutionTaskState { + Init, + Started, + Resolved +} + +class NotebookCellExecutionTask extends Disposable { + private _onDidChangeState = new Emitter(); + readonly onDidChangeState = this._onDidChangeState.event; + + private _state = NotebookCellExecutionTaskState.Init; + get state(): NotebookCellExecutionTaskState { return this._state; } + + private readonly _tokenSource: CancellationTokenSource; + + private _executionOrder: number | undefined; + + constructor( + private readonly _uri: vscode.Uri, + private readonly _document: ExtHostNotebookDocument, + private readonly _cell: ExtHostCell, + private readonly _proxy: MainThreadNotebookShape) { + super(); + this._tokenSource = this._register(new CancellationTokenSource()); + + this._executionOrder = _cell.internalMetadata.executionOrder; + this.mixinMetadata({ + runState: NotebookCellExecutionState.Pending, + lastRunDuration: null, + executionOrder: null + }); + } + + cancel(): void { + this._tokenSource.cancel(); + } + + private async applyEdits(edits: IImmediateCellEditOperation[]): Promise { + return this._proxy.$applyEdits(this._uri, edits, false); + } + + private verifyStateForOutput() { + if (this._state === NotebookCellExecutionTaskState.Init) { + throw new Error('Must call start before modifying cell output'); + } + + if (this._state === NotebookCellExecutionTaskState.Resolved) { + throw new Error('Cannot modify cell output after calling resolve'); + } + } + + private mixinMetadata(mixinMetadata: NullablePartialNotebookCellMetadata) { + const edits: IImmediateCellEditOperation[] = [ + { editType: CellEditType.PartialMetadata, handle: this._cell.handle, metadata: mixinMetadata } + ]; + this.applyEdits(edits); + } + + private cellIndexToHandle(cellIndex: number | undefined): number | undefined { + const cell = typeof cellIndex === 'number' ? this._document.getCellFromIndex(cellIndex) : this._cell; + if (!cell) { + return; + } + + return cell.handle; + } + + asApiObject(): vscode.NotebookCellExecutionTask { + const that = this; + return Object.freeze({ + get document() { return that._document.notebookDocument; }, + get cell() { return that._cell.cell; }, + + get executionOrder() { return that._executionOrder; }, + set executionOrder(v: number | undefined) { + that._executionOrder = v; + that.mixinMetadata({ + executionOrder: v + }); + }, + + start(context?: vscode.NotebookCellExecuteStartContext): void { + if (that._state === NotebookCellExecutionTaskState.Resolved || that._state === NotebookCellExecutionTaskState.Started) { + throw new Error('Cannot call start again'); + } + + that._state = NotebookCellExecutionTaskState.Started; + that._onDidChangeState.fire(); + + that.mixinMetadata({ + runState: NotebookCellExecutionState.Executing, + runStartTime: context?.startTime + }); + }, + + end(result?: vscode.NotebookCellExecuteEndContext): void { + if (that._state === NotebookCellExecutionTaskState.Resolved) { + throw new Error('Cannot call resolve twice'); + } + + that._state = NotebookCellExecutionTaskState.Resolved; + that._onDidChangeState.fire(); + + that.mixinMetadata({ + runState: NotebookCellExecutionState.Idle, + lastRunSuccess: result?.success ?? null, + lastRunDuration: result?.duration ?? null, + }); + }, + + clearOutput(cellIndex?: number): Thenable { + that.verifyStateForOutput(); + return this.replaceOutput([], cellIndex); + }, + + async appendOutput(outputs: vscode.NotebookCellOutput[], cellIndex?: number): Promise { + that.verifyStateForOutput(); + const handle = that.cellIndexToHandle(cellIndex); + if (typeof handle !== 'number') { + return; + } + + return that.applyEdits([{ editType: CellEditType.Output, handle, append: true, outputs: outputs.map(typeConverters.NotebookCellOutput.from) }]); + }, + + async replaceOutput(outputs: vscode.NotebookCellOutput[], cellIndex?: number): Promise { + that.verifyStateForOutput(); + const handle = that.cellIndexToHandle(cellIndex); + if (typeof handle !== 'number') { + return; + } + + return that.applyEdits([{ editType: CellEditType.Output, handle, outputs: outputs.map(typeConverters.NotebookCellOutput.from) }]); + }, + + async appendOutputItems(items: vscode.NotebookCellOutputItem[], outputId: string): Promise { + that.verifyStateForOutput(); + return that.applyEdits([{ editType: CellEditType.OutputItems, append: true, items: items.map(typeConverters.NotebookCellOutputItem.from), outputId }]); + }, + + async replaceOutputItems(items: vscode.NotebookCellOutputItem[], outputId: string): Promise { + that.verifyStateForOutput(); + return that.applyEdits([{ editType: CellEditType.OutputItems, items: items.map(typeConverters.NotebookCellOutputItem.from), outputId }]); + }, + + token: that._tokenSource.token + }); + } +} diff --git a/src/vs/workbench/api/common/extHostNotebookDocument.ts b/src/vs/workbench/api/common/extHostNotebookDocument.ts index a87b137a2aa..c923100305a 100644 --- a/src/vs/workbench/api/common/extHostNotebookDocument.ts +++ b/src/vs/workbench/api/common/extHostNotebookDocument.ts @@ -6,7 +6,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, DisposableStore, dispose } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; -import { deepFreeze } from 'vs/base/common/objects'; +import { deepFreeze, equals } from 'vs/base/common/objects'; import { URI } from 'vs/base/common/uri'; import { CellKind, INotebookDocumentPropertiesChangeData, MainThreadNotebookShape } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; @@ -51,7 +51,9 @@ export class ExtHostCell { private _outputs: extHostTypes.NotebookCellOutput[]; private _metadata: extHostTypes.NotebookCellMetadata; + private _previousResult: vscode.NotebookCellExecutionSummary | undefined; + private _internalMetadata: NotebookCellMetadata; readonly handle: number; readonly uri: URI; readonly cellKind: CellKind; @@ -67,7 +69,9 @@ export class ExtHostCell { this.uri = URI.revive(_cellData.uri); this.cellKind = _cellData.cellKind; this._outputs = _cellData.outputs.map(extHostTypeConverters.NotebookCellOutput.to); - this._metadata = extHostTypeConverters.NotebookCellMetadata.to(_cellData.metadata ?? {}); + this._internalMetadata = _cellData.metadata ?? {}; + this._metadata = extHostTypeConverters.NotebookCellMetadata.to(this._internalMetadata); + this._previousResult = extHostTypeConverters.NotebookCellPreviousExecutionResult.to(this._internalMetadata); } dispose() { @@ -75,6 +79,10 @@ export class ExtHostCell { this._onDidDispose.dispose(); } + get internalMetadata(): NotebookCellMetadata { + return this._internalMetadata; + } + get cell(): vscode.NotebookCell { if (!this._cell) { const that = this; @@ -89,6 +97,7 @@ export class ExtHostCell { document: data.document, get outputs() { return that._outputs.slice(0); }, get metadata() { return that._metadata; }, + get latestExecutionSummary() { return that._previousResult; } }); } return this._cell; @@ -110,7 +119,9 @@ export class ExtHostCell { } setMetadata(newMetadata: NotebookCellMetadata): void { + this._internalMetadata = newMetadata; this._metadata = extHostTypeConverters.NotebookCellMetadata.to(newMetadata); + this._previousResult = extHostTypeConverters.NotebookCellPreviousExecutionResult.to(newMetadata); } } @@ -118,6 +129,7 @@ export interface INotebookEventEmitter { emitModelChange(events: vscode.NotebookCellsChangeEvent): void; emitCellOutputsChange(event: vscode.NotebookCellOutputsChangeEvent): void; emitCellMetadataChange(event: vscode.NotebookCellMetadataChangeEvent): void; + emitCellExecutionStateChange(event: vscode.NotebookCellExecutionStateChangeEvent): void; } @@ -308,10 +320,22 @@ export class ExtHostNotebookDocument extends Disposable { } } - private _changeCellMetadata(index: number, newMetadata: NotebookCellMetadata | undefined): void { + private _changeCellMetadata(index: number, newMetadata: NotebookCellMetadata): void { const cell = this._cells[index]; - cell.setMetadata(newMetadata || {}); - this._emitter.emitCellMetadataChange(deepFreeze({ document: this.notebookDocument, cell: cell.cell })); + + const originalInternalMetadata = cell.internalMetadata; + const originalExtMetadata = cell.cell.metadata; + cell.setMetadata(newMetadata); + const newExtMetadata = cell.cell.metadata; + + if (!equals(originalExtMetadata, newExtMetadata)) { + this._emitter.emitCellMetadataChange(deepFreeze({ document: this.notebookDocument, cell: cell.cell })); + } + + if (originalInternalMetadata.runState !== newMetadata.runState) { + const executionState = newMetadata.runState ?? extHostTypes.NotebookCellExecutionState.Idle; + this._emitter.emitCellExecutionStateChange(deepFreeze({ document: this.notebookDocument, cell: cell.cell, executionState })); + } } getCellFromIndex(index: number): ExtHostCell | undefined { diff --git a/src/vs/workbench/api/common/extHostNotebookEditor.ts b/src/vs/workbench/api/common/extHostNotebookEditor.ts index 6f8a7a5f8cd..b8b752a876a 100644 --- a/src/vs/workbench/api/common/extHostNotebookEditor.ts +++ b/src/vs/workbench/api/common/extHostNotebookEditor.ts @@ -204,9 +204,9 @@ export class ExtHostNotebookEditor { const prevIndex = compressedEditsIndex; const prev = compressedEdits[prevIndex]; - if (prev.editType === CellEditType.Replace && editData.cellEdits[i].editType === CellEditType.Replace) { - const edit = editData.cellEdits[i]; - if ((edit.editType !== CellEditType.DocumentMetadata) && prev.index === edit.index) { + const edit = editData.cellEdits[i]; + if (prev.editType === CellEditType.Replace && edit.editType === CellEditType.Replace) { + if (prev.index === edit.index) { prev.cells.push(...(editData.cellEdits[i] as ICellReplaceEdit).cells); prev.count += (editData.cellEdits[i] as ICellReplaceEdit).count; continue; diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 00bd48ce44e..6876415daef 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -568,7 +568,7 @@ export namespace WorkspaceEdit { metadata: entry.metadata, resource: entry.uri, edit: { - editType: notebooks.CellEditType.Metadata, + editType: notebooks.CellEditType.PartialMetadata, index: entry.index, metadata: entry.newMetadata } @@ -594,7 +594,6 @@ export namespace WorkspaceEdit { resource: entry.uri, edit: { editType: notebooks.CellEditType.OutputItems, - index: entry.index, outputId: entry.outputId, items: entry.newOutputItems?.map(NotebookCellOutputItem.from) || [], append: entry.append @@ -1420,7 +1419,7 @@ export namespace NotebookCellRange { export namespace NotebookCellMetadata { export function to(data: notebooks.NotebookCellMetadata): types.NotebookCellMetadata { - return new types.NotebookCellMetadata(data.editable, data.breakpointMargin, data.hasExecutionOrder, data.executionOrder, data.runState, data.runStartTime, data.statusMessage, data.lastRunDuration, data.inputCollapsed, data.outputCollapsed, data.custom); + return new types.NotebookCellMetadata(data.editable, data.breakpointMargin, data.hasExecutionOrder, data.statusMessage, data.inputCollapsed, data.outputCollapsed, data.custom); } } @@ -1431,9 +1430,26 @@ export namespace NotebookDocumentMetadata { } export function to(data: notebooks.NotebookDocumentMetadata): types.NotebookDocumentMetadata { - return new types.NotebookDocumentMetadata(data.editable, data.cellEditable, data.cellHasExecutionOrder, data.custom, data.runState, data.trusted); + return new types.NotebookDocumentMetadata(data.editable, data.cellEditable, data.cellHasExecutionOrder, data.custom, data.trusted); + } +} + +export namespace NotebookCellPreviousExecutionResult { + export function to(data: notebooks.NotebookCellMetadata): vscode.NotebookCellExecutionSummary { + return { + duration: data.lastRunDuration, + executionOrder: data.executionOrder, + success: data.lastRunSuccess + }; } + export function from(data: vscode.NotebookCellExecutionSummary): Partial { + return { + lastRunSuccess: data.success, + lastRunDuration: data.duration, + executionOrder: data.executionOrder + }; + } } export namespace NotebookCellKind { @@ -1465,7 +1481,10 @@ export namespace NotebookCellData { cellKind: NotebookCellKind.from(data.kind), language: data.language, source: data.source, - metadata: data.metadata, + metadata: { + ...data.metadata, + ...NotebookCellPreviousExecutionResult.from(data.latestExecutionSummary ?? {}) + }, outputs: data.outputs ? data.outputs.map(NotebookCellOutput.from) : [] }; } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index bef6a7bf01c..8b23d243e99 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -707,7 +707,7 @@ export class WorkspaceEdit implements vscode.WorkspaceEdit { } replaceNotebookCellMetadata(uri: URI, index: number, cellMetadata: vscode.NotebookCellMetadata, metadata?: vscode.WorkspaceEditEntryMetadata): void { - this._edits.push({ _type: FileEditType.Cell, metadata, uri, edit: { editType: CellEditType.Metadata, index, metadata: cellMetadata } }); + this._edits.push({ _type: FileEditType.Cell, metadata, uri, edit: { editType: CellEditType.PartialMetadata, index, metadata: cellMetadata } }); } // --- text @@ -2930,11 +2930,7 @@ export class NotebookCellMetadata { readonly editable?: boolean, readonly breakpointMargin?: boolean, readonly hasExecutionOrder?: boolean, - readonly executionOrder?: number, - readonly runState?: NotebookCellRunState, - readonly runStartTime?: number, readonly statusMessage?: string, - readonly lastRunDuration?: number, readonly inputCollapsed?: boolean, readonly outputCollapsed?: boolean, readonly custom?: Record, @@ -2944,17 +2940,13 @@ export class NotebookCellMetadata { editable?: boolean | null, breakpointMargin?: boolean | null, hasExecutionOrder?: boolean | null, - executionOrder?: number | null, - runState?: NotebookCellRunState | null, - runStartTime?: number | null, statusMessage?: string | null, - lastRunDuration?: number | null, inputCollapsed?: boolean | null, outputCollapsed?: boolean | null, custom?: Record | null, }): NotebookCellMetadata { - let { editable, breakpointMargin, hasExecutionOrder, executionOrder, runState, runStartTime, statusMessage, lastRunDuration, inputCollapsed, outputCollapsed, custom } = change; + let { editable, breakpointMargin, hasExecutionOrder, statusMessage, inputCollapsed, outputCollapsed, custom } = change; if (editable === undefined) { editable = this.editable; @@ -2971,31 +2963,11 @@ export class NotebookCellMetadata { } else if (hasExecutionOrder === null) { hasExecutionOrder = undefined; } - if (executionOrder === undefined) { - executionOrder = this.executionOrder; - } else if (executionOrder === null) { - executionOrder = undefined; - } - if (runState === undefined) { - runState = this.runState; - } else if (runState === null) { - runState = undefined; - } - if (runStartTime === undefined) { - runStartTime = this.runStartTime; - } else if (runStartTime === null) { - runStartTime = undefined; - } if (statusMessage === undefined) { statusMessage = this.statusMessage; } else if (statusMessage === null) { statusMessage = undefined; } - if (lastRunDuration === undefined) { - lastRunDuration = this.lastRunDuration; - } else if (lastRunDuration === null) { - lastRunDuration = undefined; - } if (inputCollapsed === undefined) { inputCollapsed = this.inputCollapsed; } else if (inputCollapsed === null) { @@ -3015,11 +2987,7 @@ export class NotebookCellMetadata { if (editable === this.editable && breakpointMargin === this.breakpointMargin && hasExecutionOrder === this.hasExecutionOrder && - executionOrder === this.executionOrder && - runState === this.runState && - runStartTime === this.runStartTime && statusMessage === this.statusMessage && - lastRunDuration === this.lastRunDuration && inputCollapsed === this.inputCollapsed && outputCollapsed === this.outputCollapsed && custom === this.custom @@ -3031,11 +2999,7 @@ export class NotebookCellMetadata { editable, breakpointMargin, hasExecutionOrder, - executionOrder, - runState, - runStartTime, statusMessage, - lastRunDuration, inputCollapsed, outputCollapsed, custom, @@ -3050,7 +3014,6 @@ export class NotebookDocumentMetadata { readonly cellEditable: boolean = true, readonly cellHasExecutionOrder: boolean = true, readonly custom: { [key: string]: any; } = {}, - readonly runState: NotebookRunState = NotebookRunState.Idle, readonly trusted: boolean = true, ) { } @@ -3059,11 +3022,10 @@ export class NotebookDocumentMetadata { cellEditable?: boolean | null, cellHasExecutionOrder?: boolean | null, custom?: { [key: string]: any; } | null, - runState?: NotebookRunState | null, trusted?: boolean | null, }): NotebookDocumentMetadata { - let { editable, cellEditable, cellHasExecutionOrder, custom, runState, trusted } = change; + let { editable, cellEditable, cellHasExecutionOrder, custom, trusted } = change; if (editable === undefined) { editable = this.editable; @@ -3085,11 +3047,6 @@ export class NotebookDocumentMetadata { } else if (custom === null) { custom = undefined; } - if (runState === undefined) { - runState = this.runState; - } else if (runState === null) { - runState = undefined; - } if (trusted === undefined) { trusted = this.trusted; } else if (trusted === null) { @@ -3100,7 +3057,6 @@ export class NotebookDocumentMetadata { cellEditable === this.cellEditable && cellHasExecutionOrder === this.cellHasExecutionOrder && custom === this.custom && - runState === this.runState && trusted === this.trusted ) { return this; @@ -3112,7 +3068,6 @@ export class NotebookDocumentMetadata { cellEditable, cellHasExecutionOrder, custom, - runState, trusted ); } @@ -3125,13 +3080,15 @@ export class NotebookCellData { language: string; outputs?: NotebookCellOutput[]; metadata?: NotebookCellMetadata; + lastExecutionSummary?: vscode.NotebookCellExecutionSummary; - constructor(kind: NotebookCellKind, source: string, language: string, outputs?: NotebookCellOutput[], metadata?: NotebookCellMetadata) { + constructor(kind: NotebookCellKind, source: string, language: string, outputs?: NotebookCellOutput[], metadata?: NotebookCellMetadata, lastExecutionSummary?: vscode.NotebookCellExecutionSummary) { this.kind = kind; this.source = source; this.language = language; this.outputs = outputs ?? []; this.metadata = metadata; + this.lastExecutionSummary = lastExecutionSummary; } } @@ -3187,16 +3144,10 @@ export enum NotebookCellKind { Code = 2 } -export enum NotebookCellRunState { - Running = 1, - Idle = 2, - Success = 3, - Error = 4 -} - -export enum NotebookRunState { - Running = 1, - Idle = 2 +export enum NotebookCellExecutionState { + Idle = 1, + Pending = 2, + Executing = 3, } export enum NotebookCellStatusBarAlignment { diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts index b7bf533cdc4..6db06303f13 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts @@ -19,9 +19,9 @@ import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; import { CATEGORIES } from 'vs/workbench/common/actions'; -import { BaseCellRenderTemplate, CellEditState, CellFocusMode, EXECUTE_CELL_COMMAND_ID, EXPAND_CELL_CONTENT_COMMAND_ID, getNotebookEditorFromEditorPane, IActiveNotebookEditor, ICellViewModel, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_RUN_STATE, NOTEBOOK_CELL_TYPE, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_KERNEL_COUNT, NOTEBOOK_OUTPUT_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { BaseCellRenderTemplate, CellEditState, CellFocusMode, EXECUTE_CELL_COMMAND_ID, EXPAND_CELL_CONTENT_COMMAND_ID, getNotebookEditorFromEditorPane, IActiveNotebookEditor, ICellViewModel, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_EXECUTION_STATE, NOTEBOOK_CELL_TYPE, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_KERNEL_COUNT, NOTEBOOK_OUTPUT_FOCUSED, NOTEBOOK_INTERRUPTIBLE_KERNEL, NOTEBOOK_HAS_RUNNING_CELL } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; -import { CellEditType, CellKind, ICellEditOperation, ICellRange, INotebookDocumentFilter, isDocumentExcludePattern, NotebookCellMetadata, NotebookCellRunState, NOTEBOOK_EDITOR_CURSOR_BOUNDARY, SelectionStateType, TransientMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellEditType, CellKind, ICellEditOperation, ICellRange, INotebookDocumentFilter, isDocumentExcludePattern, NotebookCellMetadata, NotebookCellExecutionState, NOTEBOOK_EDITOR_CURSOR_BOUNDARY, TransientMetadata, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -240,7 +240,11 @@ export abstract class NotebookCellAction extends const executeCellCondition = ContextKeyExpr.or( ContextKeyExpr.and( - ContextKeyExpr.notEquals(NOTEBOOK_CELL_RUN_STATE.key, NotebookCellRunState[NotebookCellRunState.Running]), + ContextKeyExpr.or( + ContextKeyExpr.equals(NOTEBOOK_CELL_EXECUTION_STATE.key, 'idle'), + ContextKeyExpr.equals(NOTEBOOK_CELL_EXECUTION_STATE.key, 'succeeded'), + ContextKeyExpr.equals(NOTEBOOK_CELL_EXECUTION_STATE.key, 'failed'), + ), ContextKeyExpr.greater(NOTEBOOK_KERNEL_COUNT.key, 0)), NOTEBOOK_CELL_TYPE.isEqualTo('markdown')); @@ -344,16 +348,21 @@ registerAction2(class ExecuteCell extends NotebookCellAction { } }); +const cellCancelCondition = ContextKeyExpr.or( + ContextKeyExpr.equals(NOTEBOOK_CELL_EXECUTION_STATE.key, 'executing'), + ContextKeyExpr.equals(NOTEBOOK_CELL_EXECUTION_STATE.key, 'pending'), +); + registerAction2(class CancelExecuteCell extends NotebookCellAction { constructor() { super({ id: CANCEL_CELL_COMMAND_ID, - precondition: ContextKeyExpr.equals(NOTEBOOK_CELL_RUN_STATE.key, NotebookCellRunState[NotebookCellRunState.Running]), + precondition: cellCancelCondition, title: localize('notebookActions.cancel', "Stop Cell Execution"), icon: icons.stopIcon, menu: { id: MenuId.NotebookCellExecute, - when: ContextKeyExpr.equals(NOTEBOOK_CELL_RUN_STATE.key, NotebookCellRunState[NotebookCellRunState.Running]), + when: cellCancelCondition, group: 'inline' }, description: { @@ -618,7 +627,7 @@ MenuRegistry.appendMenuItem(MenuId.NotebookToolbar, { }, order: -1, group: 'navigation', - when: ContextKeyExpr.and(NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK.toNegated(), executeNotebookCondition) + when: ContextKeyExpr.and(executeNotebookCondition, ContextKeyExpr.or(NOTEBOOK_INTERRUPTIBLE_KERNEL.toNegated(), NOTEBOOK_HAS_RUNNING_CELL.toNegated())) }); MenuRegistry.appendMenuItem(MenuId.NotebookToolbar, { @@ -629,7 +638,7 @@ MenuRegistry.appendMenuItem(MenuId.NotebookToolbar, { }, order: -1, group: 'navigation', - when: ContextKeyExpr.and(NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK) + when: ContextKeyExpr.and(NOTEBOOK_HAS_RUNNING_CELL, NOTEBOOK_INTERRUPTIBLE_KERNEL) }); registerAction2(class extends NotebookCellAction { @@ -681,7 +690,7 @@ registerAction2(class extends NotebookCellAction { }); async function runCell(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { - if (context.cell.metadata?.runState === NotebookCellRunState.Running) { + if (context.cell.metadata?.runState === NotebookCellExecutionState.Executing) { return; } @@ -1300,11 +1309,11 @@ registerAction2(class extends NotebookCellAction { editor.viewModel.notebookDocument.applyEdits([{ editType: CellEditType.Output, index, outputs: [] }], true, undefined, () => undefined, undefined); - if (context.cell.metadata && context.cell.metadata?.runState !== NotebookCellRunState.Running) { + if (context.cell.metadata && context.cell.metadata?.runState !== NotebookCellExecutionState.Executing) { context.notebookEditor.viewModel.notebookDocument.applyEdits([{ editType: CellEditType.Metadata, index, metadata: { ...context.cell.metadata, - runState: NotebookCellRunState.Idle, + runState: NotebookCellExecutionState.Idle, runStartTime: undefined, lastRunDuration: undefined, statusMessage: undefined, @@ -1515,11 +1524,11 @@ registerAction2(class extends NotebookAction { })), true, undefined, () => undefined, undefined); const clearExecutionMetadataEdits = editor.viewModel.notebookDocument.cells.map((cell, index) => { - if (cell.metadata && cell.metadata?.runState !== NotebookCellRunState.Running) { + if (cell.metadata && cell.metadata?.runState !== NotebookCellExecutionState.Executing) { return { editType: CellEditType.Metadata, index, metadata: { ...cell.metadata, - runState: NotebookCellRunState.Idle, + runState: NotebookCellExecutionState.Idle, runStartTime: undefined, lastRunDuration: undefined, statusMessage: undefined, diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts index 1e3a8dbeb57..e4dd2f0b2fc 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts @@ -383,7 +383,6 @@ abstract class AbstractElementRenderer extends Disposable { private _applySanitizedMetadataChanges(currentMetadata: NotebookCellMetadata, newMetadata: any) { let result: { [key: string]: any } = {}; - let newLangauge: string | undefined = undefined; try { const newMetadataObj = JSON.parse(newMetadata); const keys = new Set([...Object.keys(newMetadataObj)]); @@ -428,25 +427,11 @@ abstract class AbstractElementRenderer extends Disposable { } break; default: - if (key === 'language') { - newLangauge = newMetadataObj[key]; - } result[key] = newMetadataObj[key]; break; } } - if (newLangauge !== undefined && newLangauge !== this.cell.modified!.language) { - const index = this.notebookEditor.textModel!.cells.indexOf(this.cell.modified!.textModel); - this.notebookEditor.textModel!.applyEdits( - [{ editType: CellEditType.CellLanguage, index, language: newLangauge }], - true, - undefined, - () => undefined, - undefined - ); - } - const index = this.notebookEditor.textModel!.cells.indexOf(this.cell.modified!.textModel); if (index < 0) { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index 9e3c54e6c61..30e62116138 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -43,7 +43,7 @@ export const NOTEBOOK_EDITOR_OPEN = new RawContextKey('notebookEditorOp export const NOTEBOOK_CELL_LIST_FOCUSED = new RawContextKey('notebookCellListFocused', false); export const NOTEBOOK_OUTPUT_FOCUSED = new RawContextKey('notebookOutputFocused', false); export const NOTEBOOK_EDITOR_EDITABLE = new RawContextKey('notebookEditable', true); -export const NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK = new RawContextKey('notebookExecuting', false); +export const NOTEBOOK_HAS_RUNNING_CELL = new RawContextKey('notebookHasRunningCell', false); // Diff Editor Keys export const IN_NOTEBOOK_TEXT_DIFF_EDITOR = new RawContextKey('isInNotebookTextDiffEditor', false); @@ -55,13 +55,15 @@ export const NOTEBOOK_CELL_EDITABLE = new RawContextKey('notebookCellEd export const NOTEBOOK_CELL_FOCUSED = new RawContextKey('notebookCellFocused', false); // bool export const NOTEBOOK_CELL_EDITOR_FOCUSED = new RawContextKey('notebookCellEditorFocused', false); // bool export const NOTEBOOK_CELL_MARKDOWN_EDIT_MODE = new RawContextKey('notebookCellMarkdownEditMode', false); // bool -export const NOTEBOOK_CELL_RUN_STATE = new RawContextKey('notebookCellRunState', undefined); // Idle, Running +export type NotebookCellExecutionStateContext = 'idle' | 'pending' | 'executing' | 'succeeded' | 'failed'; +export const NOTEBOOK_CELL_EXECUTION_STATE = new RawContextKey('notebookCellExecutionState', undefined); export const NOTEBOOK_CELL_HAS_OUTPUTS = new RawContextKey('notebookCellHasOutputs', false); // bool export const NOTEBOOK_CELL_INPUT_COLLAPSED = new RawContextKey('notebookCellInputIsCollapsed', false); // bool export const NOTEBOOK_CELL_OUTPUT_COLLAPSED = new RawContextKey('notebookCellOutputIsCollapsed', false); // bool // Kernels export const NOTEBOOK_HAS_MULTIPLE_KERNELS = new RawContextKey('notebookHasMultipleKernels', false); export const NOTEBOOK_KERNEL_COUNT = new RawContextKey('notebookKernelCount', 0); +export const NOTEBOOK_INTERRUPTIBLE_KERNEL = new RawContextKey('notebookInterruptibleKernel', false); //#endregion diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorKernelManager.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorKernelManager.ts index ebc29bb5ba6..9f3a0df42f0 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorKernelManager.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorKernelManager.ts @@ -8,24 +8,25 @@ import { IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/ import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { Memento } from 'vs/workbench/common/memento'; -import { ICellViewModel, NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK, NOTEBOOK_HAS_MULTIPLE_KERNELS, NOTEBOOK_KERNEL_COUNT } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { ICellViewModel, NOTEBOOK_HAS_MULTIPLE_KERNELS, NOTEBOOK_HAS_RUNNING_CELL, NOTEBOOK_INTERRUPTIBLE_KERNEL, NOTEBOOK_KERNEL_COUNT } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { configureKernelIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; import { NotebookKernelProviderAssociation, NotebookKernelProviderAssociations, notebookKernelProviderAssociationsSettingId } from 'vs/workbench/contrib/notebook/browser/notebookKernelAssociation'; -import { NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; -import { CellKind, INotebookKernel, NotebookCellRunState, NotebookRunState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellViewModel, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; +import { cellIndexesToRanges, CellKind, ICellRange, INotebookKernel, NotebookCellExecutionState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider'; const NotebookEditorActiveKernelCache = 'workbench.editor.notebook.activeKernel'; export interface IKernelManagerDelegate { viewModel: NotebookViewModel | undefined; + onDidChangeViewModel: Event; getId(): string; getContributedNotebookProviders(resource?: URI): readonly NotebookProviderInfo[]; getContributedNotebookProvider(viewType: string): NotebookProviderInfo | undefined; @@ -46,9 +47,14 @@ export class NotebookEditorKernelManager extends Disposable { private _contributedKernelsComputePromise: CancelablePromise | null = null; private _initialKernelComputationDone: boolean = false; - private readonly _notebookExecuting: IContextKey; private readonly _notebookHasMultipleKernels: IContextKey; private readonly _notebookKernelCount: IContextKey; + private readonly _interruptibleKernel: IContextKey; + private readonly _someCellRunning: IContextKey; + + private _cellStateListeners: IDisposable[] = []; + private _executionCount = 0; + private _viewModelDisposables: DisposableStore; get activeKernel() { return this._activeKernel; @@ -67,6 +73,8 @@ export class NotebookEditorKernelManager extends Disposable { return; } + this._interruptibleKernel.set(!!kernel?.implementsInterrupt); + this._activeKernel = kernel; this._activeKernelResolvePromise = undefined; @@ -104,9 +112,47 @@ export class NotebookEditorKernelManager extends Disposable { this._activeKernelMemento = new Memento(NotebookEditorActiveKernelCache, storageService); - this._notebookExecuting = NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK.bindTo(contextKeyService); this._notebookHasMultipleKernels = NOTEBOOK_HAS_MULTIPLE_KERNELS.bindTo(contextKeyService); this._notebookKernelCount = NOTEBOOK_KERNEL_COUNT.bindTo(contextKeyService); + this._interruptibleKernel = NOTEBOOK_INTERRUPTIBLE_KERNEL.bindTo(contextKeyService); + this._someCellRunning = NOTEBOOK_HAS_RUNNING_CELL.bindTo(contextKeyService); + + this._viewModelDisposables = this._register(new DisposableStore()); + this._register(this._delegate.onDidChangeViewModel(() => { + this._viewModelDisposables.clear(); + this.initCellListeners(); + })); + } + + private initCellListeners(): void { + dispose(this._cellStateListeners); + this._cellStateListeners = []; + + if (!this._delegate.viewModel) { + return; + } + + const addCellStateListener = (c: ICellViewModel) => { + return (c as CellViewModel).onDidChangeState(() => { + if (c.metadata?.runState === NotebookCellExecutionState.Pending) { + this._executionCount++; + } else if (c.metadata?.runState === NotebookCellExecutionState.Idle) { + this._executionCount--; + } + + this._someCellRunning.set(this._executionCount > 0); + }); + }; + + this._cellStateListeners = this._delegate.viewModel.viewCells.map(addCellStateListener); + + this._viewModelDisposables.add(this._delegate.viewModel.onDidChangeViewCells(e => { + e.splices.reverse().forEach(splice => { + const [start, deleted, newCells] = splice; + const deletedCells = this._cellStateListeners.splice(start, deleted, ...newCells.map(addCellStateListener)); + dispose(deletedCells); + }); + })); } public async setKernels(tokenSource: CancellationTokenSource) { @@ -167,15 +213,6 @@ export class NotebookEditorKernelManager extends Disposable { return result; } - updateForMetadata(): void { - if (!this._delegate.viewModel) { - return; - } - - const notebookMetadata = this._delegate.viewModel.metadata; - this._notebookExecuting.set(notebookMetadata.runState === NotebookRunState.Running); - } - private async _setKernelsFromProviders(provider: NotebookProviderInfo, kernels: INotebookKernel[], tokenSource: CancellationTokenSource) { const rawAssociations = this._configurationService.getValue(notebookKernelProviderAssociationsSettingId) || []; const userSetKernelProvider = rawAssociations.filter(e => e.viewType === this._delegate.viewModel?.viewType)[0]?.kernelProvider; @@ -366,12 +403,12 @@ export class NotebookEditorKernelManager extends Disposable { return; } - if (this._delegate.viewModel.metadata.runState !== NotebookRunState.Running) { - return; - } - await this._ensureActiveKernel(); - await this._activeKernel?.cancelNotebookCell!(this._delegate.viewModel.uri, undefined); + + const fullRange: ICellRange = { + start: 0, end: this._delegate.viewModel.length + }; + await this._activeKernel?.cancelNotebookCellExecution!(this._delegate.viewModel.uri, [fullRange]); } async executeNotebook(): Promise { @@ -384,8 +421,11 @@ export class NotebookEditorKernelManager extends Disposable { return; } + const fullRange: ICellRange = { + start: 0, end: this._delegate.viewModel.length + }; this._activeKernelExecuted = true; - await this._activeKernel?.executeNotebookCell!(this._delegate.viewModel.uri, undefined); + await this._activeKernel?.executeNotebookCellsRequest(this._delegate.viewModel.uri, [fullRange]); } async cancelNotebookCellExecution(cell: ICellViewModel): Promise { @@ -398,12 +438,15 @@ export class NotebookEditorKernelManager extends Disposable { } const metadata = cell.getEvaluatedMetadata(this._delegate.viewModel.metadata); - if (metadata.runState !== NotebookCellRunState.Running) { + if (metadata.runState === NotebookCellExecutionState.Idle) { return; } await this._ensureActiveKernel(); - await this._activeKernel?.cancelNotebookCell!(this._delegate.viewModel.uri, cell.handle); + + const idx = this._delegate.viewModel.getCellIndex(cell); + const ranges = cellIndexesToRanges([idx]); + await this._activeKernel?.cancelNotebookCellExecution!(this._delegate.viewModel.uri, ranges); } async executeNotebookCell(cell: ICellViewModel): Promise { @@ -416,8 +459,14 @@ export class NotebookEditorKernelManager extends Disposable { throw new Error('Cell is not executable: ' + cell.uri); } + if (!this.activeKernel) { + return; + } + + const idx = this._delegate.viewModel.getCellIndex(cell); + const range = cellIndexesToRanges([idx]); this._activeKernelExecuted = true; - await this._activeKernel?.executeNotebookCell!(this._delegate.viewModel.uri, cell.handle); + await this._activeKernel!.executeNotebookCellsRequest(this._delegate.viewModel.uri, range); } private canExecuteNotebook(): boolean { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index f32df4a8180..5aeb8acb388 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -351,6 +351,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._kernelManger = instantiationService.createInstance(NotebookEditorKernelManager, { getId() { return that.getId(); }, loadKernelPreloads: that._loadKernelPreloads.bind(that), + onDidChangeViewModel: that.onDidChangeModel, get viewModel() { return that.viewModel; }, getContributedNotebookProviders(resource?: URI) { return that.notebookService.getContributedNotebookProviders(resource); @@ -967,8 +968,6 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._editorEditable.set(!!notebookMetadata?.editable); this._overflowContainer.classList.toggle('notebook-editor-editable', !!notebookMetadata?.editable); this.getDomNode().classList.toggle('notebook-editor-editable', !!notebookMetadata?.editable); - - this._kernelManger.updateForMetadata(); } private async _resolveWebview(): Promise | null> { diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellContextKeys.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellContextKeys.ts index be7dbb29180..3acacd15b13 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellContextKeys.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellContextKeys.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { INotebookTextModel, NotebookCellRunState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { NOTEBOOK_CELL_TYPE, NOTEBOOK_VIEW_TYPE, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_RUN_STATE, NOTEBOOK_CELL_HAS_OUTPUTS, CellViewModelStateChangeEvent, CellEditState, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_FOCUSED, INotebookEditor, NOTEBOOK_CELL_EDITOR_FOCUSED, CellFocusMode } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { INotebookTextModel, NotebookCellExecutionState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NOTEBOOK_CELL_TYPE, NOTEBOOK_VIEW_TYPE, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_EXECUTION_STATE, NOTEBOOK_CELL_HAS_OUTPUTS, CellViewModelStateChangeEvent, CellEditState, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_FOCUSED, INotebookEditor, NOTEBOOK_CELL_EDITOR_FOCUSED, CellFocusMode, NotebookCellExecutionStateContext } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { MarkdownCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; @@ -17,7 +17,7 @@ export class CellContextKeyManager extends Disposable { private cellEditable!: IContextKey; private cellFocused!: IContextKey; private cellEditorFocused!: IContextKey; - private cellRunState!: IContextKey; + private cellRunState!: IContextKey; private cellHasOutputs!: IContextKey; private cellContentCollapsed!: IContextKey; private cellOutputCollapsed!: IContextKey; @@ -41,7 +41,7 @@ export class CellContextKeyManager extends Disposable { this.cellFocused = NOTEBOOK_CELL_FOCUSED.bindTo(this.contextKeyService); this.cellEditorFocused = NOTEBOOK_CELL_EDITOR_FOCUSED.bindTo(this.contextKeyService); this.markdownEditMode = NOTEBOOK_CELL_MARKDOWN_EDIT_MODE.bindTo(this.contextKeyService); - this.cellRunState = NOTEBOOK_CELL_RUN_STATE.bindTo(this.contextKeyService); + this.cellRunState = NOTEBOOK_CELL_EXECUTION_STATE.bindTo(this.contextKeyService); this.cellHasOutputs = NOTEBOOK_CELL_HAS_OUTPUTS.bindTo(this.contextKeyService); this.cellContentCollapsed = NOTEBOOK_CELL_INPUT_COLLAPSED.bindTo(this.contextKeyService); this.cellOutputCollapsed = NOTEBOOK_CELL_OUTPUT_COLLAPSED.bindTo(this.contextKeyService); @@ -115,8 +115,20 @@ export class CellContextKeyManager extends Disposable { const metadata = this.element.getEvaluatedMetadata(this.notebookTextModel.metadata); this.cellEditable.set(!!metadata.editable); - const runState = metadata.runState ?? NotebookCellRunState.Idle; - this.cellRunState.set(NotebookCellRunState[runState]); + const runState = metadata.runState ?? NotebookCellExecutionState.Idle; + if (runState === NotebookCellExecutionState.Idle) { + if (metadata.lastRunSuccess === true) { + this.cellRunState.set('succeeded'); + } else if (metadata.lastRunSuccess === false) { + this.cellRunState.set('failed'); + } else { + this.cellRunState.set('idle'); + } + } else if (runState === NotebookCellExecutionState.Executing) { + this.cellRunState.set('executing'); + } else if (runState === NotebookCellExecutionState.Pending) { + this.cellRunState.set('pending'); + } } private updateForEditState() { diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts index b3fdbf16f77..c719479a3ef 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as Codicons from 'vs/base/common/codicons'; import { getPixelRatio, getZoomLevel } from 'vs/base/browser/browser'; import * as DOM from 'vs/base/browser/dom'; import * as aria from 'vs/base/browser/ui/aria/aria'; @@ -48,7 +49,7 @@ import { StatefulMarkdownCell } from 'vs/workbench/contrib/notebook/browser/view import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { MarkdownCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel'; import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; -import { CellEditType, CellKind, NotebookCellMetadata, NotebookCellRunState, NotebookCellsChangeType, ShowCellStatusBarKey } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellEditType, CellKind, NotebookCellMetadata, NotebookCellExecutionState, NotebookCellsChangeType, ShowCellStatusBarKey } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CodiconActionViewItem, createAndFillInActionBarActionsWithVerticalSeparators, VerticalSeparator, VerticalSeparatorViewItem } from './cellActionView'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { errorStateIcon, successStateIcon, unfoldIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; @@ -889,9 +890,9 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende } return this.notebookEditor.viewModel.getCellIndex(element); - }); + }, element.metadata?.lastRunSuccess); - if (metadata.runState === NotebookCellRunState.Running) { + if (metadata.runState === NotebookCellExecutionState.Executing) { if (metadata.runStartTime) { templateData.elementDisposables.add(templateData.timer.start(metadata.runStartTime)); } else { @@ -907,7 +908,7 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende editorOptions.setGlyphMargin(metadata.breakpointMargin); } - if (metadata.runState === NotebookCellRunState.Running) { + if (metadata.runState === NotebookCellExecutionState.Executing) { templateData.progressBar.infinite().show(500); } else { templateData.progressBar.hide(); @@ -1103,8 +1104,9 @@ export class RunStateRenderer { private static readonly MIN_SPINNER_TIME = 200; private spinnerTimer: any | undefined; - private pendingNewState: NotebookCellRunState | undefined; - private lastRunState: NotebookCellRunState | undefined; + private lastRunState: NotebookCellExecutionState | undefined; + private pendingNewState: NotebookCellExecutionState | undefined; + private pendingLastRunSuccess: boolean | undefined; constructor(private readonly element: HTMLElement) { DOM.hide(element); @@ -1117,41 +1119,51 @@ export class RunStateRenderer { } } - renderState(runState: NotebookCellRunState = NotebookCellRunState.Idle, getCellIndex: () => number) { + renderState(runState: NotebookCellExecutionState = NotebookCellExecutionState.Idle, getCellIndex: () => number, lastRunSuccess: boolean | undefined = undefined) { if (this.spinnerTimer) { this.pendingNewState = runState; + this.pendingLastRunSuccess = lastRunSuccess; return; } - if (runState === NotebookCellRunState.Success) { + let runStateTooltip: string | undefined; + if (runState === NotebookCellExecutionState.Idle && lastRunSuccess) { aria.alert(`Code cell at ${getCellIndex()} finishes running successfully`); DOM.reset(this.element, renderIcon(successStateIcon)); - } else if (runState === NotebookCellRunState.Error) { + } else if (runState === NotebookCellExecutionState.Idle && !lastRunSuccess) { aria.alert(`Code cell at ${getCellIndex()} finishes running with errors`); DOM.reset(this.element, renderIcon(errorStateIcon)); - } else if (runState === NotebookCellRunState.Running) { - if (this.lastRunState !== NotebookCellRunState.Running) { + } else if (runState === NotebookCellExecutionState.Executing) { + runStateTooltip = localize('runStateExecuting', "Executing"); + if (this.lastRunState !== NotebookCellExecutionState.Executing) { aria.alert(`Code cell at ${getCellIndex()} starts running`); } DOM.reset(this.element, renderIcon(syncing)); - this.spinnerTimer = setTimeout(() => { this.spinnerTimer = undefined; - if (this.pendingNewState) { - this.renderState(this.pendingNewState, getCellIndex); + if (this.pendingNewState && this.pendingNewState !== runState) { + this.renderState(this.pendingNewState, getCellIndex, this.pendingLastRunSuccess); this.pendingNewState = undefined; } }, RunStateRenderer.MIN_SPINNER_TIME); + } else if (runState === NotebookCellExecutionState.Pending) { + // Not spinning + runStateTooltip = localize('runStatePending', "Pending"); + DOM.reset(this.element, renderIcon(Codicons.Codicon.sync)); } else { this.element.innerText = ''; } - if (runState === NotebookCellRunState.Idle) { + if (runState === NotebookCellExecutionState.Idle && typeof lastRunSuccess !== 'boolean') { DOM.hide(this.element); } else { this.element.style.display = 'flex'; } + if (runStateTooltip) { + this.element.setAttribute('title', runStateTooltip); + } + this.lastRunState = runState; } } diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts index de4ea968280..33de4d08c29 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts @@ -7,7 +7,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; -import { INotebookTextModel, NotebookCellOutputsSplice, NotebookDocumentMetadata, NotebookCellMetadata, ICellEditOperation, CellEditType, CellUri, notebookDocumentMetadataDefaults, diff, NotebookCellsChangeType, ICellDto2, TransientOptions, NotebookTextModelChangedEvent, NotebookRawContentEvent, IOutputDto, ICellOutput, IOutputItemDto, ISelectionState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookTextModel, NotebookCellOutputsSplice, NotebookDocumentMetadata, NotebookCellMetadata, ICellEditOperation, CellEditType, CellUri, notebookDocumentMetadataDefaults, diff, NotebookCellsChangeType, ICellDto2, TransientOptions, NotebookTextModelChangedEvent, NotebookRawContentEvent, IOutputDto, ICellOutput, IOutputItemDto, ISelectionState, NullablePartialNotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ITextSnapshot } from 'vs/editor/common/model'; import { IUndoRedoService, UndoRedoElementType, IUndoRedoElement, IResourceUndoRedoElement, UndoRedoGroup, IWorkspaceUndoRedoElement } from 'vs/platform/undoRedo/common/undoRedo'; import { MoveCellEdit, SpliceCellsEdit, CellMetadataEdit } from 'vs/workbench/contrib/notebook/common/model/cellEdit'; @@ -317,18 +317,40 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel this._operationManager.pushStackElement(label, selectionState, undoRedoGroup, this.alternativeVersionId); } + private _getCellIndexByHandle(handle: number) { + return this.cells.findIndex(c => c.handle === handle); + } + + private _getCellIndexWithOutputIdHandle(outputId: string) { + return this.cells.findIndex(c => !!c.outputs.find(o => o.outputId === outputId)); + } + applyEdits(rawEdits: ICellEditOperation[], synchronous: boolean, beginSelectionState: ISelectionState | undefined, endSelectionsComputer: () => ISelectionState | undefined, undoRedoGroup: UndoRedoGroup | undefined, computeUndoRedo: boolean = true): boolean { this._eventEmitter.beginDeferredEmit(); this.pushStackElement('edit', beginSelectionState, undoRedoGroup); const edits = rawEdits.map((edit, index) => { + let cellIndex: number = -1; + if ('index' in edit) { + cellIndex = edit.index; + } else if ('handle' in edit) { + cellIndex = this._getCellIndexByHandle(edit.handle); + this._assertIndex(cellIndex); + } else if ('outputId' in edit) { + cellIndex = this._getCellIndexWithOutputIdHandle(edit.outputId); + this._assertIndex(cellIndex); + } else if (edit.editType !== CellEditType.DocumentMetadata) { + throw new Error('Invalid cell edit'); + } + return { edit, + cellIndex, end: (edit.editType === CellEditType.DocumentMetadata) ? undefined - : (edit.editType === CellEditType.Replace ? edit.index + edit.count : edit.index), + : (edit.editType === CellEditType.Replace ? edit.index + edit.count : cellIndex), originalIndex: index, }; }).sort((a, b) => { @@ -343,15 +365,15 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel return b.end - a.end || b.originalIndex - a.originalIndex; }); - for (const { edit } of edits) { + for (const { edit, cellIndex } of edits) { switch (edit.editType) { case CellEditType.Replace: this._replaceCells(edit.index, edit.count, edit.cells, synchronous, computeUndoRedo); break; case CellEditType.Output: //TODO@jrieken,@rebornix no event, no undo stop (?) - this._assertIndex(edit.index); - const cell = this._cells[edit.index]; + this._assertIndex(cellIndex); + const cell = this._cells[cellIndex]; if (edit.append) { this._spliceNotebookCellOutputs(cell.handle, [[cell.outputs.length, 0, edit.outputs.map(op => new NotebookCellOutputTextModel(op))]], computeUndoRedo); } else { @@ -360,8 +382,8 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel break; case CellEditType.OutputItems: { - this._assertIndex(edit.index); - const cell = this._cells[edit.index]; + this._assertIndex(cellIndex); + const cell = this._cells[cellIndex]; if (edit.append) { this._appendNotebookCellOutputItems(cell.handle, edit.outputId, edit.items); } else { @@ -374,6 +396,10 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel this._assertIndex(edit.index); this._changeCellMetadata(this._cells[edit.index].handle, edit.metadata, computeUndoRedo); break; + case CellEditType.PartialMetadata: + this._assertIndex(cellIndex); + this._changeCellMetadataPartial(this._cells[cellIndex].handle, edit.metadata, computeUndoRedo); + break; case CellEditType.CellLanguage: this._assertIndex(edit.index); this._changeCellLanguage(this._cells[edit.index].handle, edit.language, computeUndoRedo); @@ -501,16 +527,10 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel } readonly label = 'Update Notebook Metadata'; undo() { - that._updateNotebookMetadata({ - ...oldMetadata, - runState: that.metadata.runState - }, false); + that._updateNotebookMetadata(oldMetadata, false); } redo() { - that._updateNotebookMetadata({ - ...metadata, - runState: that.metadata.runState - }, false); + that._updateNotebookMetadata(metadata, false); } }(), undefined, undefined); } @@ -623,9 +643,26 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel return true; } - private _changeCellMetadata(handle: number, metadata: NotebookCellMetadata, computeUndoRedo: boolean) { - const cell = this._cells.find(cell => cell.handle === handle); + private _changeCellMetadataPartial(handle: number, metadata: NullablePartialNotebookCellMetadata, computeUndoRedo: boolean) { + const cell = this._mapping.get(handle); + if (!cell) { + return; + } + const newMetadata: NotebookCellMetadata = { + ...cell.metadata + }; + let k: keyof NullablePartialNotebookCellMetadata; + for (k in metadata) { + const value = metadata[k] ?? undefined; + newMetadata[k] = value as any; // TS... + } + + return this._changeCellMetadata(handle, newMetadata, computeUndoRedo); + } + + private _changeCellMetadata(handle: number, metadata: NotebookCellMetadata, computeUndoRedo: boolean) { + const cell = this._mapping.get(handle); if (!cell) { return; } @@ -648,12 +685,11 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel } }), undefined, undefined); } - // should be deferred - cell.metadata = metadata; - } else { - cell.metadata = metadata; } + // should be deferred + cell.metadata = metadata; + this._eventEmitter.emit({ kind: NotebookCellsChangeType.ChangeCellMetadata, index: this._cells.indexOf(cell), metadata: cell.metadata, transient: !triggerDirtyChange }, true); } diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index bb9fbece060..d1ce5192aa7 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -61,7 +61,6 @@ export const notebookDocumentMetadataDefaults: Required]: NotebookCellMetadata[Key] | null +}; + +export interface ICellPartialMetadataEdit { + editType: CellEditType.PartialMetadata; + index: number; + metadata: Partial; +} + +export interface ICellPartialMetadataEditByHandle { + editType: CellEditType.PartialMetadata; + handle: number; + metadata: Partial; +} export interface ICellLanguageEdit { editType: CellEditType.CellLanguage; @@ -384,7 +410,8 @@ export interface ICellMoveEdit { newIdx: number; } -export type ICellEditOperation = ICellReplaceEdit | ICellOutputEdit | ICellMetadataEdit | ICellLanguageEdit | IDocumentMetadataEdit | ICellMoveEdit | ICellOutputItemEdit; +export type IImmediateCellEditOperation = ICellOutputEditByHandle | ICellPartialMetadataEditByHandle | ICellOutputItemEdit; +export type ICellEditOperation = IImmediateCellEditOperation | ICellReplaceEdit | ICellOutputEdit | ICellMetadataEdit | ICellPartialMetadataEdit | IDocumentMetadataEdit | ICellMoveEdit | ICellOutputItemEdit | ICellLanguageEdit; export interface NotebookDataDto { readonly cells: ICellDto2[]; @@ -731,10 +758,11 @@ export interface INotebookKernel { isPreferred?: boolean; preloads?: URI[]; supportedLanguages?: string[] + implementsInterrupt?: boolean; resolve(uri: URI, editorId: string, token: CancellationToken): Promise; - executeNotebookCell(uri: URI, handle: number | undefined): Promise; - cancelNotebookCell(uri: URI, handle: number | undefined): Promise; + executeNotebookCellsRequest(uri: URI, ranges: ICellRange[]): Promise; + cancelNotebookCellExecution(uri: URI, ranges: ICellRange[]): Promise; } export interface INotebookKernelProvider { diff --git a/src/vs/workbench/contrib/notebook/test/notebookEditorKernelManager.test.ts b/src/vs/workbench/contrib/notebook/test/notebookEditorKernelManager.test.ts index 9a25a984e5c..ec77c7684f0 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookEditorKernelManager.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookEditorKernelManager.test.ts @@ -17,7 +17,7 @@ import { NOTEBOOK_KERNEL_COUNT } from 'vs/workbench/contrib/notebook/browser/not import { NotebookEditorKernelManager } from 'vs/workbench/contrib/notebook/browser/notebookEditorKernelManager'; import { NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { CellKind, INotebookKernel, IOutputDto, NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, ICellRange, INotebookKernel, IOutputDto, NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { setupInstantiationService, withTestNotebook as _withTestNotebook } from 'vs/workbench/contrib/notebook/test/testNotebookEditor'; import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; import { TestQuickInputService } from 'vs/workbench/test/browser/workbenchTestServices'; @@ -29,13 +29,15 @@ suite('NotebookEditorKernelManager', () => { instantiationService.stub(IQuickInputService, new TestQuickInputService()); const loadKernelPreloads = async () => { }; + const onDidChangeViewModel = () => { }; + const testDelegate = { loadKernelPreloads, onDidChangeViewModel }; async function withTestNotebook(cells: [string, string, CellKind, IOutputDto[], NotebookCellMetadata][], callback: (viewModel: NotebookViewModel, textModel: NotebookTextModel) => void | Promise) { return _withTestNotebook(cells, (editor) => callback(editor.viewModel, editor.viewModel.notebookDocument)); } test('ctor', () => { - instantiationService.createInstance(NotebookEditorKernelManager, {}); + instantiationService.createInstance(NotebookEditorKernelManager, testDelegate); const contextKeyService = instantiationService.get(IContextKeyService); assert.strictEqual(contextKeyService.getContextKeyValue(NOTEBOOK_KERNEL_COUNT.key), 0); @@ -45,7 +47,7 @@ suite('NotebookEditorKernelManager', () => { await withTestNotebook( [], async (viewModel) => { - const kernelManager: NotebookEditorKernelManager = instantiationService.createInstance(NotebookEditorKernelManager, { viewModel, loadKernelPreloads }); + const kernelManager: NotebookEditorKernelManager = instantiationService.createInstance(NotebookEditorKernelManager, { ...testDelegate, ...{ viewModel } }); const cell = viewModel.createCell(1, 'var c = 3', 'javascript', CellKind.Code, {}, [], true); await assertThrowsAsync(async () => await kernelManager.executeNotebookCell(cell)); @@ -56,7 +58,7 @@ suite('NotebookEditorKernelManager', () => { await withTestNotebook( [], async (viewModel) => { - const kernelManager: NotebookEditorKernelManager = instantiationService.createInstance(NotebookEditorKernelManager, { viewModel, loadKernelPreloads }); + const kernelManager: NotebookEditorKernelManager = instantiationService.createInstance(NotebookEditorKernelManager, { ...testDelegate, ...{ viewModel } }); kernelManager.activeKernel = new TestNotebookKernel({ languages: ['testlang'] }); const cell = viewModel.createCell(1, 'var c = 3', 'javascript', CellKind.Code, {}, [], true); @@ -68,10 +70,10 @@ suite('NotebookEditorKernelManager', () => { await withTestNotebook( [], async (viewModel) => { - const kernelManager: NotebookEditorKernelManager = instantiationService.createInstance(NotebookEditorKernelManager, { viewModel, loadKernelPreloads }); + const kernelManager: NotebookEditorKernelManager = instantiationService.createInstance(NotebookEditorKernelManager, { ...testDelegate, ...{ viewModel } }); const kernel = new TestNotebookKernel({ languages: ['javascript'] }); const executeSpy = sinon.spy(); - kernel.executeNotebookCell = executeSpy; + kernel.executeNotebookCellsRequest = executeSpy; kernelManager.activeKernel = kernel; const cell = viewModel.createCell(0, 'var c = 3', 'javascript', CellKind.Code, {}, [], true); @@ -94,10 +96,10 @@ class TestNotebookKernel implements INotebookKernel { preloads?: URI[] | undefined; supportedLanguages?: string[] | undefined; async resolve(uri: URI, editorId: string, token: CancellationToken): Promise { } - executeNotebookCell(uri: URI, handle: number | undefined): Promise { + executeNotebookCellsRequest(uri: URI, ranges: ICellRange[]): Promise { throw new Error('Method not implemented.'); } - cancelNotebookCell(uri: URI, handle: number | undefined): Promise { + cancelNotebookCellExecution(): Promise { throw new Error('Method not implemented.'); } diff --git a/src/vs/workbench/contrib/notebook/test/notebookTextModel.test.ts b/src/vs/workbench/contrib/notebook/test/notebookTextModel.test.ts index 1f9790c7cb0..8c2f92a2228 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookTextModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookTextModel.test.ts @@ -253,6 +253,12 @@ suite('NotebookTextModel', () => { }], true, undefined, () => undefined, undefined); }); + textModel.applyEdits([{ + index: 0, + editType: CellEditType.Metadata, + metadata: { executionOrder: 15 }, + }], true, undefined, () => undefined, undefined); + textModel.applyEdits([{ index: 0, editType: CellEditType.Metadata, @@ -261,6 +267,34 @@ suite('NotebookTextModel', () => { assert.equal(textModel.cells.length, 1); assert.equal(textModel.cells[0].metadata?.editable, false); + assert.equal(textModel.cells[0].metadata?.executionOrder, undefined); + } + ); + }); + + test('partial metadata', async function () { + await withTestNotebook( + [ + ['var a = 1;', 'javascript', CellKind.Code, [], { editable: true }], + ], + (editor) => { + const textModel = editor.viewModel.notebookDocument; + + textModel.applyEdits([{ + index: 0, + editType: CellEditType.PartialMetadata, + metadata: { executionOrder: 15 }, + }], true, undefined, () => undefined, undefined); + + textModel.applyEdits([{ + index: 0, + editType: CellEditType.PartialMetadata, + metadata: { editable: false }, + }], true, undefined, () => undefined, undefined); + + assert.strictEqual(textModel.cells.length, 1); + assert.strictEqual(textModel.cells[0].metadata?.editable, false); + assert.strictEqual(textModel.cells[0].metadata?.executionOrder, 15); } ); }); diff --git a/src/vs/workbench/test/browser/api/extHostNotebook.test.ts b/src/vs/workbench/test/browser/api/extHostNotebook.test.ts index f6b79819b66..8477d5ae6d2 100644 --- a/src/vs/workbench/test/browser/api/extHostNotebook.test.ts +++ b/src/vs/workbench/test/browser/api/extHostNotebook.test.ts @@ -13,7 +13,7 @@ import { mock } from 'vs/base/test/common/mock'; import { IModelAddedData, MainContext, MainThreadCommandsShape, MainThreadNotebookShape } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook'; import { ExtHostNotebookDocument } from 'vs/workbench/api/common/extHostNotebookDocument'; -import { CellKind, CellUri, NotebookCellsChangeType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, CellUri, NotebookCellExecutionState, NotebookCellsChangeType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { URI } from 'vs/base/common/uri'; import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; @@ -412,4 +412,30 @@ suite('NotebookCell#Document', function () { assert.strictEqual(first.document.languageId, 'fooLang'); assert.ok(removedDoc === addedDoc); }); + + test('change cell execution state does not trigger onDidChangeMetadata event', async function () { + let didFireOnDidChangeMetadata = false; + let e = extHostNotebooks.onDidChangeCellMetadata(() => { + didFireOnDidChangeMetadata = true; + }); + + const changeExeState = Event.toPromise(extHostNotebooks.onDidChangeNotebookCellExecutionState); + + extHostNotebooks.$acceptModelChanged(notebook.uri, { + versionId: 12, rawEvents: [{ + kind: NotebookCellsChangeType.ChangeCellMetadata, + index: 0, + metadata: { + ...notebook.getCellFromIndex(0)?.internalMetadata, + ...{ + runState: NotebookCellExecutionState.Executing + } + } + }] + }, false); + + await changeExeState; + assert.strictEqual(didFireOnDidChangeMetadata, false); + e.dispose(); + }); }); diff --git a/src/vs/workbench/test/browser/api/extHostTypes.test.ts b/src/vs/workbench/test/browser/api/extHostTypes.test.ts index 55b761b3d49..4a413eb88c2 100644 --- a/src/vs/workbench/test/browser/api/extHostTypes.test.ts +++ b/src/vs/workbench/test/browser/api/extHostTypes.test.ts @@ -655,7 +655,6 @@ suite('ExtHostTypes', function () { assert.strictEqual(obj.cellHasExecutionOrder, notebookDocumentMetadataDefaults.cellHasExecutionOrder); assert.deepStrictEqual(obj.custom, notebookDocumentMetadataDefaults.custom); assert.strictEqual(obj.editable, notebookDocumentMetadataDefaults.editable); - assert.strictEqual(obj.runState, notebookDocumentMetadataDefaults.runState); assert.strictEqual(obj.trusted, notebookDocumentMetadataDefaults.trusted); }); @@ -683,22 +682,4 @@ suite('ExtHostTypes', function () { assert.strictEqual(newObj.custom, undefined); }); - - test('Unable to reset executionOrder of cells #116956', function () { - - let obj = new types.NotebookCellMetadata(); - assert.strictEqual(obj.executionOrder, undefined); - - obj = obj.with({ executionOrder: 23 }); - assert.strictEqual(obj.executionOrder, 23); - - obj = obj.with({ executionOrder: undefined }); - assert.strictEqual(obj.executionOrder, 23); - - obj = obj.with({}); - assert.strictEqual(obj.executionOrder, 23); - - obj = obj.with({ executionOrder: null }); - assert.strictEqual(obj.executionOrder, undefined); - }); });