diff --git a/extensions/integration-tests/src/notebook.test.ts b/extensions/integration-tests/src/notebook.test.ts index da09ae7399..3c9a0a125d 100644 --- a/extensions/integration-tests/src/notebook.test.ts +++ b/extensions/integration-tests/src/notebook.test.ts @@ -36,6 +36,10 @@ if (context.RunTest) { await (new NotebookTester()).sqlNbMultipleCellsTest(this.test.title); }); + test('Clear cell output - SQL notebook', async function () { + await (new NotebookTester()).sqlNbClearOutputs(this.test.title); + }); + test('Clear all outputs - SQL notebook ', async function () { await (new NotebookTester()).sqlNbClearAllOutputs(this.test.title); }); @@ -133,6 +137,11 @@ class NotebookTester { await this.verifyClearAllOutputs(notebook); } + async sqlNbClearOutputs(title: string): Promise { + let notebook = await this.openNotebook(sqlNotebookContent, sqlKernelMetadata, title + this.invocationCount++); + await this.verifyClearOutputs(notebook); + } + @stressify({ dop: NotebookTester.ParallelCount }) async sqlNbMultipleCellsTest(title: string): Promise { let notebook = await this.openNotebook(sqlNotebookMultipleCellsContent, sqlKernelMetadata, title + this.invocationCount++, true); @@ -380,15 +389,22 @@ class NotebookTester { assert(clearedOutputs, 'Outputs of all the code cells from Python notebook should be cleared'); console.log('After clearing cell outputs'); } + + async verifyClearOutputs(notebook: azdata.nb.NotebookEditor): Promise { + let cellWithOutputs = notebook.document.cells[0].contents && notebook.document.cells[0].contents.outputs && notebook.document.cells[0].contents.outputs.length > 0; + assert(cellWithOutputs === true, 'Expected first cell to have outputs'); + let clearedOutputs = await notebook.clearOutput(notebook.document.cells[0]); + let firstCell = notebook.document.cells[0]; + assert(firstCell.contents && firstCell.contents.outputs && firstCell.contents.outputs.length === 0, `Expected Output: 0, Actual: '${firstCell.contents.outputs.length}'`); + assert(clearedOutputs, 'Outputs of requested code cell should be cleared'); + } + async cellLanguageTest(content: azdata.nb.INotebookContents, testName: string, languageConfigured: string, metadataInfo: any) { let notebookJson = Object.assign({}, content, { metadata: metadataInfo }); let uri = writeNotebookToFile(notebookJson, testName); - console.log('Notebook uri ' + uri); let notebook = await azdata.nb.showNotebookDocument(uri); - console.log('Notebook is opened'); await notebook.document.save(); let languageInNotebook = notebook.document.cells[0].contents.metadata.language; - console.log('Language set in cell: ' + languageInNotebook); assert(languageInNotebook === languageConfigured, `Expected cell language is: ${languageConfigured}, Actual: ${languageInNotebook}`); } } diff --git a/extensions/notebook/package.json b/extensions/notebook/package.json index 14368bcdde..c7de136b9f 100644 --- a/extensions/notebook/package.json +++ b/extensions/notebook/package.json @@ -65,6 +65,10 @@ "title": "%notebook.command.runactivecell%", "icon": "resources/dark/touchbar_run_cell.png" }, + { + "command": "notebook.command.clearactivecellresult", + "title": "%notebook.command.clearactivecellresult%" + }, { "command": "notebook.command.runallcells", "title": "%notebook.command.runallcells%" @@ -155,6 +159,10 @@ "command": "notebook.command.runactivecell", "when": "notebookEditorVisible" }, + { + "command": "notebook.command.clearactivecellresult", + "when": "notebookEditorVisible" + }, { "command": "notebook.command.runallcells", "when": "notebookEditorVisible" @@ -234,6 +242,11 @@ "key": "F5", "when": "activeEditor == workbench.editor.notebookEditor" }, + { + "command": "notebook.command.clearactivecellresult", + "key": "Ctrl+Shift+R", + "when": "activeEditor == workbench.editor.notebookEditor" + }, { "command": "notebook.command.runallcells", "key": "Ctrl+Shift+F5", diff --git a/extensions/notebook/package.nls.json b/extensions/notebook/package.nls.json index 347cf607ef..f79a04fb0d 100644 --- a/extensions/notebook/package.nls.json +++ b/extensions/notebook/package.nls.json @@ -10,6 +10,7 @@ "notebook.command.open": "Open Notebook", "notebook.analyzeJupyterNotebook": "Analyze in Notebook", "notebook.command.runactivecell": "Run Cell", + "notebook.command.clearactivecellresult": "Clear Cell Result", "notebook.command.runallcells": "Run Cells", "notebook.command.addcode": "Add Code Cell", "notebook.command.addtext": "Add Text Cell", diff --git a/extensions/notebook/src/extension.ts b/extensions/notebook/src/extension.ts index cd4ae05b90..7b23299103 100644 --- a/extensions/notebook/src/extension.ts +++ b/extensions/notebook/src/extension.ts @@ -42,6 +42,9 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.runallcells', () => { runAllCells(); })); + extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.clearactivecellresult', () => { + clearActiveCellOutput(); + })); extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.addcell', async () => { let cellType: CellType; try { @@ -152,6 +155,19 @@ async function runActiveCell(): Promise { } } +async function clearActiveCellOutput(): Promise { + try { + let notebook = azdata.nb.activeNotebookEditor; + if (notebook) { + await notebook.clearOutput(); + } else { + throw new Error(noNotebookVisible); + } + } catch (err) { + vscode.window.showErrorMessage(getErrorMessage(err)); + } +} + async function runAllCells(): Promise { try { let notebook = azdata.nb.activeNotebookEditor; diff --git a/src/sql/azdata.proposed.d.ts b/src/sql/azdata.proposed.d.ts index 7410fa9576..57175424ce 100644 --- a/src/sql/azdata.proposed.d.ts +++ b/src/sql/azdata.proposed.d.ts @@ -4418,6 +4418,11 @@ declare module 'azdata' { */ runAllCells(): Thenable; + /** + * Clears the outputs of the active code cell in a notebook. + */ + clearOutput(cell?: NotebookCell): Thenable; + /** Clears the outputs of all code cells in a Notebook * @return A promise that resolves with a value indicating if the outputs are cleared or not. */ diff --git a/src/sql/workbench/api/node/extHostNotebookEditor.ts b/src/sql/workbench/api/node/extHostNotebookEditor.ts index f0772d2cf4..c8e45a5659 100644 --- a/src/sql/workbench/api/node/extHostNotebookEditor.ts +++ b/src/sql/workbench/api/node/extHostNotebookEditor.ts @@ -159,6 +159,11 @@ export class ExtHostNotebookEditor implements azdata.nb.NotebookEditor, IDisposa return this._proxy.$runAllCells(this._id); } + public clearOutput(cell: azdata.nb.NotebookCell): Thenable { + let uri = cell ? cell.uri : undefined; + return this._proxy.$clearOutput(this._id, uri); + } + public clearAllOutputs(): Thenable { return this._proxy.$clearAllOutputs(this._id); } diff --git a/src/sql/workbench/api/node/mainThreadNotebookDocumentsAndEditors.ts b/src/sql/workbench/api/node/mainThreadNotebookDocumentsAndEditors.ts index ef0ab2a157..8259ae21b8 100644 --- a/src/sql/workbench/api/node/mainThreadNotebookDocumentsAndEditors.ts +++ b/src/sql/workbench/api/node/mainThreadNotebookDocumentsAndEditors.ts @@ -139,6 +139,13 @@ class MainThreadNotebookEditor extends Disposable { return this.editor.runAllCells(); } + public clearOutput(cell: ICellModel): Promise { + if (!this.editor) { + return Promise.resolve(false); + } + return this.editor.clearOutput(cell); + } + public clearAllOutputs(): Promise { if (!this.editor) { return Promise.resolve(false); @@ -384,6 +391,28 @@ export class MainThreadNotebookDocumentsAndEditors extends Disposable implements return editor.runAllCells(); } + $clearOutput(id: string, cellUri: UriComponents): Promise { + // Requires an editor and the matching cell in that editor + let editor = this.getEditor(id); + if (!editor) { + return Promise.reject(disposed(`TextEditor(${id})`)); + } + let cell: ICellModel; + if (cellUri) { + let uriString = URI.revive(cellUri).toString(); + cell = editor.cells.find(c => c.cellUri.toString() === uriString); + // If it's markdown what should we do? Show notification?? + } else { + // Use the active cell in this case, or 1st cell if there's none active + cell = editor.model.activeCell; + } + if (!cell || (cell && cell.cellType !== CellTypes.Code)) { + return Promise.reject(localize('clearResultActiveCell', "Clear result requires a code cell to be selected. Please select a code cell to run.")); + } + + return editor.clearOutput(cell); + } + $clearAllOutputs(id: string): Promise { let editor = this.getEditor(id); if (!editor) { diff --git a/src/sql/workbench/api/node/sqlExtHost.protocol.ts b/src/sql/workbench/api/node/sqlExtHost.protocol.ts index 6e5ee38fc8..bb9faf2748 100644 --- a/src/sql/workbench/api/node/sqlExtHost.protocol.ts +++ b/src/sql/workbench/api/node/sqlExtHost.protocol.ts @@ -920,6 +920,7 @@ export interface MainThreadNotebookDocumentsAndEditorsShape extends IDisposable $tryApplyEdits(id: string, modelVersionId: number, edits: ISingleNotebookEditOperation[], opts: IUndoStopOptions): Promise; $runCell(id: string, cellUri: UriComponents): Promise; $runAllCells(id: string): Promise; + $clearOutput(id: string, cellUri: UriComponents): Promise; $clearAllOutputs(id: string): Promise; $changeKernel(id: string, kernel: azdata.nb.IKernelInfo): Promise; } diff --git a/src/sql/workbench/parts/notebook/notebook.component.ts b/src/sql/workbench/parts/notebook/notebook.component.ts index c35502838c..83add32747 100644 --- a/src/sql/workbench/parts/notebook/notebook.component.ts +++ b/src/sql/workbench/parts/notebook/notebook.component.ts @@ -535,6 +535,26 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe return true; } + public async clearOutput(cell: ICellModel): Promise { + try { + await this.modelReady; + let uriString = cell.cellUri.toString(); + if (this._model.cells.findIndex(c => c.cellUri.toString() === uriString) > -1) { + this.selectCell(cell); + // Clear outputs of the requested cell if cell type is code cell. + // If cell is markdown cell, clearOutputs() is a no-op + if (cell.cellType === CellTypes.Code) { + (cell as CellModel).clearOutputs(); + } + return true; + } else { + return Promise.reject(new Error(localize('cellNotFound', "cell with URI {0} was not found in this model", uriString))); + } + } catch (e) { + return Promise.reject(e); + } + } + public async clearAllOutputs(): Promise { try { await this.modelReady; diff --git a/src/sql/workbench/services/notebook/common/notebookService.ts b/src/sql/workbench/services/notebook/common/notebookService.ts index b16abf43df..1eb0521419 100644 --- a/src/sql/workbench/services/notebook/common/notebookService.ts +++ b/src/sql/workbench/services/notebook/common/notebookService.ts @@ -129,5 +129,6 @@ export interface INotebookEditor { executeEdits(edits: ISingleNotebookEditOperation[]): boolean; runCell(cell: ICellModel): Promise; runAllCells(): Promise; + clearOutput(cell: ICellModel): Promise; clearAllOutputs(): Promise; }