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;
});
});