From 7cf92807e19868d0555331f06752da50efcb06fe Mon Sep 17 00:00:00 2001 From: Borys Palka Date: Tue, 29 Oct 2019 22:58:15 +0100 Subject: [PATCH 01/14] get moreDetials of variable --- src/breakpoints/body.tsx | 4 ++-- src/breakpoints/index.ts | 6 +++--- src/callstack/index.ts | 6 +++--- src/editors/index.ts | 2 +- src/handlers/console.ts | 4 ++-- src/service.ts | 18 ++++++++++++++++++ src/variables/body/index.tsx | 33 +++++++++++++++++++++++++++++---- src/variables/index.ts | 29 ++++++++++++++++++++++++++--- 8 files changed, 84 insertions(+), 18 deletions(-) diff --git a/src/breakpoints/body.tsx b/src/breakpoints/body.tsx index bc0fdd58..4abf80e9 100644 --- a/src/breakpoints/body.tsx +++ b/src/breakpoints/body.tsx @@ -1,11 +1,11 @@ // Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. -import React, { useEffect, useState } from 'react'; -import { Breakpoints } from '.'; import { ReactWidget } from '@jupyterlab/apputils'; import { ArrayExt } from '@phosphor/algorithm'; import { ISignal } from '@phosphor/signaling'; +import React, { useEffect, useState } from 'react'; +import { Breakpoints } from '.'; export class Body extends ReactWidget { constructor(model: Breakpoints.Model) { diff --git a/src/breakpoints/index.ts b/src/breakpoints/index.ts index c9c9ed3c..bff9df96 100644 --- a/src/breakpoints/index.ts +++ b/src/breakpoints/index.ts @@ -3,11 +3,11 @@ import { Toolbar, ToolbarButton } from '@jupyterlab/apputils'; -import { Widget, Panel, PanelLayout } from '@phosphor/widgets'; -import { DebugProtocol } from 'vscode-debugprotocol'; -import { Body } from './body'; import { Signal } from '@phosphor/signaling'; +import { Panel, PanelLayout, Widget } from '@phosphor/widgets'; +import { DebugProtocol } from 'vscode-debugprotocol'; import { ILineInfo } from '../handlers/cell'; +import { Body } from './body'; export class Breakpoints extends Panel { constructor(options: Breakpoints.IOptions) { diff --git a/src/callstack/index.ts b/src/callstack/index.ts index 97c6b737..f893165a 100644 --- a/src/callstack/index.ts +++ b/src/callstack/index.ts @@ -3,10 +3,10 @@ import { Toolbar, ToolbarButton } from '@jupyterlab/apputils'; -import { Widget, Panel, PanelLayout } from '@phosphor/widgets'; -import { Body } from './body'; +import { ISignal, Signal } from '@phosphor/signaling'; +import { Panel, PanelLayout, Widget } from '@phosphor/widgets'; import { DebugProtocol } from 'vscode-debugprotocol'; -import { Signal, ISignal } from '@phosphor/signaling'; +import { Body } from './body'; export class Callstack extends Panel { constructor(options: Callstack.IOptions) { diff --git a/src/editors/index.ts b/src/editors/index.ts index 412e93c0..bf34f2e3 100644 --- a/src/editors/index.ts +++ b/src/editors/index.ts @@ -3,7 +3,7 @@ | Distributed under the terms of the Modified BSD License. |----------------------------------------------------------------------------*/ -import { CodeEditorWrapper, CodeEditor } from '@jupyterlab/codeeditor'; +import { CodeEditor, CodeEditorWrapper } from '@jupyterlab/codeeditor'; import { ISignal, Signal } from '@phosphor/signaling'; diff --git a/src/handlers/console.ts b/src/handlers/console.ts index faffee02..affeea7a 100644 --- a/src/handlers/console.ts +++ b/src/handlers/console.ts @@ -1,7 +1,7 @@ // Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. -import { IConsoleTracker, CodeConsole } from '@jupyterlab/console'; +import { CodeConsole, IConsoleTracker } from '@jupyterlab/console'; import { CellManager } from '../handlers/cell'; @@ -11,9 +11,9 @@ import { Breakpoints } from '../breakpoints'; import { IDebugger } from '../tokens'; -import { Debugger } from '../debugger'; import { IDisposable } from '@phosphor/disposable'; import { Signal } from '@phosphor/signaling'; +import { Debugger } from '../debugger'; export class DebuggerConsoleHandler implements IDisposable { constructor(options: DebuggerConsoleHandler.IOptions) { diff --git a/src/service.ts b/src/service.ts index 2dd90b19..03a194d8 100644 --- a/src/service.ts +++ b/src/service.ts @@ -143,6 +143,7 @@ export class DebugService implements IDebugger { this.session.client.kernel.requestExecute({ code }); await this.getAllFrames(); + this._model.variablesModel.variableExapnded.connect(this.getVariable); } getAllFrames = async () => { @@ -199,6 +200,23 @@ export class DebugService implements IDebugger { return reply.body.scopes; }; + getVariable = async (_: any, variable: DebugProtocol.Variable) => { + if (!variable && variable.variablesReference !== 0) { + return; + } + const reply = await this.session.sendRequest('variables', { + variablesReference: variable.variablesReference + }); + let newVariable = { ...variable }; + reply.body.variables.forEach((ele: DebugProtocol.Variable) => { + newVariable = { [ele.evaluateName]: ele, ...newVariable }; + }); + + this._model.variablesModel.currentVariable = newVariable; + + return reply.body.variables; + }; + getVariables = async (scopes: DebugProtocol.Scope[]) => { if (!scopes || scopes.length === 0) { return; diff --git a/src/variables/body/index.tsx b/src/variables/body/index.tsx index 190ac61e..2f79573c 100644 --- a/src/variables/body/index.tsx +++ b/src/variables/body/index.tsx @@ -3,11 +3,12 @@ import { Variables } from '../index'; -import { ObjectInspector, ObjectLabel, ITheme } from 'react-inspector'; +import { ITheme, ObjectInspector, ObjectLabel } from 'react-inspector'; import { ReactWidget } from '@jupyterlab/apputils'; -import React, { useState, useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; + import { ArrayExt } from '@phosphor/algorithm'; export class Body extends ReactWidget { @@ -28,11 +29,29 @@ const VariableComponent = ({ model }: { model: Variables.Model }) => { const [data, setData] = useState(model.scopes); useEffect(() => { + const convert = (scopes: Variables.IScope[]) => { + const convertet = scopes.map(scope => { + scope.variables = scope.variables.map(variable => { + const func = () => { + void model.getMoreDataOfVariable(variable); + }; + if (variable.haveMoreDetails) { + return { ...variable }; + } else { + return { getMoreDetails: func, ...variable }; + } + }); + return { ...scope }; + }); + return convertet; + }; + const updateScopes = (_: Variables.Model, update: Variables.IScope[]) => { if (ArrayExt.shallowEqual(data, update)) { return; } - setData(update); + const newData = convert(update); + setData(newData); }; model.scopesChanged.connect(updateScopes); @@ -77,8 +96,14 @@ const defaultNodeRenderer = ({ }) => { const label = data.name === '' || data.name == null ? name : data.name; const value = data.value; + let dontDisplay = false; + + if (data instanceof Function) { + data(); + dontDisplay = true; + } - return depth === 0 ? ( + return dontDisplay ? null : depth === 0 ? ( {label} : diff --git a/src/variables/index.ts b/src/variables/index.ts index 012cdef3..1bbf71d7 100644 --- a/src/variables/index.ts +++ b/src/variables/index.ts @@ -1,11 +1,11 @@ // Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. -import { Panel, Widget, PanelLayout } from '@phosphor/widgets'; +import { Panel, PanelLayout, Widget } from '@phosphor/widgets'; import { Body } from './body'; -import { Signal, ISignal } from '@phosphor/signaling'; +import { ISignal, Signal } from '@phosphor/signaling'; import { DebugProtocol } from 'vscode-debugprotocol'; @@ -52,7 +52,10 @@ class VariablesHeader extends Widget { } export namespace Variables { - export interface IVariable extends DebugProtocol.Variable {} + export interface IVariable extends DebugProtocol.Variable { + getMoreDetails?: any; + haveMoreDetails?: boolean; + } export interface IScope { name: string; @@ -76,8 +79,19 @@ export namespace Variables { if (this._currentVariable === variable) { return; } + + variable.haveMoreDetails = true; this._currentVariable = variable; this._currentChanged.emit(variable); + + const newScope = this.scopes.map(scope => { + const findIndex = scope.variables.findIndex( + ele => ele.variablesReference === variable.variablesReference + ); + scope.variables[findIndex] = variable; + return { ...scope }; + }); + this.scopes = [...newScope]; } get scopes(): IScope[] { @@ -101,10 +115,18 @@ export namespace Variables { return this._variablesChanged; } + get variableExapnded(): ISignal { + return this._variableExapnded; + } + getCurrentVariables(): IVariable[] { return this.variables; } + async getMoreDataOfVariable(variable: IVariable) { + this._variableExapnded.emit(variable); + } + protected _state: IScope[]; private _currentVariable: IVariable; @@ -113,6 +135,7 @@ export namespace Variables { private _currentChanged = new Signal(this); private _variablesChanged = new Signal(this); private _scopesChanged = new Signal(this); + private _variableExapnded = new Signal(this); } export interface IOptions extends Panel.IOptions { From 47c54a861b1266ff03cc50e10d8b93e8d46b87d9 Mon Sep 17 00:00:00 2001 From: Borys Palka Date: Wed, 30 Oct 2019 11:24:59 +0100 Subject: [PATCH 02/14] add symbold for check if variable had get addtional variables --- src/variables/body/index.tsx | 10 ++++++++-- src/variables/index.ts | 4 ++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/variables/body/index.tsx b/src/variables/body/index.tsx index 2f79573c..d1260294 100644 --- a/src/variables/body/index.tsx +++ b/src/variables/body/index.tsx @@ -98,8 +98,14 @@ const defaultNodeRenderer = ({ const value = data.value; let dontDisplay = false; - if (data instanceof Function) { - data(); + if (expanded) { + if (data.getMoreDetails) { + data.getMoreDetails(); + dontDisplay = true; + } + } + + if (typeof data === 'symbol') { dontDisplay = true; } diff --git a/src/variables/index.ts b/src/variables/index.ts index 1bbf71d7..5c5d8e09 100644 --- a/src/variables/index.ts +++ b/src/variables/index.ts @@ -54,7 +54,7 @@ class VariablesHeader extends Widget { export namespace Variables { export interface IVariable extends DebugProtocol.Variable { getMoreDetails?: any; - haveMoreDetails?: boolean; + haveMoreDetails?: Symbol; } export interface IScope { @@ -80,7 +80,7 @@ export namespace Variables { return; } - variable.haveMoreDetails = true; + variable.haveMoreDetails = Symbol('haveDetails'); this._currentVariable = variable; this._currentChanged.emit(variable); From 9640d31fc9b1838452233c6fdc8ebe19686174a1 Mon Sep 17 00:00:00 2001 From: Borys Palka Date: Wed, 30 Oct 2019 16:30:13 +0100 Subject: [PATCH 03/14] typos resolve --- src/service.ts | 2 +- src/variables/body/index.tsx | 4 ++-- src/variables/index.ts | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/service.ts b/src/service.ts index d4405f40..78bb0104 100644 --- a/src/service.ts +++ b/src/service.ts @@ -150,7 +150,7 @@ export class DebugService implements IDebugger { } this._model.callstackModel.currentFrameChanged.connect(this.onChangeFrame); - this._model.variablesModel.variableExapnded.connect(this.getVariable); + this._model.variablesModel.variableExpanded.connect(this.getVariable); }; onChangeFrame = (_: Callstack.Model, update: Callstack.IFrame) => { diff --git a/src/variables/body/index.tsx b/src/variables/body/index.tsx index d1260294..1b5e0fd4 100644 --- a/src/variables/body/index.tsx +++ b/src/variables/body/index.tsx @@ -30,7 +30,7 @@ const VariableComponent = ({ model }: { model: Variables.Model }) => { useEffect(() => { const convert = (scopes: Variables.IScope[]) => { - const convertet = scopes.map(scope => { + const converted = scopes.map(scope => { scope.variables = scope.variables.map(variable => { const func = () => { void model.getMoreDataOfVariable(variable); @@ -43,7 +43,7 @@ const VariableComponent = ({ model }: { model: Variables.Model }) => { }); return { ...scope }; }); - return convertet; + return converted; }; const updateScopes = (_: Variables.Model, update: Variables.IScope[]) => { diff --git a/src/variables/index.ts b/src/variables/index.ts index 5c5d8e09..93c0edd5 100644 --- a/src/variables/index.ts +++ b/src/variables/index.ts @@ -115,8 +115,8 @@ export namespace Variables { return this._variablesChanged; } - get variableExapnded(): ISignal { - return this._variableExapnded; + get variableExpanded(): ISignal { + return this._variableExpanded; } getCurrentVariables(): IVariable[] { @@ -124,7 +124,7 @@ export namespace Variables { } async getMoreDataOfVariable(variable: IVariable) { - this._variableExapnded.emit(variable); + this._variableExpanded.emit(variable); } protected _state: IScope[]; @@ -135,7 +135,7 @@ export namespace Variables { private _currentChanged = new Signal(this); private _variablesChanged = new Signal(this); private _scopesChanged = new Signal(this); - private _variableExapnded = new Signal(this); + private _variableExpanded = new Signal(this); } export interface IOptions extends Panel.IOptions { From d8931da0062ebfefc7b818aab52947166f2c350a Mon Sep 17 00:00:00 2001 From: Borys Palka Date: Wed, 6 Nov 2019 13:47:03 +0100 Subject: [PATCH 04/14] add filter obj and add Demo Data Set breakpoints via the service Convert to source breakpoint in the service Filter breakpoints with the same line numbers Pass this to disconnect Rename onNewCell to onActiveCellChanged Ensure session is ready Use changed signal for models add hover method for breakpoints hover gutter by only css fixed issue with variablesreference 0 and foreach in service Use source path for the breakpoints view Rebase DisplayClassVariable branch --- src/breakpoints/body.tsx | 6 +- src/breakpoints/index.ts | 41 +++--- src/callstack/index.ts | 88 +++++++------ src/debugger.ts | 39 +++--- src/handlers/cell.ts | 54 +++++--- src/handlers/console.ts | 12 +- src/handlers/notebook.ts | 62 +++++----- src/index.ts | 61 +++++---- src/service.ts | 234 +++++++++++++++++++++++++++-------- src/session.ts | 136 +++++++++----------- src/tokens.ts | 66 +++++----- src/variables/body/index.tsx | 76 +++++++----- src/variables/index.ts | 135 ++++++++++++++++++-- style/breakpoints.css | 16 ++- style/callstack.css | 4 + tests/src/debugger.spec.ts | 10 ++ 16 files changed, 693 insertions(+), 347 deletions(-) diff --git a/src/breakpoints/body.tsx b/src/breakpoints/body.tsx index 450c8af5..3460d71e 100644 --- a/src/breakpoints/body.tsx +++ b/src/breakpoints/body.tsx @@ -35,10 +35,10 @@ const BreakpointsComponent = ({ model }: { model: Breakpoints.Model }) => { setBreakpoints(updates); }; - model.breakpointsChanged.connect(updateBreakpoints); + model.changed.connect(updateBreakpoints); return () => { - model.breakpointsChanged.disconnect(updateBreakpoints); + model.changed.disconnect(updateBreakpoints); }; }); @@ -98,7 +98,7 @@ const BreakpointComponent = ({ checked={active} /> - {breakpoint.source.name} : {breakpoint.line} + {breakpoint.source.path} : {breakpoint.line} ); diff --git a/src/breakpoints/index.ts b/src/breakpoints/index.ts index 46fcbd85..ab61d5dd 100644 --- a/src/breakpoints/index.ts +++ b/src/breakpoints/index.ts @@ -2,19 +2,22 @@ // Distributed under the terms of the Modified BSD License. import { Toolbar, ToolbarButton } from '@jupyterlab/apputils'; - +import { IDisposable } from '@phosphor/disposable'; import { Signal } from '@phosphor/signaling'; import { Panel, PanelLayout, Widget } from '@phosphor/widgets'; import { DebugProtocol } from 'vscode-debugprotocol'; +import { IDebugger } from '../tokens'; + import { Body } from './body'; export class Breakpoints extends Panel { constructor(options: Breakpoints.IOptions) { super(); this.model = options.model; + this.service = options.service; this.addClass('jp-DebuggerBreakpoints'); this.title.label = 'Breakpoints'; @@ -44,7 +47,7 @@ export class Breakpoints extends Panel { new ToolbarButton({ iconClassName: 'jp-CloseAllIcon', onClick: () => { - this.model.removeAllBreakpoints(); + void this.service.updateBreakpoints([]); }, tooltip: 'Remove All Breakpoints' }) @@ -54,6 +57,7 @@ export class Breakpoints extends Panel { private isAllActive = true; readonly body: Widget; readonly model: Breakpoints.Model; + readonly service: IDebugger; } class BreakpointsHeader extends Widget { @@ -77,12 +81,12 @@ export namespace Breakpoints { active: boolean; } - export class Model { + export class Model implements IDisposable { constructor(model: IBreakpoint[]) { this._breakpoints = model; } - breakpointsChanged = new Signal(this); + changed = new Signal(this); get breakpoints(): IBreakpoint[] { return this._breakpoints; @@ -94,7 +98,7 @@ export namespace Breakpoints { set breakpoints(breakpoints: IBreakpoint[]) { this._breakpoints = [...breakpoints]; - this.breakpointsChanged.emit(this._breakpoints); + this.changed.emit(this._breakpoints); } set breakpoint(breakpoint: IBreakpoint) { @@ -109,10 +113,6 @@ export namespace Breakpoints { } } - addBreakpoint(breakpoint: IBreakpoint) { - this.breakpoints = [...this._breakpoints, breakpoint]; - } - set type(newType: SessionTypes) { if (newType === this._selectedType) { return; @@ -122,15 +122,6 @@ export namespace Breakpoints { this.breakpoints = this._state[newType]; } - removeBreakpointAtLine(line: number) { - const breakpoints = this.breakpoints.filter(ele => ele.line !== line); - this.breakpoints = breakpoints; - } - - removeAllBreakpoints() { - this.breakpoints = []; - } - changeLines(lines: number[]) { if (!lines && this.breakpoints.length === 0) { return; @@ -147,6 +138,18 @@ export namespace Breakpoints { } } + get isDisposed(): boolean { + return this._isDisposed; + } + + dispose(): void { + if (this._isDisposed) { + return; + } + this._isDisposed = true; + Signal.clearData(this); + } + private _selectedType: SessionTypes; private _breakpointChanged = new Signal(this); private _breakpoints: IBreakpoint[]; @@ -154,6 +157,7 @@ export namespace Breakpoints { console: [] as Breakpoints.IBreakpoint[], notebook: [] as Breakpoints.IBreakpoint[] }; + private _isDisposed: boolean = false; } /** @@ -161,6 +165,7 @@ export namespace Breakpoints { */ export interface IOptions extends Panel.IOptions { model: Model; + service: IDebugger; } } diff --git a/src/callstack/index.ts b/src/callstack/index.ts index f893165a..0931d126 100644 --- a/src/callstack/index.ts +++ b/src/callstack/index.ts @@ -1,8 +1,9 @@ // Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. -import { Toolbar, ToolbarButton } from '@jupyterlab/apputils'; +import { CommandToolbarButton, Toolbar } from '@jupyterlab/apputils'; +import { CommandRegistry } from '@phosphor/commands'; import { ISignal, Signal } from '@phosphor/signaling'; import { Panel, PanelLayout, Widget } from '@phosphor/widgets'; import { DebugProtocol } from 'vscode-debugprotocol'; @@ -12,7 +13,9 @@ export class Callstack extends Panel { constructor(options: Callstack.IOptions) { super(); - this.model = options.model; + const { commands, model } = options; + + this.model = model; this.addClass('jp-DebuggerCallstack'); this.title.label = 'Callstack'; @@ -24,52 +27,33 @@ export class Callstack extends Panel { header.toolbar.addItem( 'continue', - new ToolbarButton({ - iconClassName: 'jp-RunIcon', - onClick: () => { - console.log('`run` was clicked'); - }, - tooltip: 'Continue' - }) - ); - header.toolbar.addItem( - 'stop', - new ToolbarButton({ - iconClassName: 'jp-StopIcon', - onClick: () => { - console.log('`stop` was clicked'); - }, - tooltip: 'Stop' + new CommandToolbarButton({ + commands: commands.registry, + id: commands.continue }) ); + header.toolbar.addItem( 'step-over', - new ToolbarButton({ - iconClassName: 'jp-StepOverIcon', - onClick: () => { - console.log('`step over` was clicked'); - }, - tooltip: 'Step Over' + new CommandToolbarButton({ + commands: commands.registry, + id: commands.next }) ); + header.toolbar.addItem( 'step-in', - new ToolbarButton({ - iconClassName: 'jp-StepInIcon', - onClick: () => { - console.log('`step in` was clicked'); - }, - tooltip: 'Step In' + new CommandToolbarButton({ + commands: commands.registry, + id: commands.stepIn }) ); + header.toolbar.addItem( 'step-out', - new ToolbarButton({ - iconClassName: 'jp-StepOutIcon', - onClick: () => { - console.log('`step out` was clicked'); - }, - tooltip: 'Step Out' + new CommandToolbarButton({ + commands: commands.registry, + id: commands.stepOut }) ); } @@ -133,7 +117,39 @@ export namespace Callstack { private _currentFrameChanged = new Signal(this); } + export interface ICommands { + /** + * The command registry. + */ + registry: CommandRegistry; + + /** + * The continue command ID. + */ + continue: string; + + /** + * The next / stepOver command ID. + */ + next: string; + + /** + * The stepIn command ID. + */ + stepIn: string; + + /** + * The stepOut command ID. + */ + stepOut: string; + } + export interface IOptions extends Panel.IOptions { + /** + * The toolbar commands interface for the callstack. + */ + commands: ICommands; + model: Model; } } diff --git a/src/debugger.ts b/src/debugger.ts index 83dc0013..2fb29021 100644 --- a/src/debugger.ts +++ b/src/debugger.ts @@ -39,10 +39,15 @@ export class Debugger extends SplitPanel { connector: options.connector }); - this.sidebar = new Debugger.Sidebar(this.model); this.service = options.debugService; this.service.model = this.model; + this.sidebar = new Debugger.Sidebar({ + model: this.model, + service: this.service, + callstackCommands: options.callstackCommands + }); + this.editors = new DebuggerEditors({ editorFactory: options.editorFactory }); @@ -78,18 +83,26 @@ export namespace Debugger { export interface IOptions { debugService: DebugService; editorFactory: CodeEditor.Factory; + callstackCommands: Callstack.ICommands; connector?: IDataConnector; } export class Sidebar extends SplitPanel { - constructor(model: Model) { + constructor(options: Sidebar.IOptions) { super(); this.orientation = 'vertical'; this.addClass('jp-DebuggerSidebar'); + const { callstackCommands, service, model } = options; this.variables = new Variables({ model: model.variablesModel }); - this.callstack = new Callstack({ model: model.callstackModel }); - this.breakpoints = new Breakpoints({ model: model.breakpointsModel }); + this.callstack = new Callstack({ + commands: callstackCommands, + model: model.callstackModel + }); + this.breakpoints = new Breakpoints({ + service, + model: model.breakpointsModel + }); this.addWidget(this.variables); this.addWidget(this.callstack); @@ -143,14 +156,6 @@ export namespace Debugger { this._codeValue = observableString; } - get currentLineChanged() { - return this._currentLineChanged; - } - - get linesCleared() { - return this._linesCleared; - } - dispose(): void { this._isDisposed = true; } @@ -167,8 +172,14 @@ export namespace Debugger { private _isDisposed = false; private _mode: IDebugger.Mode; private _modeChanged = new Signal(this); - private _currentLineChanged = new Signal(this); - private _linesCleared = new Signal(this); + } + + export namespace Sidebar { + export interface IOptions { + model: Debugger.Model; + service: IDebugger; + callstackCommands: Callstack.ICommands; + } } export namespace Model { diff --git a/src/handlers/cell.ts b/src/handlers/cell.ts index 85524ed7..01c54aaf 100644 --- a/src/handlers/cell.ts +++ b/src/handlers/cell.ts @@ -5,41 +5,58 @@ import { CodeCell } from '@jupyterlab/cells'; import { CodeMirrorEditor } from '@jupyterlab/codemirror'; +import { IDisposable } from '@phosphor/disposable'; + +import { Signal } from '@phosphor/signaling'; + import { Doc, Editor } from 'codemirror'; import { Breakpoints, SessionTypes } from '../breakpoints'; -import { IDisposable } from '@phosphor/disposable'; import { Debugger } from '../debugger'; -import { IDebugger } from '../tokens'; -import { Signal } from '@phosphor/signaling'; +import { IDebugger } from '../tokens'; const LINE_HIGHLIGHT_CLASS = 'jp-breakpoint-line-highlight'; export class CellManager implements IDisposable { constructor(options: CellManager.IOptions) { - this._debuggerModel = options.debuggerModel; this._debuggerService = options.debuggerService; - this.breakpointsModel = options.breakpointsModel; + this.onModelChanged(); + this._debuggerService.modelChanged.connect(() => this.onModelChanged()); this.activeCell = options.activeCell; this.onActiveCellChanged(); + } - this._debuggerModel.currentLineChanged.connect((_, lineNumber) => { - this.showCurrentLine(lineNumber); - }); + isDisposed: boolean; - this._debuggerModel.linesCleared.connect(() => { + private onModelChanged() { + this._debuggerModel = this._debuggerService.model; + if (!this._debuggerModel) { + return; + } + this.breakpointsModel = this._debuggerModel.breakpointsModel; + + this._debuggerModel.variablesModel.changed.connect(() => { this.cleanupHighlight(); + const firstFrame = this._debuggerModel.callstackModel.frames[0]; + if (!firstFrame) { + return; + } + this.showCurrentLine(firstFrame.line); }); - this.breakpointsModel.breakpointsChanged.connect(async () => { + this.breakpointsModel.changed.connect(async () => { + if (!this.activeCell || this.activeCell.isDisposed) { + return; + } this.addBreakpointsToEditor(this.activeCell); - await this._debuggerService.updateBreakpoints(); }); - } - isDisposed: boolean; + if (this.activeCell) { + this._debuggerModel.codeValue = this.activeCell.model.value; + } + } private showCurrentLine(lineNumber: number) { if (!this.activeCell) { @@ -134,7 +151,8 @@ export class CellManager implements IDisposable { lineInfo.line + 1 ); }); - this._debuggerModel.breakpointsModel.breakpoints = editorBreakpoints; + + void this._debuggerService.updateBreakpoints(editorBreakpoints); editor.setOption('lineNumbers', true); editor.editor.setOption('gutters', [ @@ -163,10 +181,11 @@ export class CellManager implements IDisposable { } const isRemoveGutter = !!info.gutterMarkers; + let breakpoints = this.breakpointsModel.breakpoints; if (isRemoveGutter) { - this.breakpointsModel.removeBreakpointAtLine(info.line + 1); + breakpoints = breakpoints.filter(ele => ele.line !== info.line + 1); } else { - this.breakpointsModel.addBreakpoint( + breakpoints.push( Private.createBreakpoint( this._debuggerService.session.client.name, this.getEditorId(), @@ -174,6 +193,8 @@ export class CellManager implements IDisposable { ) ); } + + void this._debuggerService.updateBreakpoints(breakpoints); }; protected onNewRenderLine = (editor: Editor, line: any) => { @@ -259,7 +280,6 @@ namespace Private { marker.innerHTML = '●'; return marker; } - export function createBreakpoint( session: string, type: string, diff --git a/src/handlers/console.ts b/src/handlers/console.ts index affeea7a..6cefac53 100644 --- a/src/handlers/console.ts +++ b/src/handlers/console.ts @@ -3,19 +3,21 @@ import { CodeConsole, IConsoleTracker } from '@jupyterlab/console'; -import { CellManager } from '../handlers/cell'; - import { CodeCell } from '@jupyterlab/cells'; +import { IDisposable } from '@phosphor/disposable'; + +import { Signal } from '@phosphor/signaling'; + import { Breakpoints } from '../breakpoints'; +import { CellManager } from '../handlers/cell'; + import { IDebugger } from '../tokens'; -import { IDisposable } from '@phosphor/disposable'; -import { Signal } from '@phosphor/signaling'; import { Debugger } from '../debugger'; -export class DebuggerConsoleHandler implements IDisposable { +export class ConsoleHandler implements IDisposable { constructor(options: DebuggerConsoleHandler.IOptions) { this.debuggerModel = options.debuggerService.model; this.debuggerService = options.debuggerService; diff --git a/src/handlers/notebook.ts b/src/handlers/notebook.ts index 8226b710..1e866264 100644 --- a/src/handlers/notebook.ts +++ b/src/handlers/notebook.ts @@ -5,39 +5,39 @@ import { INotebookTracker, NotebookTracker } from '@jupyterlab/notebook'; import { CodeCell } from '@jupyterlab/cells'; -import { CellManager } from './cell'; - -import { Debugger } from '../debugger'; +import { IDisposable } from '@phosphor/disposable'; -import { IDebugger } from '../tokens'; +import { Signal } from '@phosphor/signaling'; import { Breakpoints } from '../breakpoints'; -import { IDisposable } from '@phosphor/disposable'; +import { Debugger } from '../debugger'; -import { Signal } from '@phosphor/signaling'; +import { IDebugger } from '../tokens'; -export class DebuggerNotebookHandler implements IDisposable { - constructor(options: DebuggerNotebookHandler.IOptions) { +import { CellManager } from './cell'; + +export class NotebookHandler implements IDisposable { + constructor(options: NotebookHandler.IOptions) { this.debuggerModel = options.debuggerService.model; this.debuggerService = options.debuggerService; this.notebookTracker = options.tracker; this.breakpoints = this.debuggerModel.breakpointsModel; - this.notebookTracker.activeCellChanged.connect(this.onNewCell, this); + this.cellManager = new CellManager({ breakpointsModel: this.breakpoints, - activeCell: this.notebookTracker.activeCell as CodeCell, debuggerModel: this.debuggerModel, debuggerService: this.debuggerService, - type: 'notebook' + type: 'notebook', + activeCell: this.notebookTracker.activeCell as CodeCell }); + + this.notebookTracker.activeCellChanged.connect( + this.onActiveCellChanged, + this + ); } - private notebookTracker: INotebookTracker; - private debuggerModel: Debugger.Model; - private debuggerService: IDebugger; - private breakpoints: Breakpoints.Model; - private cellManager: CellManager; isDisposed: boolean; dispose(): void { @@ -46,26 +46,28 @@ export class DebuggerNotebookHandler implements IDisposable { } this.isDisposed = true; this.cellManager.dispose(); - this.notebookTracker.activeCellChanged.disconnect(this.onNewCell); + this.notebookTracker.activeCellChanged.disconnect( + this.onActiveCellChanged, + this + ); Signal.clearData(this); } - protected onNewCell(notebookTracker: NotebookTracker, codeCell: CodeCell) { - if (this.cellManager) { - this.cellManager.activeCell = codeCell; - } else { - this.cellManager = new CellManager({ - breakpointsModel: this.breakpoints, - activeCell: codeCell, - debuggerModel: this.debuggerModel, - debuggerService: this.debuggerService, - type: 'notebook' - }); - } + protected onActiveCellChanged( + notebookTracker: NotebookTracker, + codeCell: CodeCell + ) { + this.cellManager.activeCell = codeCell; } + + private notebookTracker: INotebookTracker; + private debuggerModel: Debugger.Model; + private debuggerService: IDebugger; + private breakpoints: Breakpoints.Model; + private cellManager: CellManager; } -export namespace DebuggerNotebookHandler { +export namespace NotebookHandler { export interface IOptions { debuggerService: IDebugger; tracker: INotebookTracker; diff --git a/src/index.ts b/src/index.ts index 7c32b3a3..58a9922b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -31,9 +31,9 @@ import { UUID } from '@phosphor/coreutils'; import { Debugger } from './debugger'; -import { DebuggerNotebookHandler } from './handlers/notebook'; +import { ConsoleHandler } from './handlers/console'; -import { DebuggerConsoleHandler } from './handlers/console'; +import { NotebookHandler } from './handlers/notebook'; import { DebugService } from './service'; @@ -57,6 +57,8 @@ export namespace CommandIDs { export const stepIn = 'debugger:stepIn'; + export const stepOut = 'debugger:stepOut'; + export const debugConsole = 'debugger:debug-console'; export const debugFile = 'debugger:debug-file'; @@ -78,16 +80,11 @@ async function setDebugSession( } else { debug.session.client = client; } - await debug.session.restoreState(); - if (debug.canStart()) { - await debug.start(); - } + await debug.restoreState(true); app.commands.notifyCommandChanged(); } -class HandlerTracker< - H extends DebuggerConsoleHandler | DebuggerNotebookHandler -> { +class DebuggerHandler { constructor(builder: new (option: any) => H) { this.builder = builder; } @@ -103,8 +100,8 @@ class HandlerTracker< }); this.handlers[widget.id] = handler; widget.disposed.connect(() => { - delete this.handlers[widget.id]; handler.dispose(); + delete this.handlers[widget.id]; }); } } @@ -126,9 +123,7 @@ const consoles: JupyterFrontEndPlugin = { tracker: IConsoleTracker, labShell: ILabShell ) => { - const handlerTracker = new HandlerTracker( - DebuggerConsoleHandler - ); + const handler = new DebuggerHandler(ConsoleHandler); labShell.currentChanged.connect(async (_, update) => { const widget = update.newValue; @@ -136,7 +131,7 @@ const consoles: JupyterFrontEndPlugin = { return; } await setDebugSession(app, debug, widget.session); - handlerTracker.update(debug, tracker, widget); + handler.update(debug, tracker, widget); }); } }; @@ -203,9 +198,7 @@ const notebooks: JupyterFrontEndPlugin = { tracker: INotebookTracker, labShell: ILabShell ) => { - const handlerTracker = new HandlerTracker( - DebuggerNotebookHandler - ); + const handler = new DebuggerHandler(NotebookHandler); labShell.currentChanged.connect(async (_, update) => { const widget = update.newValue; @@ -213,7 +206,7 @@ const notebooks: JupyterFrontEndPlugin = { return; } await setDebugSession(app, debug, widget.session); - handlerTracker.update(debug, tracker, widget); + handler.update(debug, tracker, widget); }); } }; @@ -281,14 +274,14 @@ const main: JupyterFrontEndPlugin = { sidebar.title.label = 'Environment'; shell.add(sidebar, 'right', { activate: false }); - if (service.canStart()) { - await service.start(); - } + await service.restoreState(true); } }); commands.addCommand(CommandIDs.debugContinue, { label: 'Continue', + caption: 'Continue', + iconClass: 'jp-MaterialIcon jp-RunIcon', isEnabled: () => { return service.isThreadStopped(); }, @@ -300,6 +293,8 @@ const main: JupyterFrontEndPlugin = { commands.addCommand(CommandIDs.next, { label: 'Next', + caption: 'Next', + iconClass: 'jp-MaterialIcon jp-StepOverIcon', isEnabled: () => { return service.isThreadStopped(); }, @@ -310,6 +305,8 @@ const main: JupyterFrontEndPlugin = { commands.addCommand(CommandIDs.stepIn, { label: 'StepIn', + caption: 'Step In', + iconClass: 'jp-MaterialIcon jp-StepInIcon', isEnabled: () => { return service.isThreadStopped(); }, @@ -318,6 +315,18 @@ const main: JupyterFrontEndPlugin = { } }); + commands.addCommand(CommandIDs.stepOut, { + label: 'StepOut', + caption: 'Step Out', + iconClass: 'jp-MaterialIcon jp-StepOutIcon', + isEnabled: () => { + return service.isThreadStopped(); + }, + execute: async () => { + await service.stepOut(); + } + }); + commands.addCommand(CommandIDs.changeMode, { label: 'Change Mode', isEnabled: () => { @@ -338,6 +347,14 @@ const main: JupyterFrontEndPlugin = { const savedMode = (await state.fetch('mode')) as IDebugger.Mode; const mode = savedMode ? savedMode : 'expanded'; + const callstackCommands = { + registry: commands, + continue: CommandIDs.debugContinue, + next: CommandIDs.next, + stepIn: CommandIDs.stepIn, + stepOut: CommandIDs.stepOut + }; + if (tracker.currentWidget) { widget = tracker.currentWidget; } else { @@ -345,6 +362,7 @@ const main: JupyterFrontEndPlugin = { content: new Debugger({ debugService: service, connector: state, + callstackCommands, editorFactory }) }); @@ -381,6 +399,7 @@ const main: JupyterFrontEndPlugin = { palette.addItem({ command: CommandIDs.debugContinue, category }); palette.addItem({ command: CommandIDs.next, category }); palette.addItem({ command: CommandIDs.stepIn, category }); + palette.addItem({ command: CommandIDs.stepOut, category }); palette.addItem({ command: CommandIDs.debugNotebook, category }); } diff --git a/src/service.ts b/src/service.ts index d76056bc..0828293f 100644 --- a/src/service.ts +++ b/src/service.ts @@ -13,8 +13,13 @@ import { IDebugger } from './tokens'; import { Variables } from './variables'; +import { Breakpoints } from './breakpoints'; + import { Callstack } from './callstack'; +/** + * A concrete implementation of IDebugger. + */ export class DebugService implements IDebugger { constructor() { // Avoids setting session with invalid client @@ -28,38 +33,43 @@ export class DebugService implements IDebugger { this._model = null; } - dispose(): void { - if (this.isDisposed) { - return; - } - this._isDisposed = true; - Signal.clearData(this); - } - + /** + * Whether the debug service is disposed. + */ get isDisposed(): boolean { return this._isDisposed; } + /** + * Returns the mode of the debugger UI. + * + * #### Notes + * There is only ever one debugger instance. If it is `expanded`, it exists + * as a `MainAreaWidget`, otherwise it is a sidebar. + */ get mode(): IDebugger.Mode { return this._model.mode; } + /** + * Sets the mode of the debugger UI to the given parameter. + * @param mode - the new mode of the debugger UI. + */ set mode(mode: IDebugger.Mode) { this._model.mode = mode; } - get model(): Debugger.Model { - return this._model; - } - - set model(model: Debugger.Model) { - this._model = model; - } - + /** + * Returns the current debug session. + */ get session(): IDebugger.ISession { return this._session; } + /** + * Sets the current debug session to the given parameter. + * @param session - the new debugger session. + */ set session(session: IDebugger.ISession) { if (this._session === session) { return; @@ -82,28 +92,106 @@ export class DebugService implements IDebugger { this._sessionChanged.emit(session); } + /** + * Returns the debugger model. + */ + get model(): Debugger.Model { + return this._model; + } + + /** + * Sets the debugger model to the given parameter. + * @param model - The new debugger model. + */ + set model(model: Debugger.Model) { + this._model = model; + this._modelChanged.emit(model); + } + + /** + * Signal emitted upon session changed. + */ + get sessionChanged(): ISignal { + return this._sessionChanged; + } + + /** + * Signal emitted upon model changed. + */ + get modelChanged(): ISignal { + return this._modelChanged; + } + + /** + * Signal emitted for debug event messages. + */ + get eventMessage(): ISignal { + return this._eventMessage; + } + + /** + * Dispose the debug service. + */ + dispose(): void { + if (this.isDisposed) { + return; + } + this._isDisposed = true; + Signal.clearData(this); + } + + /** + * Whether the current debugger is started. + */ isStarted(): boolean { return this._session !== null && this._session.isStarted; } + /** + * Whether the current thread is stopped. + */ isThreadStopped(): boolean { return this._threadStopped.has(this.currentThread()); } - canStart(): boolean { - return ( - this._model !== null && this._session !== null && !this._session.isStarted - ); - } - + /** + * Starts a debugger. + * Precondition: canStart() && !isStarted() + */ async start(): Promise { await this.session.start(); } + /** + * Stops the debugger. + * Precondition: isStarted() + */ async stop(): Promise { await this.session.stop(); } + /** + * Restore the state of a debug session. + * @param autoStart - when true, starts the debugger + * if it has not been started yet. + */ + + async restoreState(autoStart: boolean): Promise { + if (!this.model || !this.session) { + return; + } + + await this.session.restoreState(); + // TODO: restore breakpoints when the model is updated + + if (!this.isStarted() && autoStart) { + await this.start(); + } + } + + /** + * Continues the execution of the current thread. + */ async continue(): Promise { try { await this.session.sendRequest('continue', { @@ -115,6 +203,9 @@ export class DebugService implements IDebugger { } } + /** + * Makes the current thread run again for one step. + */ async next(): Promise { try { await this.session.sendRequest('next', { @@ -125,6 +216,9 @@ export class DebugService implements IDebugger { } } + /** + * Makes the current thread step in a function / method if possible. + */ async stepIn(): Promise { try { await this.session.sendRequest('stepIn', { @@ -135,13 +229,51 @@ export class DebugService implements IDebugger { } } - get sessionChanged(): ISignal { - return this._sessionChanged; + /** + * Makes the current thread step out a function / method if possible. + */ + async stepOut(): Promise { + try { + await this.session.sendRequest('stepOut', { + threadId: this.currentThread() + }); + } catch (err) { + console.error('Error:', err.message); + } } - get eventMessage(): ISignal { - return this._eventMessage; - } + /** + * Update all breakpoints at once. + */ + updateBreakpoints = async (breakpoints: Breakpoints.IBreakpoint[]) => { + if (!this.session.isStarted) { + return; + } + // Workaround: this should not be called before the session has started + await this.ensureSessionReady(); + const code = this._model.codeValue.text; + const dumpedCell = await this.dumpCell(code); + const sourceBreakpoints = Private.toSourceBreakpoints(breakpoints); + const reply = await this.setBreakpoints( + sourceBreakpoints, + dumpedCell.sourcePath + ); + let kernelBreakpoints = reply.body.breakpoints.map(breakpoint => { + return { + ...breakpoint, + active: true, + source: { path: this.session.client.name } + }; + }); + + // filter breakpoints with the same line number + kernelBreakpoints = kernelBreakpoints.filter( + (breakpoint, i, arr) => + arr.findIndex(el => el.line === breakpoint.line) === i + ); + this._model.breakpointsModel.breakpoints = kernelBreakpoints; + await this.session.sendRequest('configurationDone', {}); + }; getAllFrames = async () => { const stackFrames = await this.getFrames(this.currentThread()); @@ -156,7 +288,6 @@ export class DebugService implements IDebugger { }); if (index === 0) { this._model.variablesModel.scopes = values; - this._model.currentLineChanged.emit(frame.line); } }); @@ -199,9 +330,6 @@ export class DebugService implements IDebugger { }; getVariable = async (_: any, variable: DebugProtocol.Variable) => { - if (!variable && variable.variablesReference !== 0) { - return; - } const reply = await this.session.sendRequest('variables', { variablesReference: variable.variablesReference }); @@ -225,39 +353,19 @@ export class DebugService implements IDebugger { return reply.body.variables; }; - getBreakpoints = (): DebugProtocol.SourceBreakpoint[] => { - return this._model.breakpointsModel.breakpoints.map(breakpoint => { - return { - line: breakpoint.line - }; - }); - }; - setBreakpoints = async ( breakpoints: DebugProtocol.SourceBreakpoint[], path: string ) => { // Workaround: this should not be called before the session has started - const client = this.session.client as IClientSession; - await client.ready; - await this.session.sendRequest('setBreakpoints', { + await this.ensureSessionReady(); + return await this.session.sendRequest('setBreakpoints', { breakpoints: breakpoints, source: { path }, sourceModified: false }); }; - updateBreakpoints = async () => { - if (!this.session.isStarted) { - return; - } - const code = this._model.codeValue.text; - const dumpedCell = await this.dumpCell(code); - const breakpoints = this.getBreakpoints(); - await this.setBreakpoints(breakpoints, dumpedCell.sourcePath); - await this.session.sendRequest('configurationDone', {}); - }; - protected convertScope = ( scopes: DebugProtocol.Scope[], variables: DebugProtocol.Variable[] @@ -275,8 +383,12 @@ export class DebugService implements IDebugger { }); }; + private async ensureSessionReady(): Promise { + const client = this.session.client as IClientSession; + return client.ready; + } + private onContinued() { - this._model.linesCleared.emit(); this._model.callstackModel.frames = []; this._model.variablesModel.scopes = []; } @@ -289,9 +401,13 @@ export class DebugService implements IDebugger { private _isDisposed: boolean = false; private _session: IDebugger.ISession; private _sessionChanged = new Signal(this); + private _modelChanged = new Signal(this); private _eventMessage = new Signal(this); private _model: Debugger.Model; + + // TODO: remove frames from the service private frames: Frame[] = []; + // TODO: move this in model private _threadStopped = new Set(); } @@ -300,3 +416,13 @@ export type Frame = { id: number; scopes: Variables.IScope[]; }; + +namespace Private { + export function toSourceBreakpoints(breakpoints: Breakpoints.IBreakpoint[]) { + return breakpoints.map(breakpoint => { + return { + line: breakpoint.line + }; + }); + } +} diff --git a/src/session.ts b/src/session.ts index aadf6bbd..b6b35568 100644 --- a/src/session.ts +++ b/src/session.ts @@ -3,8 +3,6 @@ import { IClientSession } from '@jupyterlab/apputils'; -import { CodeEditor } from '@jupyterlab/codeeditor'; - import { KernelMessage } from '@jupyterlab/services'; import { PromiseDelegate } from '@phosphor/coreutils'; @@ -13,6 +11,9 @@ import { ISignal, Signal } from '@phosphor/signaling'; import { IDebugger } from './tokens'; +/** + * A concrete implementation of IDebugger.ISession. + */ export class DebugSession implements IDebugger.ISession { /** * Instantiate a new debug session @@ -24,29 +25,39 @@ export class DebugSession implements IDebugger.ISession { } /** - * The client session to connect to a debugger. + * Whether the debug session is disposed. */ - private _client: IClientSession; - - get id() { - return this.client.name; + get isDisposed(): boolean { + return this._isDisposed; } - get type() { - return this.client.type; + /** + * A signal emitted when the debug session is disposed. + */ + get disposed(): ISignal { + return this._disposed; } + /** + * Returns the API client session to connect to a debugger. + */ get client(): IClientSession { return this._client; } + /** + * Sets the API client session to connect to a debugger to + * the given parameter. + * + * @param client - The new API client session. + */ set client(client: IClientSession | null) { if (this._client === client) { return; } if (this._client) { - Signal.clearData(this._client); + this._client.iopubMessage.disconnect(this._handleEvent, this); } this._client = client; @@ -57,9 +68,18 @@ export class DebugSession implements IDebugger.ISession { } /** - * The code editors in a debugger session. + * Whether the debug session is started + */ + get isStarted(): boolean { + return this._isStarted; + } + + /** + * Signal emitted for debug event messages. */ - editors: CodeEditor.IEditor[]; + get eventMessage(): ISignal { + return this._eventMessage; + } /** * Dispose the debug session. @@ -73,79 +93,47 @@ export class DebugSession implements IDebugger.ISession { Signal.clearData(this); } - /** - * A signal emitted when the debug session is disposed. - */ - get disposed(): ISignal { - return this._disposed; - } - - /** - * Whether the debug session is disposed. - */ - get isDisposed(): boolean { - return this._isDisposed; - } - - /** - * Whether the debug session is started - */ - get isStarted(): boolean { - return this._isStarted; - } - /** * Start a new debug session */ async start(): Promise { - try { - await this.sendRequest('initialize', { - clientID: 'jupyterlab', - clientName: 'JupyterLab', - adapterID: 'python', - pathFormat: 'path', - linesStartAt1: true, - columnsStartAt1: true, - supportsVariableType: true, - supportsVariablePaging: true, - supportsRunInTerminalRequest: true, - locale: 'en-us' - }); - - this._isStarted = true; - - await this.sendRequest('attach', {}); - } catch (err) { - console.error('Error:', err.message); - } + await this.sendRequest('initialize', { + clientID: 'jupyterlab', + clientName: 'JupyterLab', + adapterID: 'python', + pathFormat: 'path', + linesStartAt1: true, + columnsStartAt1: true, + supportsVariableType: true, + supportsVariablePaging: true, + supportsRunInTerminalRequest: true, + locale: 'en-us' + }); + + this._isStarted = true; + + await this.sendRequest('attach', {}); } /** * Stop the running debug session. */ async stop(): Promise { - try { - await this.sendRequest('disconnect', { - restart: false, - terminateDebuggee: true - }); - this._isStarted = false; - } catch (err) { - console.error('Error:', err.message); - } + await this.sendRequest('disconnect', { + restart: false, + terminateDebuggee: true + }); + this._isStarted = false; } /** * Restore the state of a debug session. */ - async restoreState(): Promise { + async restoreState(): Promise { await this.client.ready; - try { - const message = await this.sendRequest('debugInfo', {}); - this._isStarted = message.body.isStarted; - } catch (err) { - console.error('Error: ', err.message); - } + const message = await this.sendRequest('debugInfo', {}); + this._isStarted = message.body.isStarted; + return message; } /** @@ -166,15 +154,10 @@ export class DebugSession implements IDebugger.ISession { return message.content as IDebugger.ISession.Response[K]; } - /** - * Signal emitted for debug event messages. - */ - get eventMessage(): ISignal { - return this._eventMessage; - } - /** * Handle debug events sent on the 'iopub' channel. + * @param sender - the emitter of the event. + * @param message - the event message. */ private _handleEvent( sender: IClientSession, @@ -210,6 +193,7 @@ export class DebugSession implements IDebugger.ISession { return reply.promise; } + private _client: IClientSession; private _disposed = new Signal(this); private _isDisposed: boolean = false; private _isStarted: boolean = false; diff --git a/src/tokens.ts b/src/tokens.ts index 98c4d9ef..4d662249 100644 --- a/src/tokens.ts +++ b/src/tokens.ts @@ -3,13 +3,11 @@ import { IClientSession } from '@jupyterlab/apputils'; -import { CodeEditor } from '@jupyterlab/codeeditor'; - import { Session } from '@jupyterlab/services'; import { Token } from '@phosphor/coreutils'; -import { IObservableDisposable } from '@phosphor/disposable'; +import { IDisposable, IObservableDisposable } from '@phosphor/disposable'; import { ISignal } from '@phosphor/signaling'; @@ -17,12 +15,13 @@ import { DebugProtocol } from 'vscode-debugprotocol'; // TODO: remove that import when an interface has // been created for Model class +import { Breakpoints } from './breakpoints'; import { Debugger } from './debugger'; /** * An interface describing an application's visual debugger. */ -export interface IDebugger { +export interface IDebugger extends IDisposable { /** * The mode of the debugger UI. * @@ -32,20 +31,30 @@ export interface IDebugger { */ mode: IDebugger.Mode; + /** + * The current debugger session. + */ + session: IDebugger.ISession; + /** * The model of the debugger. */ - readonly model: Debugger.Model; + model: Debugger.Model; /** - * The current debugger session. + * Signal emitted upon session changed. */ - session: IDebugger.ISession; + readonly sessionChanged: ISignal; + + /** + * Signal emitted upon model changed. + */ + readonly modelChanged: ISignal; /** - * Whether the debugger can start. + * Signal emitted for debug event messages. */ - canStart(): boolean; + readonly eventMessage: ISignal; /** * Whether the current debugger is started. @@ -69,6 +78,13 @@ export interface IDebugger { */ stop(): Promise; + /** + * Restore the state of a debug session. + * @param autoStart - when true, starts the debugger + * if it has not been started yet. + */ + restoreState(autoStart: boolean): Promise; + /** * Continues the execution of the current thread. */ @@ -85,19 +101,14 @@ export interface IDebugger { stepIn(): Promise; /** - * Update all breakpoints at once. - */ - updateBreakpoints(): Promise; - - /** - * Signal emitted upon session changed. + * Makes the current thread step out a function / method if possible. */ - sessionChanged: ISignal; + stepOut(): Promise; /** - * Signal emitted for debug event messages. + * Update all breakpoints at once. */ - eventMessage: ISignal; + updateBreakpoints(breakpoints: Breakpoints.IBreakpoint[]): Promise; } /** @@ -119,14 +130,16 @@ export namespace IDebugger { client: IClientSession | Session.ISession; /** - * The code editors in a debugger session. + * Whether the debug session is started */ - editors: CodeEditor.IEditor[]; - + readonly isStarted: boolean; /** - * Whether the debug session is started + * Signal emitted for debug event messages. */ - isStarted: boolean; + readonly eventMessage: ISignal< + IDebugger.ISession, + IDebugger.ISession.Event + >; /** * Start a new debug session. @@ -141,7 +154,7 @@ export namespace IDebugger { /** * Restore the state of a debug session. */ - restoreState(): Promise; + restoreState(): Promise; /** * Send a debug request to the kernel. @@ -150,11 +163,6 @@ export namespace IDebugger { command: K, args: IDebugger.ISession.Request[K] ): Promise; - - /** - * Signal emitted for debug event messages. - */ - eventMessage: ISignal; } export namespace ISession { diff --git a/src/variables/body/index.tsx b/src/variables/body/index.tsx index 9a4b9764..70fda72e 100644 --- a/src/variables/body/index.tsx +++ b/src/variables/body/index.tsx @@ -28,46 +28,63 @@ export class Body extends ReactWidget { const VariableComponent = ({ model }: { model: Variables.Model }) => { const [data, setData] = useState(model.scopes); - useEffect(() => { - const convert = (scopes: Variables.IScope[]) => { - const converted = scopes.map(scope => { - scope.variables = scope.variables.map(variable => { - const func = () => { - void model.getMoreDataOfVariable(variable); - }; - if (variable.haveMoreDetails) { - return { ...variable }; - } else { - return { getMoreDetails: func, ...variable }; - } - }); - return { ...scope }; + const filterVariable = (variable: any) => { + const tableKey = ['name', 'value']; + const filteredObj = Object.keys(variable) + .filter( + k => + tableKey.includes(k) || + (k !== 'presentationHint' && typeof (variable as any)[k] === 'object') + ) + .reduce((res, key) => { + if (typeof variable[key] === 'object') { + variable[key] = filterVariable(variable[key]); + } + return Object.assign(res, { [key]: variable[key] }); + }, {}); + + return filteredObj; + }; + + const convert = (scopes: Variables.IScope[]) => { + const converted = scopes.map(scope => { + const newVariable = scope.variables.map(variable => { + const func = () => { + model.getMoreDataOfVariable(variable); + }; + + if (variable.haveMoreDetails || variable.variablesReference === 0) { + return { ...filterVariable(variable) }; + } else { + return { getMoreDetails: func, ...filterVariable(variable) }; + } }); - return converted; - }; - - const updateScopes = (_: Variables.Model, update: Variables.IScope[]) => { - if (ArrayExt.shallowEqual(data, update)) { + return { name: scope.name, variables: newVariable }; + }); + return converted; + }; + useEffect(() => { + const updateScopes = (self: Variables.Model) => { + if (ArrayExt.shallowEqual(data, self.scopes)) { return; } - const newData = convert(update); - setData(newData); + setData(self.scopes); }; - model.scopesChanged.connect(updateScopes); + model.changed.connect(updateScopes); return () => { - model.scopesChanged.disconnect(updateScopes); + model.changed.disconnect(updateScopes); }; }); const List = () => ( <> - {data.map(scopes => ( + {convert(data).map(scope => ( {label} diff --git a/src/variables/index.ts b/src/variables/index.ts index 93c0edd5..5978bbd0 100644 --- a/src/variables/index.ts +++ b/src/variables/index.ts @@ -64,11 +64,11 @@ export namespace Variables { export class Model { constructor(model: IScope[] = []) { - this._state = model; + this._state = DEMO_DATA; } - get scopesChanged(): ISignal { - return this._scopesChanged; + get changed(): ISignal { + return this._changed; } get currentVariable(): IVariable { @@ -100,7 +100,7 @@ export namespace Variables { set scopes(scopes: IScope[]) { this._state = scopes; - this._scopesChanged.emit(scopes); + this._changed.emit(); } get variables(): IVariable[] { @@ -109,10 +109,7 @@ export namespace Variables { set variables(variables: IVariable[]) { this._currentScope.variables = variables; - } - - get variablesChanged(): ISignal { - return this._variablesChanged; + this._changed.emit(); } get variableExpanded(): ISignal { @@ -123,7 +120,7 @@ export namespace Variables { return this.variables; } - async getMoreDataOfVariable(variable: IVariable) { + getMoreDataOfVariable(variable: IVariable) { this._variableExpanded.emit(variable); } @@ -133,12 +130,128 @@ export namespace Variables { private _currentScope: IScope; private _currentChanged = new Signal(this); - private _variablesChanged = new Signal(this); - private _scopesChanged = new Signal(this); private _variableExpanded = new Signal(this); + private _changed = new Signal(this); } export interface IOptions extends Panel.IOptions { model: Model; } } + +const DEMO_DATA = [ + { + name: 'Locals', + variables: [ + { + evaluateName: 'Person', + name: 'Person', + type: 'type', + value: "", + variablesReference: 11 + }, + { + evaluateName: 'display', + name: 'display', + type: 'builtin_function_or_method', + value: + '', + variablesReference: 4 + }, + { + evaluateName: 'p1', + name: 'p1', + type: 'Person', + value: '<__main__.Person object at 0x7f565c73b0b8>', + variablesReference: 13, + 'p1.name': { + evaluateName: 'p1.name', + name: 'name', + type: 'str', + presentationHint: { + attributes: ['rawString'] + }, + value: 'John', + variablesReference: 0 + }, + 'p1.age': { + evaluateName: 'p1.age', + name: 'age', + type: 'int', + presentationHint: { + attributes: ['rawString'] + }, + value: 35, + variablesReference: 0 + } + }, + { + evaluateName: 'ptvsd', + name: 'ptvsd', + type: 'module', + value: + "", + variablesReference: 5 + }, + { + evaluateName: '_pydev_stop_at_break', + name: '_pydev_stop_at_break', + type: 'function', + value: '', + variablesReference: 12 + }, + { + evaluateName: '__annotations__', + name: '__annotations__', + type: 'dict', + value: '{}', + variablesReference: 6 + }, + { + evaluateName: '__builtins__', + name: '__builtins__', + type: 'module', + value: "", + variablesReference: 7 + }, + { + evaluateName: '__doc__', + name: '__doc__', + type: 'NoneType', + value: 'None', + variablesReference: 0 + }, + { + evaluateName: '__loader__', + name: '__loader__', + type: 'type', + value: "", + variablesReference: 8 + }, + { + evaluateName: '__name__', + name: '__name__', + presentationHint: { + attributes: ['rawString'] + }, + type: 'str', + value: "'__main__'", + variablesReference: 0 + }, + { + evaluateName: '__package__', + name: '__package__', + type: 'NoneType', + value: 'None', + variablesReference: 0 + }, + { + evaluateName: '__spec__', + name: '__spec__', + type: 'NoneType', + value: 'None', + variablesReference: 0 + } + ] + } +]; diff --git a/style/breakpoints.css b/style/breakpoints.css index 91bf07d5..b7205732 100644 --- a/style/breakpoints.css +++ b/style/breakpoints.css @@ -9,7 +9,7 @@ .jp-breakpoint-marker { position: absolute; - left: -35px; + left: -34px; top: -1px; color: var(--jp-error-color1); } @@ -17,3 +17,17 @@ .jp-breakpoint-line-highlight { background-color: var(--jp-warn-color0); } + +.CodeMirror-gutter-wrapper::after { + content: '●'; + color: var(--jp-error-color1); + opacity: 0; + position: absolute; + left: 3px; + top: -1px; +} + +.jp-CodeCell.jp-mod-selected .CodeMirror-gutter-wrapper:hover::after, +.jp-Editor.jp-mod-focused .CodeMirror-gutter-wrapper:hover::after { + opacity: 0.5; +} diff --git a/style/callstack.css b/style/callstack.css index f3ab2772..781cb762 100644 --- a/style/callstack.css +++ b/style/callstack.css @@ -21,3 +21,7 @@ color: white; background: var(--jp-brand-color1); } + +.jp-DebuggerCallstack .jp-ToolbarButtonComponent-label { + display: none; +} diff --git a/tests/src/debugger.spec.ts b/tests/src/debugger.spec.ts index e7fc38e4..eb9d60eb 100644 --- a/tests/src/debugger.spec.ts +++ b/tests/src/debugger.spec.ts @@ -2,6 +2,8 @@ import { expect } from 'chai'; import { CodeMirrorEditorFactory } from '@jupyterlab/codemirror'; +import { CommandRegistry } from '@phosphor/commands'; + import { Debugger } from '../../lib/debugger'; import { DebugService } from '../../lib/service'; @@ -10,6 +12,7 @@ class TestPanel extends Debugger {} describe('Debugger', () => { const service = new DebugService(); + const registry = new CommandRegistry(); const editorServices = new CodeMirrorEditorFactory(); const editorFactory = editorServices.newInlineEditor; @@ -18,6 +21,13 @@ describe('Debugger', () => { beforeEach(() => { panel = new TestPanel({ debugService: service, + callstackCommands: { + registry, + continue: '', + next: '', + stepIn: '', + stepOut: '' + }, editorFactory: editorFactory }); }); From 40329fb159784c06cb3e429d6deabeac3a844479 Mon Sep 17 00:00:00 2001 From: Borys Palka Date: Thu, 7 Nov 2019 15:23:53 +0100 Subject: [PATCH 05/14] add method for convertingData for display by ObjectInspector --- src/variables/body/index.tsx | 74 +++++++++++++++++----- src/variables/index.ts | 119 +---------------------------------- 2 files changed, 60 insertions(+), 133 deletions(-) diff --git a/src/variables/body/index.tsx b/src/variables/body/index.tsx index 70fda72e..14300572 100644 --- a/src/variables/body/index.tsx +++ b/src/variables/body/index.tsx @@ -3,7 +3,7 @@ import { Variables } from '../index'; -import { ITheme, ObjectInspector, ObjectLabel } from 'react-inspector'; +import { ITheme, ObjectInspector, ObjectRootLabel } from 'react-inspector'; import { ReactWidget } from '@jupyterlab/apputils'; @@ -28,25 +28,44 @@ export class Body extends ReactWidget { const VariableComponent = ({ model }: { model: Variables.Model }) => { const [data, setData] = useState(model.scopes); - const filterVariable = (variable: any) => { - const tableKey = ['name', 'value']; + const filterVariable = ( + variable: Variables.IVariable, + isObject?: boolean, + v?: any + ): Object => { + const tableKey = ['name', 'value', 'type']; const filteredObj = Object.keys(variable) .filter( - k => - tableKey.includes(k) || - (k !== 'presentationHint' && typeof (variable as any)[k] === 'object') + key => + (isObject ? key === 'value' : tableKey.includes(key)) || + (key !== 'presentationHint' && + typeof (variable as any)[key] === 'object') ) .reduce((res, key) => { - if (typeof variable[key] === 'object') { - variable[key] = filterVariable(variable[key]); + let valueOfKey = + key === 'value' ? convertType(variable) : (variable as any)[key]; + if (typeof valueOfKey === 'object') { + return Object.assign(res, filterVariable( + valueOfKey, + true, + key + ) as Object); } - return Object.assign(res, { [key]: variable[key] }); + if (isObject) { + return Object.assign(res, { + [v]: valueOfKey + }); + } + + return Object.assign(res, { + [key]: valueOfKey + }); }, {}); return filteredObj; }; - const convert = (scopes: Variables.IScope[]) => { + const convertForObjectInspector = (scopes: Variables.IScope[]) => { const converted = scopes.map(scope => { const newVariable = scope.variables.map(variable => { const func = () => { @@ -63,6 +82,7 @@ const VariableComponent = ({ model }: { model: Variables.Model }) => { }); return converted; }; + useEffect(() => { const updateScopes = (self: Variables.Model) => { if (ArrayExt.shallowEqual(data, self.scopes)) { @@ -80,7 +100,7 @@ const VariableComponent = ({ model }: { model: Variables.Model }) => { const List = () => ( <> - {convert(data).map(scope => ( + {convertForObjectInspector(data).map(scope => ( { return <>{List()}; }; +const convertType = (variable: Variables.IVariable) => { + const type = variable.type; + let value: any = variable.value; + if (type === 'int' || type === 'float') { + return value * 1; + } + if (type === 'bool') { + return value === 'False' ? false : true; + } + if (type === 'str') { + return (value as string) + .split('') + .slice(1, value.length - 1) + .join(''); + } + return value; +}; + const defaultNodeRenderer = ({ depth, name, @@ -111,13 +149,19 @@ const defaultNodeRenderer = ({ expanded: boolean; theme?: string | Partial; }) => { - const label = data.name === '' || data.name == null ? name : data.name; - const value = data.value; let dontDisplay = false; + const types = ['bool', 'str', 'int', 'float']; + const label = data.name === '' || data.name == null ? name : data.name; + const value = types.includes(data.type) + ? data.value + : data.type === 'type' + ? 'class' + : data.type; if (expanded) { if (data.getMoreDetails) { data.getMoreDetails(); + dontDisplay = true; } } @@ -131,10 +175,10 @@ const defaultNodeRenderer = ({ {label} : - {value} + {value} ) : ( - + ); }; diff --git a/src/variables/index.ts b/src/variables/index.ts index 5978bbd0..1c227959 100644 --- a/src/variables/index.ts +++ b/src/variables/index.ts @@ -64,7 +64,7 @@ export namespace Variables { export class Model { constructor(model: IScope[] = []) { - this._state = DEMO_DATA; + this._state = model; } get changed(): ISignal { @@ -138,120 +138,3 @@ export namespace Variables { model: Model; } } - -const DEMO_DATA = [ - { - name: 'Locals', - variables: [ - { - evaluateName: 'Person', - name: 'Person', - type: 'type', - value: "", - variablesReference: 11 - }, - { - evaluateName: 'display', - name: 'display', - type: 'builtin_function_or_method', - value: - '', - variablesReference: 4 - }, - { - evaluateName: 'p1', - name: 'p1', - type: 'Person', - value: '<__main__.Person object at 0x7f565c73b0b8>', - variablesReference: 13, - 'p1.name': { - evaluateName: 'p1.name', - name: 'name', - type: 'str', - presentationHint: { - attributes: ['rawString'] - }, - value: 'John', - variablesReference: 0 - }, - 'p1.age': { - evaluateName: 'p1.age', - name: 'age', - type: 'int', - presentationHint: { - attributes: ['rawString'] - }, - value: 35, - variablesReference: 0 - } - }, - { - evaluateName: 'ptvsd', - name: 'ptvsd', - type: 'module', - value: - "", - variablesReference: 5 - }, - { - evaluateName: '_pydev_stop_at_break', - name: '_pydev_stop_at_break', - type: 'function', - value: '', - variablesReference: 12 - }, - { - evaluateName: '__annotations__', - name: '__annotations__', - type: 'dict', - value: '{}', - variablesReference: 6 - }, - { - evaluateName: '__builtins__', - name: '__builtins__', - type: 'module', - value: "", - variablesReference: 7 - }, - { - evaluateName: '__doc__', - name: '__doc__', - type: 'NoneType', - value: 'None', - variablesReference: 0 - }, - { - evaluateName: '__loader__', - name: '__loader__', - type: 'type', - value: "", - variablesReference: 8 - }, - { - evaluateName: '__name__', - name: '__name__', - presentationHint: { - attributes: ['rawString'] - }, - type: 'str', - value: "'__main__'", - variablesReference: 0 - }, - { - evaluateName: '__package__', - name: '__package__', - type: 'NoneType', - value: 'None', - variablesReference: 0 - }, - { - evaluateName: '__spec__', - name: '__spec__', - type: 'NoneType', - value: 'None', - variablesReference: 0 - } - ] - } -]; From c6300ada70e3fc96d85370b84173216c809a6b56 Mon Sep 17 00:00:00 2001 From: Borys Palka Date: Tue, 12 Nov 2019 11:38:30 +0100 Subject: [PATCH 06/14] refactor after code review --- src/service.ts | 17 ++++++++++---- src/variables/body/index.tsx | 2 +- src/variables/index.ts | 43 +----------------------------------- 3 files changed, 15 insertions(+), 47 deletions(-) diff --git a/src/service.ts b/src/service.ts index 0828293f..991ca6f6 100644 --- a/src/service.ts +++ b/src/service.ts @@ -333,12 +333,21 @@ export class DebugService implements IDebugger { const reply = await this.session.sendRequest('variables', { variablesReference: variable.variablesReference }); - let newVariable = { ...variable }; - reply.body.variables.forEach((ele: DebugProtocol.Variable) => { - newVariable = { [ele.evaluateName]: ele, ...newVariable }; + let newVariable = { ...variable, haveMoreDetails: Symbol('haveDetails') }; + + reply.body.variables.forEach((variable: DebugProtocol.Variable) => { + newVariable = { [variable.name]: variable, ...newVariable }; + }); + + const newScope = this._model.variablesModel.scopes.map(scope => { + const findIndex = scope.variables.findIndex( + ele => ele.variablesReference === variable.variablesReference + ); + scope.variables[findIndex] = newVariable; + return { ...scope }; }); - this._model.variablesModel.currentVariable = newVariable; + this._model.variablesModel.scopes = [...newScope]; return reply.body.variables; }; diff --git a/src/variables/body/index.tsx b/src/variables/body/index.tsx index 14300572..37030205 100644 --- a/src/variables/body/index.tsx +++ b/src/variables/body/index.tsx @@ -69,7 +69,7 @@ const VariableComponent = ({ model }: { model: Variables.Model }) => { const converted = scopes.map(scope => { const newVariable = scope.variables.map(variable => { const func = () => { - model.getMoreDataOfVariable(variable); + model.expandVariable(variable); }; if (variable.haveMoreDetails || variable.variablesReference === 0) { diff --git a/src/variables/index.ts b/src/variables/index.ts index 1c227959..d5c9c730 100644 --- a/src/variables/index.ts +++ b/src/variables/index.ts @@ -71,29 +71,6 @@ export namespace Variables { return this._changed; } - get currentVariable(): IVariable { - return this._currentVariable; - } - - set currentVariable(variable: IVariable) { - if (this._currentVariable === variable) { - return; - } - - variable.haveMoreDetails = Symbol('haveDetails'); - this._currentVariable = variable; - this._currentChanged.emit(variable); - - const newScope = this.scopes.map(scope => { - const findIndex = scope.variables.findIndex( - ele => ele.variablesReference === variable.variablesReference - ); - scope.variables[findIndex] = variable; - return { ...scope }; - }); - this.scopes = [...newScope]; - } - get scopes(): IScope[] { return this._state; } @@ -103,33 +80,15 @@ export namespace Variables { this._changed.emit(); } - get variables(): IVariable[] { - return this._currentScope ? this._currentScope.variables : []; - } - - set variables(variables: IVariable[]) { - this._currentScope.variables = variables; - this._changed.emit(); - } - get variableExpanded(): ISignal { return this._variableExpanded; } - getCurrentVariables(): IVariable[] { - return this.variables; - } - - getMoreDataOfVariable(variable: IVariable) { + expandVariable(variable: IVariable) { this._variableExpanded.emit(variable); } protected _state: IScope[]; - - private _currentVariable: IVariable; - private _currentScope: IScope; - - private _currentChanged = new Signal(this); private _variableExpanded = new Signal(this); private _changed = new Signal(this); } From e5353fedc21a0fc4489db9a391ec9154ea7b0956 Mon Sep 17 00:00:00 2001 From: Borys Palka Date: Tue, 12 Nov 2019 13:30:38 +0100 Subject: [PATCH 07/14] not render node variables during get/expanded variable --- src/variables/body/index.tsx | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/variables/body/index.tsx b/src/variables/body/index.tsx index 37030205..3f130761 100644 --- a/src/variables/body/index.tsx +++ b/src/variables/body/index.tsx @@ -68,14 +68,13 @@ const VariableComponent = ({ model }: { model: Variables.Model }) => { const convertForObjectInspector = (scopes: Variables.IScope[]) => { const converted = scopes.map(scope => { const newVariable = scope.variables.map(variable => { - const func = () => { - model.expandVariable(variable); - }; - if (variable.haveMoreDetails || variable.variablesReference === 0) { return { ...filterVariable(variable) }; } else { - return { getMoreDetails: func, ...filterVariable(variable) }; + return { + expandVariable: model.expandVariable(variable), + ...filterVariable(variable) + }; } }); return { name: scope.name, variables: newVariable }; @@ -149,7 +148,6 @@ const defaultNodeRenderer = ({ expanded: boolean; theme?: string | Partial; }) => { - let dontDisplay = false; const types = ['bool', 'str', 'int', 'float']; const label = data.name === '' || data.name == null ? name : data.name; const value = types.includes(data.type) @@ -158,14 +156,20 @@ const defaultNodeRenderer = ({ ? 'class' : data.type; - if (expanded) { - if (data.getMoreDetails) { - data.getMoreDetails(); - dontDisplay = true; + useEffect(() => { + if (expanded) { + if (data.expandVariable) { + data.expandVariable(); + // for not node without rest variables,props + void requestAnimationFrame(() => { + return; + }); + return; + } } - } + }); - return dontDisplay ? null : depth === 0 ? ( + return depth === 0 ? ( {label} : From dbead195a59b24f12c80fc43db2960939ee650f3 Mon Sep 17 00:00:00 2001 From: Jeremy Tuloup Date: Wed, 13 Nov 2019 13:08:38 +0100 Subject: [PATCH 08/14] Call expandVariable on expand --- src/service.ts | 4 ++-- src/variables/body/index.tsx | 14 ++++++-------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/service.ts b/src/service.ts index 991ca6f6..a13afbe0 100644 --- a/src/service.ts +++ b/src/service.ts @@ -339,7 +339,7 @@ export class DebugService implements IDebugger { newVariable = { [variable.name]: variable, ...newVariable }; }); - const newScope = this._model.variablesModel.scopes.map(scope => { + const newScopes = this._model.variablesModel.scopes.map(scope => { const findIndex = scope.variables.findIndex( ele => ele.variablesReference === variable.variablesReference ); @@ -347,7 +347,7 @@ export class DebugService implements IDebugger { return { ...scope }; }); - this._model.variablesModel.scopes = [...newScope]; + this._model.variablesModel.scopes = [...newScopes]; return reply.body.variables; }; diff --git a/src/variables/body/index.tsx b/src/variables/body/index.tsx index a968802a..99ad2cfe 100644 --- a/src/variables/body/index.tsx +++ b/src/variables/body/index.tsx @@ -45,11 +45,10 @@ const VariableComponent = ({ model }: { model: Variables.Model }) => { let valueOfKey = key === 'value' ? convertType(variable) : (variable as any)[key]; if (typeof valueOfKey === 'object') { - return Object.assign(res, filterVariable( - valueOfKey, - true, - key - ) as Object); + return Object.assign( + res, + filterVariable(valueOfKey, true, key) as Object + ); } if (isObject) { return Object.assign(res, { @@ -66,20 +65,19 @@ const VariableComponent = ({ model }: { model: Variables.Model }) => { }; const convertForObjectInspector = (scopes: Variables.IScope[]) => { - const converted = scopes.map(scope => { + return scopes.map(scope => { const newVariable = scope.variables.map(variable => { if (variable.haveMoreDetails || variable.variablesReference === 0) { return { ...filterVariable(variable) }; } else { return { - expandVariable: model.expandVariable(variable), + expandVariable: () => model.expandVariable(variable), ...filterVariable(variable) }; } }); return { name: scope.name, variables: newVariable }; }); - return converted; }; useEffect(() => { From b3a1164cf65bd3af50bf97c0ff33fc4798c3347a Mon Sep 17 00:00:00 2001 From: Jeremy Tuloup Date: Wed, 13 Nov 2019 13:57:30 +0100 Subject: [PATCH 09/14] Simplify node renderer effect --- src/variables/body/index.tsx | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/variables/body/index.tsx b/src/variables/body/index.tsx index 99ad2cfe..1657cc63 100644 --- a/src/variables/body/index.tsx +++ b/src/variables/body/index.tsx @@ -155,16 +155,10 @@ const defaultNodeRenderer = ({ : data.type; useEffect(() => { - if (expanded) { - if (data.expandVariable) { - data.expandVariable(); - // for not node without rest variables,props - void requestAnimationFrame(() => { - return; - }); - return; - } + if (!expanded || !data.expandVariable) { + return; } + data.expandVariable(); }); return depth === 0 ? ( From 4f66079128573d252f07e3381bb2badddb4f9a6f Mon Sep 17 00:00:00 2001 From: Jeremy Tuloup Date: Wed, 13 Nov 2019 15:19:23 +0100 Subject: [PATCH 10/14] Refactor convertType --- src/variables/body/index.tsx | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/variables/body/index.tsx b/src/variables/body/index.tsx index 1657cc63..65785c21 100644 --- a/src/variables/body/index.tsx +++ b/src/variables/body/index.tsx @@ -114,21 +114,19 @@ const VariableComponent = ({ model }: { model: Variables.Model }) => { }; const convertType = (variable: Variables.IVariable) => { - const type = variable.type; - let value: any = variable.value; - if (type === 'int' || type === 'float') { - return value * 1; + const { type, value } = variable; + switch (type) { + case 'int': + return parseInt(value, 10); + case 'float': + return parseFloat(value); + case 'bool': + return value === 'False' ? false : true; + case 'str': + return value.slice(1, value.length - 1); + default: + return value; } - if (type === 'bool') { - return value === 'False' ? false : true; - } - if (type === 'str') { - return (value as string) - .split('') - .slice(1, value.length - 1) - .join(''); - } - return value; }; const defaultNodeRenderer = ({ From 2a2cb0bf7a25ad51f65345e05afd5aaf2d86a28c Mon Sep 17 00:00:00 2001 From: Jeremy Tuloup Date: Wed, 13 Nov 2019 16:35:26 +0100 Subject: [PATCH 11/14] Streamline the use of Object.assign --- src/variables/body/index.tsx | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/variables/body/index.tsx b/src/variables/body/index.tsx index 65785c21..49d452ad 100644 --- a/src/variables/body/index.tsx +++ b/src/variables/body/index.tsx @@ -45,20 +45,16 @@ const VariableComponent = ({ model }: { model: Variables.Model }) => { let valueOfKey = key === 'value' ? convertType(variable) : (variable as any)[key]; if (typeof valueOfKey === 'object') { - return Object.assign( - res, - filterVariable(valueOfKey, true, key) as Object - ); + return { ...res, ...filterVariable(valueOfKey, true, key) }; } if (isObject) { - return Object.assign(res, { - [keyObj]: valueOfKey - }); + return { ...res, [keyObj]: valueOfKey }; } - return Object.assign(res, { + return { + ...res, [key]: valueOfKey - }); + }; }, {}); return filteredObj; From 31a1ed109efca589636f1d4a2afbad3574b120b5 Mon Sep 17 00:00:00 2001 From: Jeremy Tuloup Date: Wed, 13 Nov 2019 16:38:34 +0100 Subject: [PATCH 12/14] Remove special case for class --- src/variables/body/index.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/variables/body/index.tsx b/src/variables/body/index.tsx index 49d452ad..c0a7ff36 100644 --- a/src/variables/body/index.tsx +++ b/src/variables/body/index.tsx @@ -142,11 +142,7 @@ const defaultNodeRenderer = ({ }) => { const types = ['bool', 'str', 'int', 'float']; const label = data.name === '' || data.name == null ? name : data.name; - const value = types.includes(data.type) - ? data.value - : data.type === 'type' - ? 'class' - : data.type; + const value = types.includes(data.type) ? data.value : data.type; useEffect(() => { if (!expanded || !data.expandVariable) { From 2adbc777768f275a1139cbbcee43c9017a4d8d74 Mon Sep 17 00:00:00 2001 From: Jeremy Tuloup Date: Wed, 13 Nov 2019 17:00:17 +0100 Subject: [PATCH 13/14] Make the filterVariable function more readable --- src/variables/body/index.tsx | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/variables/body/index.tsx b/src/variables/body/index.tsx index c0a7ff36..f1a96ff4 100644 --- a/src/variables/body/index.tsx +++ b/src/variables/body/index.tsx @@ -28,6 +28,7 @@ export class Body extends ReactWidget { const VariableComponent = ({ model }: { model: Variables.Model }) => { const [data, setData] = useState(model.scopes); + // TODO: this should be simplified and extracted from the component const filterVariable = ( variable: Variables.IVariable, isObject?: boolean, @@ -35,12 +36,21 @@ const VariableComponent = ({ model }: { model: Variables.Model }) => { ): Object => { const tableKey = ['name', 'value', 'type']; const filteredObj = Object.keys(variable) - .filter( - key => - (isObject ? key === 'value' : tableKey.includes(key)) || - (key !== 'presentationHint' && - typeof (variable as any)[key] === 'object') - ) + .filter(key => { + if (isObject && key === 'value') { + return true; + } + if (tableKey.includes(key)) { + return true; + } + if ( + typeof (variable as any)[key] === 'object' && + key !== 'presentationHint' + ) { + return true; + } + return false; + }) .reduce((res, key) => { let valueOfKey = key === 'value' ? convertType(variable) : (variable as any)[key]; From 327ba2770daa36e8faba21f60885bf7337421616 Mon Sep 17 00:00:00 2001 From: Jeremy Tuloup Date: Wed, 13 Nov 2019 17:18:41 +0100 Subject: [PATCH 14/14] Rename haveMoreDetails to expanded --- src/service.ts | 2 +- src/variables/body/index.tsx | 2 +- src/variables/index.ts | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/service.ts b/src/service.ts index a13afbe0..c9f792fa 100644 --- a/src/service.ts +++ b/src/service.ts @@ -333,7 +333,7 @@ export class DebugService implements IDebugger { const reply = await this.session.sendRequest('variables', { variablesReference: variable.variablesReference }); - let newVariable = { ...variable, haveMoreDetails: Symbol('haveDetails') }; + let newVariable = { ...variable, expanded: true }; reply.body.variables.forEach((variable: DebugProtocol.Variable) => { newVariable = { [variable.name]: variable, ...newVariable }; diff --git a/src/variables/body/index.tsx b/src/variables/body/index.tsx index f1a96ff4..6ed77350 100644 --- a/src/variables/body/index.tsx +++ b/src/variables/body/index.tsx @@ -73,7 +73,7 @@ const VariableComponent = ({ model }: { model: Variables.Model }) => { const convertForObjectInspector = (scopes: Variables.IScope[]) => { return scopes.map(scope => { const newVariable = scope.variables.map(variable => { - if (variable.haveMoreDetails || variable.variablesReference === 0) { + if (variable.expanded || variable.variablesReference === 0) { return { ...filterVariable(variable) }; } else { return { diff --git a/src/variables/index.ts b/src/variables/index.ts index d5c9c730..1af306c5 100644 --- a/src/variables/index.ts +++ b/src/variables/index.ts @@ -53,8 +53,7 @@ class VariablesHeader extends Widget { export namespace Variables { export interface IVariable extends DebugProtocol.Variable { - getMoreDetails?: any; - haveMoreDetails?: Symbol; + expanded?: boolean; } export interface IScope {