diff --git a/extensions/integration-tests/src/notebook.test.ts b/extensions/integration-tests/src/notebook.test.ts index 3423f2ea9a..018e19eb04 100644 --- a/extensions/integration-tests/src/notebook.test.ts +++ b/extensions/integration-tests/src/notebook.test.ts @@ -43,6 +43,40 @@ if (context.RunTest) { // console.log('Python3 NB done'); // }); + // test('Clear all outputs - Python3 notebook ', async function () { + // let notebook = await openNotebook(pySparkNotebookContent, pythonKernelMetadata); + // //Check if at least one cell with output + // let cellWithOutputs = notebook.document.cells.find(cell => cell.contents && cell.contents.outputs && cell.contents.outputs.length > 0); + // console.log("Before clearing cell outputs"); + // if (cellWithOutputs) { + // let clearedOutputs = await notebook.clearAllOutputs(); + // let cells = notebook.document.cells; + // cells.forEach(cell => { + // assert(cell.contents && cell.contents.outputs && cell.contents.outputs.length === 0, `Expected Output: 0, Acutal: '${cell.contents.outputs.length}'`); + // }); + // assert(clearedOutputs, 'Outputs of all the code cells from Python notebook should be cleared'); + // console.log("After clearing cell outputs"); + // } + // assert(cellWithOutputs === undefined, 'Could not find notebook cells with outputs'); + // }); + + test('Clear all outputs - SQL notebook ', async function () { + let notebook = await openNotebook(sqlNotebookContent, sqlKernelMetadata); + let cellWithOutputs = notebook.document.cells.find(cell => cell.contents && cell.contents.outputs && cell.contents.outputs.length > 0); + console.log("Before clearing cell outputs"); + if (cellWithOutputs) { + let clearedOutputs = await notebook.clearAllOutputs(); + let cells = notebook.document.cells; + cells.forEach(cell => { + assert(cell.contents && cell.contents.outputs && cell.contents.outputs.length === 0, `Expected Output: 0, Acutal: '${cell.contents.outputs.length}'`); + }); + assert(clearedOutputs, 'Outputs of all the code cells from SQL notebook should be cleared'); + console.log("After clearing cell outputs"); + } + assert(cellWithOutputs === undefined, 'Could not find notebook cells with outputs'); + }); + + // test('PySpark3 notebook test', async function () { // this.timeout(12000); // let notebook = await openNotebook(pySparkNotebookContent, pySpark3KernelMetadata); diff --git a/src/sql/azdata.proposed.d.ts b/src/sql/azdata.proposed.d.ts index 5d44721d8c..e115da8f59 100644 --- a/src/sql/azdata.proposed.d.ts +++ b/src/sql/azdata.proposed.d.ts @@ -4024,6 +4024,12 @@ declare module 'azdata' { * @return A promise that resolves with a value indicating if the cell was run or not. */ runCell(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. + */ + clearAllOutputs(): Thenable; } export interface NotebookCell { diff --git a/src/sql/parts/notebook/media/dark/clear_results_inverse.svg b/src/sql/parts/notebook/media/dark/clear_results_inverse.svg new file mode 100644 index 0000000000..3b5fb6a3b2 --- /dev/null +++ b/src/sql/parts/notebook/media/dark/clear_results_inverse.svg @@ -0,0 +1 @@ +clear_results_inverse \ No newline at end of file diff --git a/src/sql/parts/notebook/media/light/clear_results.svg b/src/sql/parts/notebook/media/light/clear_results.svg new file mode 100644 index 0000000000..22b0fbc973 --- /dev/null +++ b/src/sql/parts/notebook/media/light/clear_results.svg @@ -0,0 +1 @@ +clear_results \ No newline at end of file diff --git a/src/sql/parts/notebook/notebook.component.ts b/src/sql/parts/notebook/notebook.component.ts index 0f70df95b6..3793db5e55 100644 --- a/src/sql/parts/notebook/notebook.component.ts +++ b/src/sql/parts/notebook/notebook.component.ts @@ -33,7 +33,7 @@ import * as notebookUtils from 'sql/parts/notebook/notebookUtils'; import { Deferred } from 'sql/base/common/promise'; import { IConnectionProfile } from 'sql/platform/connection/common/interfaces'; import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar'; -import { KernelsDropdown, AttachToDropdown, AddCellAction, TrustedAction } from 'sql/parts/notebook/notebookActions'; +import { KernelsDropdown, AttachToDropdown, AddCellAction, TrustedAction, ClearAllOutputsAction } from 'sql/parts/notebook/notebookActions'; import { IObjectExplorerService } from 'sql/workbench/services/objectExplorer/common/objectExplorerService'; import * as TaskUtilities from 'sql/workbench/common/taskUtilities'; import { ISingleNotebookEditOperation } from 'sql/workbench/api/common/sqlExtHostTypes'; @@ -41,6 +41,7 @@ import { IConnectionDialogService } from 'sql/workbench/services/connection/comm import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService'; import { CellMagicMapper } from 'sql/parts/notebook/models/cellMagicMapper'; import { IExtensionsViewlet, VIEWLET_ID } from 'vs/workbench/contrib/extensions/common/extensions'; +import { CellModel } from 'sql/parts/notebook/models/cell'; export const NOTEBOOK_SELECTOR: string = 'notebook-component'; @@ -371,6 +372,8 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe let addTextCellButton = new AddCellAction('notebook.AddTextCell', localize('text', 'Text'), 'notebook-button icon-add'); addTextCellButton.cellType = CellTypes.Markdown; + let clearResultsButton = new ClearAllOutputsAction('notebook.ClearAllOutputs', localize('clearResults', 'Clear Results'), 'notebook-button icon-clear-results'); + this._trustedAction = this.instantiationService.createInstance(TrustedAction, 'notebook.Trusted'); this._trustedAction.enabled = false; @@ -382,7 +385,8 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe { element: attachToContainer }, { action: addCodeCellButton }, { action: addTextCellButton }, - { action: this._trustedAction } + { action: this._trustedAction }, + { action: clearResultsButton } ]); } @@ -479,4 +483,19 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe } } + public async clearAllOutputs(): Promise { + try { + await this.modelReady; + this._model.cells.forEach(cell => { + if (cell.cellType === CellTypes.Code) { + (cell as CellModel).clearOutputs(); + } + }); + return Promise.resolve(true); + } + catch (e) { + return Promise.reject(e); + } + } + } diff --git a/src/sql/parts/notebook/notebook.css b/src/sql/parts/notebook/notebook.css index 9ffc34e8e1..18864289b0 100644 --- a/src/sql/parts/notebook/notebook.css +++ b/src/sql/parts/notebook/notebook.css @@ -68,6 +68,15 @@ background-image: url("./media/dark/nottrusted_inverse.svg"); } +.notebookEditor .notebook-button.icon-clear-results{ + background-image: url("./media/light/clear_results.svg"); +} + +.vs-dark .notebookEditor .notebook-button.icon-clear-results, +.hc-black .notebookEditor .notebook-button.icon-clear-results{ + background-image: url("./media/dark/clear_results_inverse.svg"); +} + .moreActions .action-label.icon.toggle-more { height: 20px; width: 20px; diff --git a/src/sql/parts/notebook/notebookActions.ts b/src/sql/parts/notebook/notebookActions.ts index 409146dc01..e7cf7d5c5e 100644 --- a/src/sql/parts/notebook/notebookActions.ts +++ b/src/sql/parts/notebook/notebookActions.ts @@ -12,7 +12,7 @@ import { INotificationService, Severity, INotificationActions } from 'vs/platfor import { SelectBox, ISelectBoxOptionsWithLabel } from 'sql/base/browser/ui/selectBox/selectBox'; import { INotebookModel } from 'sql/parts/notebook/models/modelInterfaces'; -import { CellType } from 'sql/parts/notebook/models/contracts'; +import { CellType, CellTypes } from 'sql/parts/notebook/models/contracts'; import { NotebookComponent } from 'sql/parts/notebook/notebook.component'; import { getErrorMessage, formatServerNameWithDatabaseNameForAttachTo, getServerFromFormattedAttachToName, getDatabaseFromFormattedAttachToName } from 'sql/parts/notebook/notebookUtils'; import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement'; @@ -21,6 +21,7 @@ import { ConnectionProfile } from 'sql/platform/connection/common/connectionProf import { noKernel } from 'sql/workbench/services/notebook/common/sessionManager'; import { IConnectionDialogService } from 'sql/workbench/services/connection/common/connectionDialogService'; import { NotebookModel } from 'sql/parts/notebook/models/notebookModel'; +import { CellModel } from 'sql/parts/notebook/models/cell'; const msgLoading = localize('loading', "Loading kernels..."); const msgChanging = localize('changing', "Changing kernel..."); @@ -53,6 +54,19 @@ export class AddCellAction extends Action { } } + +// Action to clear outputs of all code cells. +export class ClearAllOutputsAction extends Action { + constructor( + id: string, label: string, cssClass: string + ) { + super(id, label, cssClass); + } + public run(context: NotebookComponent): Promise { + return context.clearAllOutputs(); + } +} + export interface IToggleableState { baseClass?: string; shouldToggleTooltip?: boolean; diff --git a/src/sql/workbench/api/node/extHostNotebookEditor.ts b/src/sql/workbench/api/node/extHostNotebookEditor.ts index dc64f45186..f2dd8ae1e3 100644 --- a/src/sql/workbench/api/node/extHostNotebookEditor.ts +++ b/src/sql/workbench/api/node/extHostNotebookEditor.ts @@ -156,6 +156,10 @@ export class ExtHostNotebookEditor implements azdata.nb.NotebookEditor, IDisposa return this._proxy.$runCell(this._id, uri); } + public clearAllOutputs(): Thenable { + return this._proxy.$clearAllOutputs(this._id); + } + public edit(callback: (editBuilder: azdata.nb.NotebookEditorEdit) => void, options?: { undoStopBefore: boolean; undoStopAfter: boolean; }): Thenable { if (this._disposed) { return Promise.reject(new Error('NotebookEditor#edit not possible on closed editors')); diff --git a/src/sql/workbench/api/node/mainThreadNotebookDocumentsAndEditors.ts b/src/sql/workbench/api/node/mainThreadNotebookDocumentsAndEditors.ts index bb1a18e800..bd66705936 100644 --- a/src/sql/workbench/api/node/mainThreadNotebookDocumentsAndEditors.ts +++ b/src/sql/workbench/api/node/mainThreadNotebookDocumentsAndEditors.ts @@ -125,6 +125,13 @@ class MainThreadNotebookEditor extends Disposable { return this.editor.runCell(cell); } + + public clearAllOutputs(): Promise { + if (!this.editor) { + return Promise.resolve(false); + } + return this.editor.clearAllOutputs(); + } } function wait(timeMs: number): Promise { @@ -359,6 +366,14 @@ export class MainThreadNotebookDocumentsAndEditors extends Disposable implements return editor.runCell(cell); } + $clearAllOutputs(id: string): Promise { + let editor = this.getEditor(id); + if (!editor) { + return Promise.reject(disposed(`TextEditor(${id})`)); + } + return editor.clearAllOutputs(); + } + //#endregion private async doOpenEditor(resource: UriComponents, options: INotebookShowOptions): Promise { diff --git a/src/sql/workbench/api/node/sqlExtHost.protocol.ts b/src/sql/workbench/api/node/sqlExtHost.protocol.ts index d8e474e562..69556593a9 100644 --- a/src/sql/workbench/api/node/sqlExtHost.protocol.ts +++ b/src/sql/workbench/api/node/sqlExtHost.protocol.ts @@ -866,6 +866,7 @@ export interface MainThreadNotebookDocumentsAndEditorsShape extends IDisposable $tryShowNotebookDocument(resource: UriComponents, options: INotebookShowOptions): Promise; $tryApplyEdits(id: string, modelVersionId: number, edits: ISingleNotebookEditOperation[], opts: IUndoStopOptions): Promise; $runCell(id: string, cellUri: UriComponents): Promise; + $clearAllOutputs(id: string): Promise; } export interface ExtHostExtensionManagementShape { diff --git a/src/sql/workbench/services/notebook/common/notebookService.ts b/src/sql/workbench/services/notebook/common/notebookService.ts index 636b57a322..fe855600e8 100644 --- a/src/sql/workbench/services/notebook/common/notebookService.ts +++ b/src/sql/workbench/services/notebook/common/notebookService.ts @@ -109,4 +109,5 @@ export interface INotebookEditor { isVisible(): boolean; executeEdits(edits: ISingleNotebookEditOperation[]): boolean; runCell(cell: ICellModel): Promise; + clearAllOutputs(): Promise; }