diff --git a/src/breakpoints/body.tsx b/src/breakpoints/body.tsx index fd28ec41..1d0b94d4 100644 --- a/src/breakpoints/body.tsx +++ b/src/breakpoints/body.tsx @@ -94,9 +94,9 @@ const BreakpointCellComponent = ({ .sort((a, b) => { return a.line - b.line; }) - .map((breakpoint: IDebugger.IBreakpoint) => ( + .map((breakpoint: IDebugger.IBreakpoint, index) => ( diff --git a/src/debugger.ts b/src/debugger.ts index dd4a0365..312808ac 100644 --- a/src/debugger.ts +++ b/src/debugger.ts @@ -96,6 +96,9 @@ export namespace Debugger { readonly sources: Sources.Model; dispose(): void { + if (this._isDisposed) { + return; + } this._isDisposed = true; this._disposed.emit(); } diff --git a/src/handlers/editor.ts b/src/handlers/editor.ts index d241259d..104ab2b0 100644 --- a/src/handlers/editor.ts +++ b/src/handlers/editor.ts @@ -43,7 +43,7 @@ export class EditorHandler implements IDisposable { this.sendEditorBreakpoints(); }, this); - this.setup(); + this.setupEditor(); } isDisposed: boolean; @@ -79,9 +79,9 @@ export class EditorHandler implements IDisposable { return; } this._editorMonitor.dispose(); - this.removeGutterClick(); - Signal.clearData(this); + this.clearEditor(); this.isDisposed = true; + Signal.clearData(this); } protected sendEditorBreakpoints() { @@ -104,7 +104,7 @@ export class EditorHandler implements IDisposable { ); } - protected setup() { + protected setupEditor() { if (!this._editor || this._editor.isDisposed) { return; } @@ -120,11 +120,13 @@ export class EditorHandler implements IDisposable { editor.editor.on('gutterClick', this.onGutterClick); } - protected removeGutterClick() { + protected clearEditor() { if (!this._editor || this._editor.isDisposed) { return; } const editor = this._editor as CodeMirrorEditor; + editor.setOption('lineNumbers', false); + editor.editor.setOption('gutters', []); editor.editor.off('gutterClick', this.onGutterClick); } diff --git a/src/handlers/file.ts b/src/handlers/file.ts index 16b32c14..776b058d 100644 --- a/src/handlers/file.ts +++ b/src/handlers/file.ts @@ -1,6 +1,8 @@ // Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. +import { DocumentWidget } from '@jupyterlab/docregistry'; + import { FileEditor } from '@jupyterlab/fileeditor'; import { IDisposable } from '@phosphor/disposable'; @@ -14,7 +16,7 @@ import { IDebugger } from '../tokens'; export class FileHandler implements IDisposable { constructor(options: DebuggerFileHandler.IOptions) { this.debuggerService = options.debuggerService; - this.fileEditor = options.widget; + this.fileEditor = options.widget.content; this.editorHandler = new EditorHandler({ debuggerService: this.debuggerService, @@ -41,6 +43,6 @@ export class FileHandler implements IDisposable { export namespace DebuggerFileHandler { export interface IOptions { debuggerService: IDebugger; - widget: FileEditor; + widget: DocumentWidget; } } diff --git a/src/index.ts b/src/index.ts index 7f931ee7..ade1e43e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -70,40 +70,21 @@ export namespace CommandIDs { export const close = 'debugger:close'; } -async function setDebugSession( - app: JupyterFrontEnd, - debug: IDebugger, - client: IClientSession | Session.ISession -) { - if (!debug.session) { - debug.session = new DebugSession({ client: client }); - } else { - debug.session.client = client; - } - const debuggingEnabled = await debug.requestDebuggingEnabled(); - if (!debuggingEnabled) { - return; - } - await debug.restoreState(true); - app.commands.notifyCommandChanged(); -} - -async function updateToolbar( +/** + * Add a button to the widget toolbar to enable and disable debugging. + * @param debug The debug service. + * @param widget The widget to add the debug toolbar button to. + */ +function updateToolbar( widget: NotebookPanel | ConsolePanel | DocumentWidget, - debug: IDebugger + onClick: () => void ) { - const isDebuggingEnabled = await debug.requestDebuggingEnabled(); - if (!isDebuggingEnabled) { - return; - } - - const toggleAttribute = () => { - if (debug.session.debuggedClients.has(debug.session.client.path)) { - widget.node.setAttribute('data-jp-debugger', 'true'); - } else { - widget.node.removeAttribute('data-jp-debugger'); - } - }; + const button = new ToolbarButton({ + className: 'jp-DebuggerSwitchButton', + iconClassName: 'jp-ToggleSwitch', + onClick, + tooltip: 'Enable / Disable Debugger' + }); const getToolbar = (): Toolbar => { if (!(widget instanceof ConsolePanel)) { @@ -114,86 +95,123 @@ async function updateToolbar( }; const toolbar = getToolbar(); - - const isToolbarNotExist = toolbar.addItem( - 'debugger-lifeCycle-button', - new ToolbarButton({ - className: 'jp-DebuggerSwitchButton', - iconClassName: 'jp-ToggleSwitch', - onClick: async () => { - if (!debug.session.debuggedClients.delete(debug.session.client.path)) { - debug.session.debuggedClients.add(debug.session.client.path); - await debug.start(); - } else { - await debug.stop(); - } - toggleAttribute(); - }, - tooltip: 'Enable / Disable Debugger' - }) - ); - - if (isToolbarNotExist && widget instanceof ConsolePanel) { + const itemAdded = toolbar.addItem('debugger-button', button); + if (itemAdded && widget instanceof ConsolePanel) { widget.insertWidget(0, toolbar); } - toggleAttribute(); } +/** + * A handler for debugging a widget. + */ class DebuggerHandler< H extends ConsoleHandler | NotebookHandler | FileHandler > { + /** + * Instantiate a new DebuggerHandler. + * @param builder The debug handler builder. + */ constructor(builder: new (option: any) => H) { - this.builder = builder; + this._builder = builder; + } + + /** + * Dispose all the handlers. + * @param debug The debug service. + */ + disposeAll(debug: IDebugger) { + const handlerIds = Object.keys(this._handlers); + if (handlerIds.length === 0) { + return; + } + debug.session.dispose(); + debug.session = null; + handlerIds.forEach(id => { + this._handlers[id].dispose(); + }); + this._handlers = {}; } - async update( + /** + * Update a debug handler for the given widget. + * @param debug The debug service. + * @param widget The widget to update. + */ + async update( debug: IDebugger, - widget: W + widget: W, + client: IClientSession | Session.ISession ): Promise { - const debuggingEnabled = await debug.requestDebuggingEnabled(); - if (!debug.model || !debuggingEnabled || !debug.isStarted) { + const debuggingEnabled = await debug.isAvailable(client); + if (!debug.model || !debuggingEnabled) { return; } - if ( - this.handlers[widget.id] && - !debug.session.debuggedClients.has(debug.session.client.path) - ) { - return debug.stop(); + // update the active debug session + if (!debug.session) { + debug.session = new DebugSession({ client: client }); + } else { + debug.session.client = client; } - const handler = new this.builder({ - debuggerService: debug, - widget - }); - - this.handlers[widget.id] = handler; + const updateAttribute = () => { + if (!debug.isStarted) { + widget.node.removeAttribute('data-jp-debugger'); + return; + } + widget.node.setAttribute('data-jp-debugger', 'true'); + }; - widget.disposed.connect(() => { - handler.dispose(); - delete this.handlers[widget.id]; - }); - debug.model.disposed.connect(async () => { - const handlerIds = Object.keys(this.handlers); - if (handlerIds.length === 0) { + const createHandler = async () => { + if (this._handlers[widget.id]) { return; } - await debug.stop(); - debug.session.dispose(); - debug.session = null; - handlerIds.forEach(id => { - this.handlers[id].dispose(); + this._handlers[widget.id] = new this._builder({ + debuggerService: debug, + widget }); - this.handlers = {}; - }); + updateAttribute(); + }; + + const removeHandler = () => { + const handler = this._handlers[widget.id]; + if (!handler) { + return; + } + handler.dispose(); + delete this._handlers[widget.id]; + updateAttribute(); + }; + + const toggleDebugging = async () => { + if (debug.isStarted) { + await debug.stop(); + removeHandler(); + } else { + await debug.restoreState(true); + await createHandler(); + } + }; - if (!debug.session.debuggedClients.has(debug.session.client.path)) { - return debug.stop(); + await debug.restoreState(false); + updateToolbar(widget, toggleDebugging); + + // check the state of the debug session + if (!debug.isStarted) { + removeHandler(); + return; } + + // if the debugger is started but there is no handler, create a new one + await createHandler(); + + // listen to the disposed signals + widget.disposed.connect(removeHandler); + debug.model.disposed.connect(removeHandler); } - private handlers: { [id: string]: H } = {}; - private builder: new (option: any) => H; + private _handlers: { [id: string]: H } = {}; + private _builder: new (option: any) => H; } /** @@ -205,15 +223,17 @@ const consoles: JupyterFrontEndPlugin = { requires: [IDebugger, ILabShell], activate: (app: JupyterFrontEnd, debug: IDebugger, labShell: ILabShell) => { const handler = new DebuggerHandler(ConsoleHandler); + debug.model.disposed.connect(() => { + handler.disposeAll(debug); + }); labShell.currentChanged.connect(async (_, update) => { const widget = update.newValue; if (!(widget instanceof ConsolePanel)) { return; } - await setDebugSession(app, debug, widget.session); - void handler.update(debug, widget); - void updateToolbar(widget, debug); + await handler.update(debug, widget, widget.session); + app.commands.notifyCommandChanged(); }); } }; @@ -232,6 +252,9 @@ const files: JupyterFrontEndPlugin = { labShell: ILabShell ) => { const handler = new DebuggerHandler(FileHandler); + debug.model.disposed.connect(() => { + handler.disposeAll(debug); + }); const activeSessions: { [id: string]: Session.ISession; @@ -260,9 +283,8 @@ const files: JupyterFrontEndPlugin = { session = sessions.connectTo(model); activeSessions[model.id] = session; } - await setDebugSession(app, debug, session); - void handler.update(debug, content); - void updateToolbar(widget, debug); + await handler.update(debug, widget, session); + app.commands.notifyCommandChanged(); } catch { return; } @@ -279,15 +301,17 @@ const notebooks: JupyterFrontEndPlugin = { requires: [IDebugger, ILabShell], activate: (app: JupyterFrontEnd, debug: IDebugger, labShell: ILabShell) => { const handler = new DebuggerHandler(NotebookHandler); + debug.model.disposed.connect(() => { + handler.disposeAll(debug); + }); labShell.currentChanged.connect(async (_, update) => { const widget = update.newValue; if (!(widget instanceof NotebookPanel)) { return; } - await setDebugSession(app, debug, widget.session); - void handler.update(debug, widget); - void updateToolbar(widget, debug); + await handler.update(debug, widget, widget.session); + app.commands.notifyCommandChanged(); }); } }; @@ -455,7 +479,6 @@ const main: JupyterFrontEndPlugin = { commands.notifyCommandChanged(); }); - shell.add(sidebar, 'right'); if (labShell.currentWidget) { labShell.currentWidget.activate(); } @@ -464,7 +487,7 @@ const main: JupyterFrontEndPlugin = { restorer.add(sidebar, 'debugger-sidebar'); } - await service.restoreState(true); + shell.add(sidebar, 'right'); } }); diff --git a/src/service.ts b/src/service.ts index 2161a6cf..5cab2793 100644 --- a/src/service.ts +++ b/src/service.ts @@ -1,6 +1,10 @@ // Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. +import { ClientSession, IClientSession } from '@jupyterlab/apputils'; + +import { Session } from '@jupyterlab/services'; + import { IDisposable } from '@phosphor/disposable'; import { ISignal, Signal } from '@phosphor/signaling'; @@ -118,6 +122,21 @@ export class DebugService implements IDebugger, IDisposable { return this._eventMessage; } + /** + * Request whether debugging is available for the given client. + * @param client The client session. + */ + async isAvailable( + client: IClientSession | Session.ISession + ): Promise { + if (client instanceof ClientSession) { + await client.ready; + } + await client.kernel.ready; + const info = (client.kernel?.info as IDebugger.ISession.IInfoReply) ?? null; + return !!(info?.debugger ?? false); + } + /** * Dispose the debug service. */ @@ -143,17 +162,9 @@ export class DebugService implements IDebugger, IDisposable { return this._model?.stoppedThreads.size > 0 ?? false; } - /** - * Request whether debugging is enabled for the current session. - */ - async requestDebuggingEnabled(): Promise { - const kernelInfo = await this._session?.requestKernelInfo(); - return kernelInfo?.debugger ?? false; - } - /** * Starts a debugger. - * Precondition: !isStarted() + * Precondition: !isStarted */ async start(): Promise { await this.session.start(); @@ -161,23 +172,26 @@ export class DebugService implements IDebugger, IDisposable { /** * Stops the debugger. - * Precondition: isStarted() + * Precondition: isStarted */ async stop(): Promise { await this.session.stop(); if (this.model) { + // TODO: create a more generic cleanup method? this._model.stoppedThreads.clear(); + const breakpoints = new Map(); + this._model.breakpoints.restoreBreakpoints(breakpoints); + this._clearModel(); } } /** * Restarts the debugger. - * Precondition: isStarted(). + * Precondition: isStarted. */ async restart(): Promise { const breakpoints = this._model.breakpoints.breakpoints; await this.stop(); - this._clearModel(); await this.start(); // No need to dump the cells again, we can simply @@ -187,6 +201,7 @@ export class DebugService implements IDebugger, IDisposable { const sourceBreakpoints = Private.toSourceBreakpoints(bps); await this._setBreakpoints(sourceBreakpoints, source); } + this._model.breakpoints.restoreBreakpoints(breakpoints); } /** @@ -223,11 +238,6 @@ export class DebugService implements IDebugger, IDisposable { }); } - // able resotre breakpoints after realod if had before breakpoints and enable lifecycle debugging - if (bpMap.size > 0 && autoStart) { - this.session.debuggedClients.add(this.session.client.path); - } - const stoppedThreads = new Set(reply.body.stoppedThreads); this._model.stoppedThreads = stoppedThreads; @@ -235,10 +245,6 @@ export class DebugService implements IDebugger, IDisposable { await this.start(); } - if (!this.session.debuggedClients.has(this.session.client.path)) { - return; - } - this._model.breakpoints.restoreBreakpoints(bpMap); if (stoppedThreads.size !== 0) { await this._getAllFrames(); diff --git a/src/session.ts b/src/session.ts index faf04ddc..b498ebfc 100644 --- a/src/session.ts +++ b/src/session.ts @@ -73,19 +73,6 @@ export class DebugSession implements IDebugger.ISession { } } - get debuggedClients(): Set { - return this._statesClient; - } - - /** - * Return the kernel info for the debug session, waiting for the - * kernel to be ready. - */ - async requestKernelInfo(): Promise { - await this._ready; - return (this.client.kernel?.info as IDebugger.ISession.IInfoReply) ?? null; - } - /** * Whether the debug session is started */ @@ -104,7 +91,7 @@ export class DebugSession implements IDebugger.ISession { * Dispose the debug session. */ dispose(): void { - if (this.isDisposed) { + if (this._isDisposed) { return; } this._isDisposed = true; @@ -215,17 +202,16 @@ export class DebugSession implements IDebugger.ISession { return reply.promise; } + private _seq = 0; private _client: IClientSession | Session.ISession; private _ready: Promise; + private _isDisposed = false; + private _isStarted = false; private _disposed = new Signal(this); - private _isDisposed: boolean = false; - private _isStarted: boolean = false; private _eventMessage = new Signal< IDebugger.ISession, IDebugger.ISession.Event >(this); - private _seq: number = 0; - private _statesClient: Set = new Set(); } /** diff --git a/src/tokens.ts b/src/tokens.ts index 032299c1..c985caca 100644 --- a/src/tokens.ts +++ b/src/tokens.ts @@ -48,9 +48,10 @@ export interface IDebugger { readonly eventMessage: ISignal; /** - * Request whether debugging is enabled for the current session. + * Request whether debugging is available for the given client. + * @param client The client session. */ - requestDebuggingEnabled(): Promise; + isAvailable(client: IClientSession | Session.ISession): Promise; /** * Computes an id based on the given code. @@ -144,11 +145,6 @@ export namespace IDebugger { */ client: IClientSession | Session.ISession; - /** - * Set of clientes which have enabled debugging. - */ - debuggedClients: Set; - /** * Whether the debug session is started */ @@ -162,12 +158,6 @@ export namespace IDebugger { IDebugger.ISession.Event >; - /** - * Return the kernel info for the debug session, waiting for the - * kernel to be ready. - */ - requestKernelInfo(): Promise; - /** * Start a new debug session. */ diff --git a/tests/src/service.spec.ts b/tests/src/service.spec.ts index c403a590..0ef38542 100644 --- a/tests/src/service.spec.ts +++ b/tests/src/service.spec.ts @@ -14,7 +14,8 @@ import { DebugSession } from '../../lib/session'; import { IDebugger } from '../../lib/tokens'; -describe('Debugging Support', () => { +describe('Debugging support', () => { + const service = new DebugService(); let xpythonClient: IClientSession; let ipykernelClient: IClientSession; @@ -43,20 +44,14 @@ describe('Debugging Support', () => { await Promise.all([xpythonClient.shutdown(), ipykernelClient.shutdown()]); }); - describe('#requestDebuggingEnabled', () => { + describe('#isAvailable', () => { it('should return true for kernels that have support for debugging', async () => { - const debugSession = new DebugSession({ client: xpythonClient }); - let service = new DebugService(); - service.session = debugSession; - const enabled = await service.requestDebuggingEnabled(); + const enabled = await service.isAvailable(xpythonClient); expect(enabled).to.be.true; }); it('should return false for kernels that do not have support for debugging', async () => { - const debugSession = new DebugSession({ client: ipykernelClient }); - let service = new DebugService(); - service.session = debugSession; - const enabled = await service.requestDebuggingEnabled(); + const enabled = await service.isAvailable(ipykernelClient); expect(enabled).to.be.false; }); });