diff --git a/src/debugger.ts b/src/debugger.ts index 17fa9d00..5e668051 100644 --- a/src/debugger.ts +++ b/src/debugger.ts @@ -128,6 +128,22 @@ export namespace Debugger { readonly variablesModel: Variables.Model; readonly connector: IDataConnector | null; + dispose(): void { + this._isDisposed = true; + this._disposed.emit(); + } + + /** + * A signal emitted when the debugger widget is disposed. + */ + get disposed(): ISignal { + return this._disposed; + } + + get isDisposed(): boolean { + return this._isDisposed; + } + get mode(): IDebugger.Mode { return this._mode; } @@ -144,10 +160,6 @@ export namespace Debugger { return this._modeChanged; } - get isDisposed(): boolean { - return this._isDisposed; - } - get codeValue() { return this._codeValue; } @@ -156,10 +168,6 @@ export namespace Debugger { this._codeValue = observableString; } - dispose(): void { - this._isDisposed = true; - } - private async _populate(): Promise { const { connector } = this; @@ -172,6 +180,7 @@ export namespace Debugger { private _isDisposed = false; private _mode: IDebugger.Mode; private _modeChanged = new Signal(this); + private _disposed = new Signal(this); } export namespace Sidebar { diff --git a/src/handlers/cell.ts b/src/handlers/cell.ts index 2fa17c61..6078706a 100644 --- a/src/handlers/cell.ts +++ b/src/handlers/cell.ts @@ -29,11 +29,12 @@ const CELL_CHANGED_TIMEOUT = 1000; export class CellManager implements IDisposable { constructor(options: CellManager.IOptions) { + // TODO: should we use the client name or a debug session id? + this._id = options.debuggerService.session.client.name; this._debuggerService = options.debuggerService; this.onModelChanged(); this._debuggerService.modelChanged.connect(() => this.onModelChanged()); this.activeCell = options.activeCell; - this.onActiveCellChanged(); } isDisposed: boolean; @@ -47,7 +48,7 @@ export class CellManager implements IDisposable { this._debuggerModel.callstackModel.currentFrameChanged.connect( (_, frame) => { - CellManager.cleanupHighlight(this.activeCell); + CellManager.clearHighlight(this.activeCell); if (!frame) { return; } @@ -72,7 +73,7 @@ export class CellManager implements IDisposable { this.addBreakpointsToEditor(this.activeCell); }); - if (this.activeCell) { + if (this.activeCell && !this.activeCell.isDisposed) { this._debuggerModel.codeValue = this.activeCell.model.value; } } @@ -81,15 +82,14 @@ export class CellManager implements IDisposable { if (this.isDisposed) { return; } - if (this.previousCell) { - this.removeListener(this.previousCell); - } if (this._cellMonitor) { this._cellMonitor.dispose(); } - this.removeListener(this.activeCell); - CellManager.cleanupHighlight(this.activeCell); + this.removeGutterClick(this.activeCell); + CellManager.clearHighlight(this.activeCell); + CellManager.clearGutter(this.activeCell); Signal.clearData(this); + this.isDisposed = true; } set previousCell(cell: CodeCell) { @@ -112,15 +112,6 @@ export class CellManager implements IDisposable { return this._activeCell; } - protected clearGutter(cell: CodeCell) { - const editor = cell.editor as CodeMirrorEditor; - editor.doc.eachLine(line => { - if ((line as ILineInfo).gutterMarkers) { - editor.editor.setGutterMarker(line, 'breakpoints', null); - } - }); - } - onActiveCellChanged() { if ( this.activeCell && @@ -133,7 +124,7 @@ export class CellManager implements IDisposable { if (this._cellMonitor) { this._cellMonitor.dispose(); } - this.removeListener(this.previousCell); + this.removeGutterClick(this.previousCell); } this._cellMonitor = new ActivityMonitor({ @@ -145,8 +136,11 @@ export class CellManager implements IDisposable { this.sendEditorBreakpoints(); }, this); + requestAnimationFrame(() => { + this.setEditor(this.activeCell); + }); + this.previousCell = this.activeCell; - this.setEditor(this.activeCell); } } @@ -187,7 +181,7 @@ export class CellManager implements IDisposable { editor.editor.on('gutterClick', this.onGutterClick); } - protected removeListener(cell: CodeCell) { + protected removeGutterClick(cell: CodeCell) { if (cell.isDisposed) { return; } @@ -201,10 +195,14 @@ export class CellManager implements IDisposable { protected onGutterClick = (editor: Editor, lineNumber: number) => { const info = editor.lineInfo(lineNumber); - if (!info) { + + if (!info || this._id !== this._debuggerService.session.client.name) { return; } + editor.focus(); + CellManager.clearGutter(this.activeCell); + const isRemoveGutter = !!info.gutterMarkers; let breakpoints: Breakpoints.IBreakpoint[] = this.getBreakpoints( this._activeCell @@ -228,16 +226,22 @@ export class CellManager implements IDisposable { }; private addBreakpointsToEditor(cell: CodeCell) { - this.clearGutter(cell); const editor = cell.editor as CodeMirrorEditor; const breakpoints = this.getBreakpoints(cell); - breakpoints.forEach(breakpoint => { - editor.editor.setGutterMarker( - breakpoint.line - 1, - 'breakpoints', - Private.createMarkerNode() - ); - }); + if ( + breakpoints.length === 0 && + this._id === this._debuggerService.session.client.name + ) { + CellManager.clearGutter(cell); + } else { + breakpoints.forEach(breakpoint => { + editor.editor.setGutterMarker( + breakpoint.line - 1, + 'breakpoints', + Private.createMarkerNode() + ); + }); + } } private getBreakpointsFromEditor(cell: CodeCell): ILineInfo[] { @@ -263,6 +267,7 @@ export class CellManager implements IDisposable { private breakpointsModel: Breakpoints.Model; private _activeCell: CodeCell; private _debuggerService: IDebugger; + private _id: string; private _cellMonitor: ActivityMonitor< IObservableString, IObservableString.IChangedArgs @@ -285,7 +290,7 @@ export namespace CellManager { */ export function showCurrentLine(cell: Cell, frame: Callstack.IFrame) { const editor = cell.editor as CodeMirrorEditor; - cleanupHighlight(cell); + clearHighlight(cell); editor.editor.addLineClass(frame.line - 1, 'wrap', LINE_HIGHLIGHT_CLASS); } @@ -293,8 +298,8 @@ export namespace CellManager { * Remove all line highlighting indicators for the given cell. * @param cell The cell to cleanup. */ - export function cleanupHighlight(cell: Cell) { - if (!cell || cell.isDisposed) { + export function clearHighlight(cell: Cell) { + if (!cell || cell.isDisposed || !cell.inputArea) { return; } const editor = cell.editor as CodeMirrorEditor; @@ -302,6 +307,23 @@ export namespace CellManager { editor.editor.removeLineClass(line, 'wrap', LINE_HIGHLIGHT_CLASS); }); } + + /** + * Remove line numbers and all gutters from cell. + * @param cell The cell to cleanup. + */ + + export function clearGutter(cell: Cell) { + if (!cell || !cell.inputArea) { + return; + } + const editor = cell.editor as CodeMirrorEditor; + editor.doc.eachLine(line => { + if ((line as ILineInfo).gutterMarkers) { + editor.editor.setGutterMarker(line, 'breakpoints', null); + } + }); + } } export interface ILineInfo { diff --git a/src/handlers/console.ts b/src/handlers/console.ts index 6cefac53..ceb09c9f 100644 --- a/src/handlers/console.ts +++ b/src/handlers/console.ts @@ -56,14 +56,6 @@ export class ConsoleHandler implements IDisposable { if (this.cellManager) { this.cellManager.previousCell = this.cellManager.activeCell; this.cellManager.activeCell = update; - } else if (!this.cellManager) { - this.cellManager = new CellManager({ - activeCell: update, - breakpointsModel: this.breakpoints, - debuggerModel: this.debuggerModel, - debuggerService: this.debuggerService, - type: 'console' - }); } } } diff --git a/src/handlers/notebook.ts b/src/handlers/notebook.ts index 2f1f6d68..d70fcd7c 100644 --- a/src/handlers/notebook.ts +++ b/src/handlers/notebook.ts @@ -1,7 +1,11 @@ // Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. -import { INotebookTracker, NotebookTracker } from '@jupyterlab/notebook'; +import { + INotebookTracker, + NotebookPanel, + NotebookTracker +} from '@jupyterlab/notebook'; import { CodeCell } from '@jupyterlab/cells'; @@ -24,6 +28,8 @@ export class NotebookHandler implements IDisposable { this.debuggerModel = options.debuggerService.model; this.debuggerService = options.debuggerService; this.notebookTracker = options.tracker; + this.notebookPanel = this.notebookTracker.currentWidget; + this.id = options.id; this.breakpoints = this.debuggerModel.breakpointsModel; @@ -53,10 +59,19 @@ export class NotebookHandler implements IDisposable { return; } this.isDisposed = true; + this.cleanAllCells(); this.cellManager.dispose(); Signal.clearData(this); } + protected cleanAllCells() { + const cells = this.notebookPanel.content.widgets; + cells.forEach(cell => { + CellManager.clearHighlight(cell); + CellManager.clearGutter(cell); + }); + } + protected onActiveCellChanged( notebookTracker: NotebookTracker, codeCell: CodeCell @@ -64,10 +79,7 @@ export class NotebookHandler implements IDisposable { if (notebookTracker.currentWidget.id !== this.id) { return; } - // TODO: do we need this requestAnimationFrame? - requestAnimationFrame(() => { - this.cellManager.activeCell = codeCell; - }); + this.cellManager.activeCell = codeCell; } private onCurrentFrameChanged( @@ -80,7 +92,7 @@ export class NotebookHandler implements IDisposable { } const cells = notebook.content.widgets; - cells.forEach(cell => CellManager.cleanupHighlight(cell)); + cells.forEach(cell => CellManager.clearHighlight(cell)); if (!frame) { return; @@ -103,6 +115,7 @@ export class NotebookHandler implements IDisposable { private debuggerService: IDebugger; private breakpoints: Breakpoints.Model; private cellManager: CellManager; + private notebookPanel: NotebookPanel; private id: string; } diff --git a/src/index.ts b/src/index.ts index 2832a6ce..ad4319aa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -68,6 +68,8 @@ export namespace CommandIDs { export const mount = 'debugger:mount'; export const changeMode = 'debugger:change-mode'; + + export const closeDebugger = 'debugger:close'; } async function setDebugSession( @@ -104,6 +106,14 @@ class DebuggerHandler { handler.dispose(); delete this.handlers[widget.id]; }); + + debug.model.disposed.connect(async () => { + await debug.stop(); + Object.keys(this.handlers).forEach(id => { + this.handlers[id].dispose(); + }); + this.handlers = {}; + }); } } @@ -201,7 +211,7 @@ const notebooks: JupyterFrontEndPlugin = { ) => { const handler = new DebuggerHandler(NotebookHandler); - labShell.currentChanged.connect(async (_, update) => { + labShell.activeChanged.connect(async (_, update) => { const widget = update.newValue; if (!(widget instanceof NotebookPanel)) { return; @@ -218,13 +228,14 @@ const notebooks: JupyterFrontEndPlugin = { const main: JupyterFrontEndPlugin = { id: '@jupyterlab/debugger:main', optional: [ILayoutRestorer, ICommandPalette], - requires: [IStateDB, IEditorServices], + requires: [IStateDB, IEditorServices, ILabShell], provides: IDebugger, autoStart: true, activate: ( app: JupyterFrontEnd, state: IStateDB, editorServices: IEditorServices, + labShell: ILabShell, restorer: ILayoutRestorer | null, palette: ICommandPalette | null ): IDebugger => { @@ -241,6 +252,22 @@ const main: JupyterFrontEndPlugin = { let widget: MainAreaWidget; + commands.addCommand(CommandIDs.closeDebugger, { + label: 'Close Debugger', + execute: args => { + if (!widget) { + return; + } + widget.content.sidebar.close(); + widget.dispose(); + } + }); + + app.contextMenu.addItem({ + command: CommandIDs.closeDebugger, + selector: '.jp-DebuggerSidebar' + }); + commands.addCommand(CommandIDs.mount, { execute: async args => { if (!widget) { @@ -275,7 +302,11 @@ const main: JupyterFrontEndPlugin = { sidebar.id = 'jp-debugger-sidebar'; sidebar.title.label = 'Environment'; + shell.add(sidebar, 'right', { activate: false }); + if (labShell.currentWidget) { + labShell.currentWidget.activate(); + } if (restorer) { restorer.add(sidebar, 'debugger-sidebar'); diff --git a/src/service.ts b/src/service.ts index 3df1f743..c1963e81 100644 --- a/src/service.ts +++ b/src/service.ts @@ -178,6 +178,7 @@ export class DebugService implements IDebugger { */ async stop(): Promise { await this.session.stop(); + this._stoppedThreads.clear(); } /** @@ -188,7 +189,6 @@ export class DebugService implements IDebugger { const breakpoints = this.model.breakpointsModel.breakpoints; await this.stop(); this.clearModel(); - this._stoppedThreads.clear(); await this.start(); // No need to dump the cells again, we can simply @@ -234,8 +234,10 @@ export class DebugService implements IDebugger { ); }); } - this._model.breakpointsModel.restoreBreakpoints(bpMap); + if (this._model) { + this._model.breakpointsModel.restoreBreakpoints(bpMap); + } if (!this.isStarted() && autoStart) { await this.start(); } diff --git a/style/breakpoints.css b/style/breakpoints.css index 2c4f2436..960b05ff 100644 --- a/style/breakpoints.css +++ b/style/breakpoints.css @@ -27,7 +27,10 @@ top: -1px; } -.jp-CodeCell.jp-mod-selected .CodeMirror-gutter-wrapper:hover::after, +.jp-Notebook + .jp-mod-editMode + .jp-CodeCell.jp-mod-selected + .CodeMirror-gutter-wrapper:hover::after, .jp-Editor.jp-mod-focused .CodeMirror:not(.jp-mod-readOnly) .CodeMirror-gutter-wrapper:hover::after { diff --git a/style/icons.css b/style/icons.css index efcc19d4..27f7bb90 100644 --- a/style/icons.css +++ b/style/icons.css @@ -15,6 +15,10 @@ background-image: url('icons/activate-breakpoints.svg'); } +[data-jp-theme-light='false'] .jp-DebuggerDeactivateIcon { + background-image: url('icons/md-dark/activate-breakpoints.svg'); +} + .expand-toggle-collapsed { display: inline-block; vertical-align: middle; @@ -35,14 +39,26 @@ background-image: url('icons/step-over.svg'); } +[data-jp-theme-light='false'] .jp-StepOverIcon { + background-image: url('icons/md-dark/step-over.svg'); +} + .jp-StepInIcon { background-image: url('icons/step-into.svg'); } +[data-jp-theme-light='false'] .jp-StepInIcon { + background-image: url('icons/md-dark/step-into.svg'); +} + .jp-StepOutIcon { background-image: url('icons/step-out.svg'); } +[data-jp-theme-light='false'] .jp-StepOutIcon { + background-image: url('icons/md-dark/step-out.svg'); +} + .jp-ClassIcon { background-image: url('icons/class.svg'); } @@ -58,3 +74,7 @@ .jp-CloseAllIcon { background-image: url('icons/close-all.svg'); } + +[data-jp-theme-light='false'] .jp-CloseAllIcon { + background-image: url('icons/md-dark/close-all.svg'); +} diff --git a/style/icons/activate-breakpoints.svg b/style/icons/activate-breakpoints.svg index c07e1be4..06429c66 100644 --- a/style/icons/activate-breakpoints.svg +++ b/style/icons/activate-breakpoints.svg @@ -1,3 +1,3 @@ - + diff --git a/style/icons/md-dark/activate-breakpoints.svg b/style/icons/md-dark/activate-breakpoints.svg new file mode 100644 index 00000000..3bf5ea51 --- /dev/null +++ b/style/icons/md-dark/activate-breakpoints.svg @@ -0,0 +1,3 @@ + + + diff --git a/style/icons/md-dark/close-all.svg b/style/icons/md-dark/close-all.svg new file mode 100644 index 00000000..545c5560 --- /dev/null +++ b/style/icons/md-dark/close-all.svg @@ -0,0 +1,4 @@ + + + + diff --git a/style/icons/md-dark/step-into.svg b/style/icons/md-dark/step-into.svg new file mode 100644 index 00000000..4162a9b4 --- /dev/null +++ b/style/icons/md-dark/step-into.svg @@ -0,0 +1,3 @@ + + + diff --git a/style/icons/md-dark/step-out.svg b/style/icons/md-dark/step-out.svg new file mode 100644 index 00000000..62c0cb1b --- /dev/null +++ b/style/icons/md-dark/step-out.svg @@ -0,0 +1,3 @@ + + + diff --git a/style/icons/md-dark/step-over.svg b/style/icons/md-dark/step-over.svg new file mode 100644 index 00000000..a8b714e7 --- /dev/null +++ b/style/icons/md-dark/step-over.svg @@ -0,0 +1,3 @@ + + +