diff --git a/packages/notebook-extension/src/index.ts b/packages/notebook-extension/src/index.ts index f02f8388b665..a77823916854 100644 --- a/packages/notebook-extension/src/index.ts +++ b/packages/notebook-extension/src/index.ts @@ -2409,18 +2409,24 @@ function addCommands( }); commands.addCommand(CommandIDs.restartAndRunToSelected, { label: trans.__('Restart Kernel and Run up to Selected Cell…'), - execute: async () => { - const restarted: boolean = await commands.execute(CommandIDs.restart, { - activate: false - }); + execute: async args => { + const current = getCurrent(tracker, shell, { activate: false, ...args }); + if (!current) { + return; + } + const { context, content } = current; + + const cells = content.widgets.slice(0, content.activeCellIndex + 1); + const restarted = await sessionDialogs.restart(current.sessionContext); + if (restarted) { - const executed: boolean = await commands.execute( - CommandIDs.runAllAbove, - { activate: false } + return NotebookActions.runCells( + content, + cells, + context.sessionContext, + sessionDialogs, + translator ); - if (executed) { - return commands.execute(CommandIDs.run); - } } }, isEnabled: isEnabledAndSingleSelected @@ -2428,12 +2434,25 @@ function addCommands( commands.addCommand(CommandIDs.restartRunAll, { label: trans.__('Restart Kernel and Run All Cells…'), caption: trans.__('Restart the kernel and run all cells'), - execute: async () => { - const restarted: boolean = await commands.execute(CommandIDs.restart, { - activate: false - }); + execute: async args => { + const current = getCurrent(tracker, shell, { activate: false, ...args }); + + if (!current) { + return; + } + const { context, content } = current; + + const cells = content.widgets; + const restarted = await sessionDialogs.restart(current.sessionContext); + if (restarted) { - await commands.execute(CommandIDs.runAll); + return NotebookActions.runCells( + content, + cells, + context.sessionContext, + sessionDialogs, + translator + ); } }, isEnabled: args => (args.toolbar ? true : isEnabled()), diff --git a/packages/notebook/src/actions.tsx b/packages/notebook/src/actions.tsx index d9011e02a76c..cfcde5ccd3e0 100644 --- a/packages/notebook/src/actions.tsx +++ b/packages/notebook/src/actions.tsx @@ -552,6 +552,44 @@ export namespace NotebookActions { return promise; } + /** + * Run specified cells. + * + * @param notebook - The target notebook widget. + * @param cells - The cells to run. + * @param sessionContext - The client session object. + * @param sessionDialogs - The session dialogs. + * @param translator - The application translator. + * + * #### Notes + * The existing selection will be preserved. + * The mode will be changed to command. + * An execution error will prevent the remaining code cells from executing. + * All markdown cells will be rendered. + */ + export function runCells( + notebook: Notebook, + cells: readonly Cell[], + sessionContext?: ISessionContext, + sessionDialogs?: ISessionContextDialogs, + translator?: ITranslator + ): Promise { + if (!notebook.model) { + return Promise.resolve(false); + } + + const state = Private.getState(notebook); + const promise = Private.runCells( + notebook, + cells, + sessionContext, + sessionDialogs, + translator + ); + Private.handleRunState(notebook, state, false); + return promise; + } + /** * Run the selected cell(s) and advance to the next cell. * @@ -699,18 +737,19 @@ export namespace NotebookActions { } const state = Private.getState(notebook); + const lastIndex = notebook.widgets.length; - notebook.widgets.forEach(child => { - notebook.select(child); - }); - - const promise = Private.runSelected( + const promise = Private.runCells( notebook, + notebook.widgets, sessionContext, sessionDialogs, translator ); + notebook.activeCellIndex = lastIndex; + notebook.deselectAll(); + Private.handleRunState(notebook, state, true); return promise; } @@ -766,20 +805,16 @@ export namespace NotebookActions { const state = Private.getState(notebook); - notebook.activeCellIndex--; - notebook.deselectAll(); - for (let i = 0; i < notebook.activeCellIndex; ++i) { - notebook.select(notebook.widgets[i]); - } - - const promise = Private.runSelected( + const promise = Private.runCells( notebook, + notebook.widgets.slice(0, notebook.activeCellIndex), sessionContext, sessionDialogs, translator ); - notebook.activeCellIndex++; + notebook.deselectAll(); + Private.handleRunState(notebook, state, true); return promise; } @@ -809,19 +844,19 @@ export namespace NotebookActions { } const state = Private.getState(notebook); + const lastIndex = notebook.widgets.length; - notebook.deselectAll(); - for (let i = notebook.activeCellIndex; i < notebook.widgets.length; ++i) { - notebook.select(notebook.widgets[i]); - } - - const promise = Private.runSelected( + const promise = Private.runCells( notebook, + notebook.widgets.slice(notebook.activeCellIndex), sessionContext, sessionDialogs, translator ); + notebook.activeCellIndex = lastIndex; + notebook.deselectAll(); + Private.handleRunState(notebook, state, true); return promise; } @@ -2245,35 +2280,24 @@ namespace Private { * Run the selected cells. * * @param notebook Notebook + * @param cells Cells to run * @param sessionContext Notebook session context * @param sessionDialogs Session dialogs * @param translator Application translator */ - export function runSelected( + export function runCells( notebook: Notebook, + cells: readonly Cell[], sessionContext?: ISessionContext, sessionDialogs?: ISessionContextDialogs, translator?: ITranslator ): Promise { + const lastCell = cells[-1]; notebook.mode = 'command'; - let lastIndex = notebook.activeCellIndex; - const selected = notebook.widgets.filter((child, index) => { - const active = notebook.isSelectedOrActive(child); - - if (active) { - lastIndex = index; - } - - return active; - }); - - notebook.activeCellIndex = lastIndex; - notebook.deselectAll(); - return Promise.all( - selected.map(child => - runCell(notebook, child, sessionContext, sessionDialogs, translator) + cells.map(cell => + runCell(notebook, cell, sessionContext, sessionDialogs, translator) ) ) .then(results => { @@ -2282,7 +2306,7 @@ namespace Private { } selectionExecuted.emit({ notebook, - lastCell: notebook.widgets[lastIndex] + lastCell }); // Post an update request. notebook.update(); @@ -2291,7 +2315,7 @@ namespace Private { }) .catch(reason => { if (reason.message.startsWith('KernelReplyNotOK')) { - selected.map(cell => { + cells.map(cell => { // Remove '*' prompt from cells that didn't execute if ( cell.model.type === 'code' && @@ -2306,7 +2330,7 @@ namespace Private { selectionExecuted.emit({ notebook, - lastCell: notebook.widgets[lastIndex] + lastCell }); notebook.update(); @@ -2315,6 +2339,45 @@ namespace Private { }); } + /** + * Run the selected cells. + * + * @param notebook Notebook + * @param sessionContext Notebook session context + * @param sessionDialogs Session dialogs + * @param translator Application translator + */ + export function runSelected( + notebook: Notebook, + sessionContext?: ISessionContext, + sessionDialogs?: ISessionContextDialogs, + translator?: ITranslator + ): Promise { + notebook.mode = 'command'; + + let lastIndex = notebook.activeCellIndex; + const selected = notebook.widgets.filter((child, index) => { + const active = notebook.isSelectedOrActive(child); + + if (active) { + lastIndex = index; + } + + return active; + }); + + notebook.activeCellIndex = lastIndex; + notebook.deselectAll(); + + return runCells( + notebook, + selected, + sessionContext, + sessionDialogs, + translator + ); + } + /** * Run a cell. */ diff --git a/packages/notebook/test/actions.spec.ts b/packages/notebook/test/actions.spec.ts index c7f0e8d29ddf..f53914f29f94 100644 --- a/packages/notebook/test/actions.spec.ts +++ b/packages/notebook/test/actions.spec.ts @@ -1000,6 +1000,36 @@ describe('@jupyterlab/notebook', () => { }); }); + describe('#runCells()', () => { + beforeEach(() => { + // Make sure all cells have valid code. + widget.widgets[2].model.sharedModel.setSource('a = 1'); + }); + + it('should change to command mode', async () => { + widget.mode = 'edit'; + const result = await NotebookActions.runCells( + widget, + [widget.widgets[2]], + sessionContext + ); + expect(result).toBe(true); + expect(widget.mode).toBe('command'); + }); + + it('should preserve the existing selection', async () => { + const next = widget.widgets[2]; + widget.select(next); + const result = await NotebookActions.runCells( + widget, + [widget.widgets[1]], + sessionContext + ); + expect(result).toBe(true); + expect(widget.isSelected(widget.widgets[2])).toBe(true); + }); + }); + describe('#runAll()', () => { beforeEach(() => { // Make sure all cells have valid code. @@ -1070,6 +1100,27 @@ describe('@jupyterlab/notebook', () => { }); }); + describe('#runAllBelow()', () => { + it('should run all selected cell and all below', async () => { + const next = widget.widgets[1] as MarkdownCell; + const cell = widget.activeCell as CodeCell; + cell.model.outputs.clear(); + next.rendered = false; + const result = await NotebookActions.runAllBelow( + widget, + sessionContext + ); + expect(result).toBe(true); + expect(cell.model.outputs.length).toBeGreaterThan(0); + expect(next.rendered).toBe(true); + }); + + it('should activate the last cell', async () => { + await NotebookActions.runAllBelow(widget, sessionContext); + expect(widget.activeCellIndex).toBe(widget.widgets.length - 1); + }); + }); + describe('#selectAbove()', () => { it('should select the cell above the active cell', () => { widget.activeCellIndex = 1;