diff --git a/packages/debugger-extension/schema/main.json b/packages/debugger-extension/schema/main.json index 21cab23dbad8..c1a7046a0733 100644 --- a/packages/debugger-extension/schema/main.json +++ b/packages/debugger-extension/schema/main.json @@ -5,6 +5,13 @@ "jupyter.lab.setting-icon-label": "Debugger", "jupyter.lab.menus": { "main": [ + { + "id": "jp-mainmenu-kernel", + "items": [ + { "type": "separator", "rank": 1.2 }, + { "command": "debugger:restart-debug", "rank": 1.2 } + ] + }, { "id": "jp-mainmenu-view", "items": [ diff --git a/packages/debugger-extension/src/index.ts b/packages/debugger-extension/src/index.ts index 7b92b7cb1baa..2bdbe17bb677 100644 --- a/packages/debugger-extension/src/index.ts +++ b/packages/debugger-extension/src/index.ts @@ -15,6 +15,7 @@ import { ICommandPalette, IThemeManager, MainAreaWidget, + sessionContextDialogs, WidgetTracker } from '@jupyterlab/apputils'; import { IEditorServices } from '@jupyterlab/codeeditor'; @@ -31,7 +32,11 @@ import { import { DocumentWidget } from '@jupyterlab/docregistry'; import { FileEditor, IEditorTracker } from '@jupyterlab/fileeditor'; import { ILoggerRegistry } from '@jupyterlab/logconsole'; -import { INotebookTracker, NotebookPanel } from '@jupyterlab/notebook'; +import { + INotebookTracker, + NotebookActions, + NotebookPanel +} from '@jupyterlab/notebook'; import { standardRendererFactories as initialFactories, RenderMimeRegistry @@ -166,19 +171,49 @@ const files: JupyterFrontEndPlugin = { const notebooks: JupyterFrontEndPlugin = { id: '@jupyterlab/debugger-extension:notebooks', autoStart: true, - requires: [IDebugger, INotebookTracker], - optional: [ILabShell], + requires: [IDebugger, INotebookTracker, ITranslator], + optional: [ILabShell, ICommandPalette], activate: ( app: JupyterFrontEnd, service: IDebugger, notebookTracker: INotebookTracker, - labShell: ILabShell | null + translator: ITranslator, + labShell: ILabShell | null, + palette: ICommandPalette | null ) => { const handler = new Debugger.Handler({ type: 'notebook', shell: app.shell, service }); + + const trans = translator.load('jupyterlab'); + app.commands.addCommand(Debugger.CommandIDs.restartDebug, { + label: trans.__('Restart Kernel and Debug…'), + caption: trans.__('Restart Kernel and Debug…'), + isEnabled: () => { + return service.isStarted; + }, + execute: async () => { + const state = service.getDebuggerState(); + console.log(state.cells); + const { context, content } = notebookTracker.currentWidget!; + + await service.stop(); + const restarted = await sessionContextDialogs!.restart( + context.sessionContext + ); + if (restarted) { + await service.restoreDebuggerState(state); + await handler.updateWidget( + notebookTracker.currentWidget!, + notebookTracker.currentWidget!.sessionContext.session + ); + await NotebookActions.runAll(content, context.sessionContext); + } + } + }); + const updateHandlerAndCommands = async ( widget: NotebookPanel ): Promise => { @@ -199,6 +234,13 @@ const notebooks: JupyterFrontEndPlugin = { return; } + if (palette) { + palette.addItem({ + category: 'Notebook Operations', + command: Debugger.CommandIDs.restartDebug + }); + } + notebookTracker.currentChanged.connect( async (_, notebookPanel: NotebookPanel) => { await updateHandlerAndCommands(notebookPanel); diff --git a/packages/debugger/src/config.ts b/packages/debugger/src/config.ts index c0be39fb3cb1..758ff3a045a0 100644 --- a/packages/debugger/src/config.ts +++ b/packages/debugger/src/config.ts @@ -64,6 +64,15 @@ export class DebuggerConfig implements IDebugger.IConfig { this._fileParams.set(kernel, { kernel, prefix, suffix }); } + /** + * Gets the parameters used for the temp files (e.e. cells) for a kernel. + * + * @param kernel - The kernel name from current session. + */ + getTmpFileParams(kernel: string): IDebugger.IConfig.FileParams { + return this._fileParams.get(kernel)!; + } + private _fileParams = new Map(); private _hashMethods = new Map string>(); } diff --git a/packages/debugger/src/debugger.ts b/packages/debugger/src/debugger.ts index bce7a7fcc41a..874cf4acdcd8 100644 --- a/packages/debugger/src/debugger.ts +++ b/packages/debugger/src/debugger.ts @@ -107,6 +107,8 @@ export namespace Debugger { export const inspectVariable = 'debugger:inspect-variable'; export const evaluate = 'debugger:evaluate'; + + export const restartDebug = 'debugger:restart-debug'; } /** diff --git a/packages/debugger/src/handler.ts b/packages/debugger/src/handler.ts index 42a9dfc950d7..eff2235b096b 100644 --- a/packages/debugger/src/handler.ts +++ b/packages/debugger/src/handler.ts @@ -105,11 +105,11 @@ export class DebuggerHandler { delete this._kernelChangedHandlers[widget.id]; delete this._statusChangedHandlers[widget.id]; delete this._iopubMessageHandlers[widget.id]; - return this._update(widget, connection); + return this.updateWidget(widget, connection); } const kernelChanged = (): void => { - void this._update(widget, connection); + void this.updateWidget(widget, connection); }; const kernelChangedHandler = this._kernelChangedHandlers[widget.id]; @@ -125,7 +125,7 @@ export class DebuggerHandler { ): void => { // FIXME-TRANS: Localizable? if (status.endsWith('restarting')) { - void this._update(widget, connection); + void this.updateWidget(widget, connection); } }; const statusChangedHandler = this._statusChangedHandlers[widget.id]; @@ -156,7 +156,7 @@ export class DebuggerHandler { connection.iopubMessage.connect(iopubMessage); this._iopubMessageHandlers[widget.id] = iopubMessage; - return this._update(widget, connection); + return this.updateWidget(widget, connection); } /** @@ -194,7 +194,7 @@ export class DebuggerHandler { * @param widget The widget to update. * @param connection The session connection. */ - private async _update( + async updateWidget( widget: DebuggerHandler.SessionWidget[DebuggerHandler.SessionType], connection: Session.ISessionConnection | null ): Promise { diff --git a/packages/debugger/src/service.ts b/packages/debugger/src/service.ts index b84243ae323f..59c5fd69ecb1 100644 --- a/packages/debugger/src/service.ts +++ b/packages/debugger/src/service.ts @@ -303,17 +303,7 @@ export class DebuggerService implements IDebugger, IDisposable { const { breakpoints } = this._model.breakpoints; await this.stop(); await this.start(); - - // Re-send the breakpoints to the kernel and update the model. - for (const [source, points] of breakpoints) { - await this._setBreakpoints( - points - .filter(({ line }) => typeof line === 'number') - .map(({ line }) => ({ line: line! })), - source - ); - } - this._model.breakpoints.restoreBreakpoints(breakpoints); + await this._restoreBreakpoints(breakpoints); } /** @@ -471,6 +461,57 @@ export class DebuggerService implements IDebugger, IDisposable { await this.session.sendRequest('configurationDone', {}); } + /** + * Get the debugger state + * + * @returns Debugger state + */ + getDebuggerState(): IDebugger.State { + const breakpoints = this._model.breakpoints.breakpoints; + let cells: string[] = []; + for (const id of breakpoints.keys()) { + const editorList = this._debuggerSources!.find({ + focus: false, + kernel: this.session?.connection?.kernel?.name ?? '', + path: this._session?.connection?.path ?? '', + source: id + }); + const tmp_cells = editorList.map(e => e.model.value.text); + cells = cells.concat(tmp_cells); + } + return { cells, breakpoints }; + } + + /** + * Restore the debugger state + * + * @param state Debugger state + * @returns Whether the state has been restored successfully or not + */ + async restoreDebuggerState(state: IDebugger.State): Promise { + await this.start(); + + for (const cell of state.cells) { + await this._dumpCell(cell); + } + + const breakpoints = new Map(); + const kernel = this.session?.connection?.kernel?.name ?? ''; + const { prefix, suffix } = this._config.getTmpFileParams(kernel); + for (const item of state.breakpoints) { + const [id, list] = item; + const unsuffixedId = id.substr(0, id.length - suffix.length); + const codeHash = unsuffixedId.substr(unsuffixedId.lastIndexOf('/') + 1); + const newId = prefix.concat(codeHash).concat(suffix); + breakpoints.set(newId, list); + } + + await this._restoreBreakpoints(breakpoints); + const config = await this.session!.sendRequest('configurationDone', {}); + await this.restoreState(false); + return config.success; + } + /** * Clear the current model. */ @@ -750,6 +791,26 @@ export class DebuggerService implements IDebugger, IDisposable { }); } + /** + * Re-send the breakpoints to the kernel and update the model. + * + * @param breakpoints The map of breakpoints to send + */ + private async _restoreBreakpoints( + breakpoints: Map + ): Promise { + for (const [source, points] of breakpoints) { + console.log(source); + await this._setBreakpoints( + points + .filter(({ line }) => typeof line === 'number') + .map(({ line }) => ({ line: line! })), + source + ); + } + this._model.breakpoints.restoreBreakpoints(breakpoints); + } + private _config: IDebugger.IConfig; private _debuggerSources: IDebugger.ISources | null; private _eventMessage = new Signal(this); diff --git a/packages/debugger/src/tokens.ts b/packages/debugger/src/tokens.ts index 19dba1dba9de..aad8bbec8572 100644 --- a/packages/debugger/src/tokens.ts +++ b/packages/debugger/src/tokens.ts @@ -153,6 +153,21 @@ export interface IDebugger { breakpoints: IDebugger.IBreakpoint[], path?: string ): Promise; + + /** + * Get the debugger state + * + * @returns Debugger state + */ + getDebuggerState(): IDebugger.State; + + /** + * Restore the debugger state + * + * @param state Debugger state + * @returns Whether the state has been restored successfully or not + */ + restoreDebuggerState(state: IDebugger.State): Promise; } /** @@ -184,6 +199,21 @@ export namespace IDebugger { */ export interface IBreakpoint extends DebugProtocol.Breakpoint {} + /* + * The state of the debugger, used for restoring a debugging session + * after restarting the kernel. + */ + export type State = { + /** + * List of cells to dump after the kernel has restarted + */ + cells: string[]; + /** + * Map of breakpoints to send back to the kernel after it has restarted + */ + breakpoints: Map; + }; + /** * Debugger file and hashing configuration. */ @@ -209,6 +239,13 @@ export namespace IDebugger { * @param params - Temporary file prefix and suffix for a kernel. */ setTmpFileParams(params: IConfig.FileParams): void; + + /** + * Gets the parameters used for the temp files (e.e. cells) for a kernel. + * + * @param kernel - The kernel name from current session. + */ + getTmpFileParams(kernel: string): IConfig.FileParams; } /** diff --git a/ui-tests/reference-output/screenshots/general_opened_menu_kernel.png b/ui-tests/reference-output/screenshots/general_opened_menu_kernel.png index 9da4032369ac..91ce1288420b 100644 Binary files a/ui-tests/reference-output/screenshots/general_opened_menu_kernel.png and b/ui-tests/reference-output/screenshots/general_opened_menu_kernel.png differ diff --git a/ui-tests/reference-output/screenshots/notebook_create_opened_menu_kernel.png b/ui-tests/reference-output/screenshots/notebook_create_opened_menu_kernel.png index 5b79bb34f828..33ed6757d406 100644 Binary files a/ui-tests/reference-output/screenshots/notebook_create_opened_menu_kernel.png and b/ui-tests/reference-output/screenshots/notebook_create_opened_menu_kernel.png differ