浏览代码

Merge pull request #8 from jupyterlab/master

merge master
KsavinN 5 年之前
父节点
当前提交
8752e6f18d

+ 19 - 0
DESIGN.md

@@ -0,0 +1,19 @@
+# `@jupyterlab/debugger` user interactions and experience
+
+- The debugger UI will only ever exist as a single instance within JupyterLab:
+  - An expanded mode which is a `MainAreaWidget<Debugger>`
+    - In expanded mode, the debugger will contain a tab panel for text editors that are launched when a user steps into code that has been stopped via breakpoints.
+    - If the user adds a breakpoint and steps into code from the sidebar, then it should automatically switch to expanded mode to accommodate displaying code files.
+    - Code files in the debugger will _always_ be read-only. They should support adding breakpoints.
+  - A condensed mode which is a condensed sidebar view of the debugger within the `right` or `left` areas of the JupyterLab shell
+- The debugger will support debugging and inspecting environment for three types of activities in JupyterLab:
+  1. Notebooks
+  1. Code consoles
+  1. Text editors that are backed by a kernel
+- A `JupyterFrontendPlugin` will exist to track each of the activities and each time the `currentChanged` signal fires on one of the trackers, the debugger UI will reflect the state of that activity, _e.g._:
+  - If a user has a single open notebook and opens the debugger, it will open either "docked" in the sidebar or in the main area depending on the last known position of the debugger.
+  - If the user then opens a new notebook or switches to a code console, then the debugger will update to show the state of the newly focused kernel.
+- The debugger should be state-less insofar as it can arbitrarily switch to displaying the breakpoints and variables of a new kernel based on a user switching from one notebook to another, _etc._:
+  - The debugger should automatically start a debugging session with the kernel transparently without end-user intervention.
+  - Any UI information that cannot be retrieved from the kernel needs to be stored in a different channel (for example, inside a `StateDB`) or discarded. In particular, if the kernel cannot return a list of breakpoints that have been set, then it becomes the debugger UI's responsibility to rehydrate and dehydrate these as needed.
+  - When the application is `restored` or when a debugger UI is instantiated, it should appear docked or expanded (this can perhaps be `type Debugger.Mode = 'condensed' | 'expanded'`) based on its last known state from a previous session.

+ 2 - 6
README.md

@@ -7,19 +7,17 @@ This extension is under active development and is not yet available.
 ## Prerequisites
 
 - JupyterLab 1.1+
+- xeus-python 0.5+
 
 ## Development
 
 ```bash
 # Create a new conda environment
-conda create -n jupyterlab-debugger -c conda-forge jupyterlab nodejs xeus-python ptvsd
+conda create -n jupyterlab-debugger -c conda-forge jupyterlab nodejs xeus-python=0.5 ptvsd
 
 # Activate the conda environment
 conda activate jupyterlab-debugger
 
-# Create a directory for the kernel debug logs in the folder where JupyterLab is started
-mkdir xpython_debug_logs
-
 # Install dependencies
 jlpm
 
@@ -41,8 +39,6 @@ XEUS_LOG=1 jupyter lab --no-browser --watch
 
 ### Tests
 
-Make sure `xeus-python` is installed and `jupyter --paths` points to where the kernel is installed.
-
 To run the tests:
 
 ```bash

+ 1 - 2
azure-pipelines.yml

@@ -16,7 +16,7 @@ steps:
 
 - bash: |
     source activate jupyterlab-debugger
-    conda install --yes --quiet -c conda-forge nodejs xeus-python ptvsd python=$PYTHON_VERSION
+    conda install --yes --quiet -c conda-forge nodejs xeus-python=0.5 ptvsd python=$PYTHON_VERSION
     python -m pip install -U --pre jupyterlab
   displayName: Install dependencies
 
@@ -28,7 +28,6 @@ steps:
 
 - bash: |
     source activate jupyterlab-debugger
-    export JUPYTER_PATH=${CONDA_PREFIX}/share/jupyter
     export XEUS_LOG=1
     jlpm run test
   displayName: Run the tests

+ 10 - 10
package.json

@@ -39,15 +39,15 @@
     "watch": "tsc -b --watch"
   },
   "dependencies": {
-    "@jupyterlab/application": "^1.1.0-alpha.1",
-    "@jupyterlab/apputils": "^1.1.0-alpha.1",
-    "@jupyterlab/codeeditor": "^1.1.0-alpha.1",
-    "@jupyterlab/console": "^1.1.0-alpha.1",
-    "@jupyterlab/coreutils": "^3.1.0-alpha.1",
-    "@jupyterlab/fileeditor": "^1.1.0-alpha.1",
-    "@jupyterlab/launcher": "^1.1.0-alpha.1",
-    "@jupyterlab/notebook": "^1.1.0-alpha.1",
-    "@jupyterlab/services": "^4.1.0-alpha.1",
+    "@jupyterlab/application": "^1.1.0",
+    "@jupyterlab/apputils": "^1.1.0",
+    "@jupyterlab/codeeditor": "^1.1.0",
+    "@jupyterlab/console": "^1.1.0",
+    "@jupyterlab/coreutils": "^3.1.0",
+    "@jupyterlab/fileeditor": "^1.1.0",
+    "@jupyterlab/launcher": "^1.1.0",
+    "@jupyterlab/notebook": "^1.1.0",
+    "@jupyterlab/services": "^4.1.0",
     "@phosphor/algorithm": "^1.2.0",
     "@phosphor/coreutils": "^1.3.1",
     "@phosphor/disposable": "^1.2.0",
@@ -57,7 +57,7 @@
   "devDependencies": {
     "@babel/core": "^7.5.5",
     "@babel/preset-env": "^7.5.5",
-    "@jupyterlab/testutils": "^1.1.0-alpha.1",
+    "@jupyterlab/testutils": "^1.1.0",
     "@types/chai": "^4.1.3",
     "@types/jest": "^24.0.17",
     "chai": "^4.2.0",

+ 83 - 0
src/breakpoints/body.tsx

@@ -0,0 +1,83 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import React, { useState } from 'react';
+import { Breakpoints } from '.';
+import { ReactWidget } from '@jupyterlab/apputils';
+import { ArrayExt } from '@phosphor/algorithm';
+import { ISignal } from '@phosphor/signaling';
+
+export class Body extends ReactWidget {
+  constructor(model: Breakpoints.IModel) {
+    super();
+    this.model = model;
+    this.addClass('jp-DebuggerBreakpoints-body');
+  }
+
+  render() {
+    return <BreakpointsComponent model={this.model} />;
+  }
+
+  readonly model: Breakpoints.IModel;
+}
+
+const BreakpointsComponent = ({ model }: { model: Breakpoints.IModel }) => {
+  const [breakpoints, setBreakpoints] = useState(model.breakpoints);
+
+  model.breakpointsChanged.connect(
+    (_: Breakpoints.IModel, updates: Breakpoints.IBreakpoint[]) => {
+      if (ArrayExt.shallowEqual(breakpoints, updates)) {
+        return;
+      }
+      setBreakpoints(updates);
+    }
+  );
+
+  return (
+    <div>
+      {breakpoints.map((breakpoint: any) => (
+        <BreakpointComponent
+          key={breakpoint.id}
+          breakpoint={breakpoint}
+          breakpointChanged={model.breakpointChanged}
+        />
+      ))}
+    </div>
+  );
+};
+
+const BreakpointComponent = ({
+  breakpoint,
+  breakpointChanged
+}: {
+  breakpoint: Breakpoints.IBreakpoint;
+  breakpointChanged: ISignal<Breakpoints.IModel, Breakpoints.IBreakpoint>;
+}) => {
+  const [active, setActive] = useState(breakpoint.active);
+  breakpoint.active = active;
+
+  breakpointChanged.connect(
+    (_: Breakpoints.IModel, updates: Breakpoints.IBreakpoint) => {
+      setActive(updates.active);
+    }
+  );
+
+  const setBreakpointEnabled = (state: boolean) => {
+    setActive(state);
+  };
+
+  return (
+    <div className={`breakpoint`}>
+      <input
+        onChange={() => {
+          setBreakpointEnabled(!active);
+        }}
+        type="checkbox"
+        checked={active}
+      />
+      <span>
+        {breakpoint.source.name} : {breakpoint.line}
+      </span>
+    </div>
+  );
+};

+ 84 - 5
src/breakpoints/index.ts

@@ -4,16 +4,20 @@
 import { Toolbar, ToolbarButton } from '@jupyterlab/apputils';
 
 import { Widget, Panel, PanelLayout } from '@phosphor/widgets';
+import { DebugProtocol } from 'vscode-debugprotocol';
+import { Body } from './body';
+import { Signal, ISignal } from '@phosphor/signaling';
 
 export class Breakpoints extends Panel {
   constructor(options: Breakpoints.IOptions = {}) {
     super();
 
-    this.model = {};
+    this.model = new Breakpoints.IModel(MOCK_BREAKPOINTS);
     this.addClass('jp-DebuggerBreakpoints');
     this.title.label = 'Breakpoints';
 
     const header = new BreakpointsHeader(this.title.label);
+    this.body = new Body(this.model);
 
     this.addWidget(header);
     this.addWidget(this.body);
@@ -22,16 +26,31 @@ export class Breakpoints extends Panel {
       'deactivate',
       new ToolbarButton({
         iconClassName: 'jp-DebuggerDeactivateIcon',
+        tooltip: `${this.isAllActive ? 'Deactivate' : 'Activate'} Breakpoints`,
         onClick: () => {
-          console.log('`deactivate` was clicked');
+          this.isAllActive = !this.isAllActive;
+          this.model.breakpoints.map((breakpoint: Breakpoints.IBreakpoint) => {
+            breakpoint.active = this.isAllActive;
+            this.model.breakpoint = breakpoint;
+          });
+        }
+      })
+    );
+
+    header.toolbar.addItem(
+      'closeAll',
+      new ToolbarButton({
+        iconClassName: 'jp-CloseAllIcon',
+        onClick: () => {
+          this.model.breakpoints = [];
         },
-        tooltip: 'Deactivate Breakpoints'
+        tooltip: 'Remove All Breakpoints'
       })
     );
   }
 
-  readonly body = new Panel();
-
+  private isAllActive = true;
+  readonly body: Widget;
   readonly model: Breakpoints.IModel;
 }
 
@@ -52,13 +71,73 @@ class BreakpointsHeader extends Widget {
 }
 
 export namespace Breakpoints {
+  export interface IBreakpoint extends DebugProtocol.Breakpoint {
+    active: boolean;
+  }
+
   /**
    * The breakpoints UI model.
    */
   export interface IModel {}
 
+  export class IModel implements IModel {
+    constructor(model: IBreakpoint[]) {
+      this._state = model;
+    }
+
+    get breakpointsChanged(): ISignal<this, IBreakpoint[]> {
+      return this._breakpointsChanged;
+    }
+
+    get breakpoints(): IBreakpoint[] {
+      return this._state;
+    }
+
+    get breakpointChanged(): ISignal<this, IBreakpoint> {
+      return this._breakpointChanged;
+    }
+
+    set breakpoints(breakpoints: IBreakpoint[]) {
+      this._state = breakpoints;
+      this._breakpointsChanged.emit(this._state);
+    }
+
+    set breakpoint(breakpoint: IBreakpoint) {
+      const index = this._state.findIndex(ele => ele.id === breakpoint.id);
+      if (index !== -1) {
+        this._state[index] = breakpoint;
+        this._breakpointChanged.emit(breakpoint);
+      }
+    }
+
+    private _state: IBreakpoint[];
+    private _breakpointsChanged = new Signal<this, IBreakpoint[]>(this);
+    private _breakpointChanged = new Signal<this, IBreakpoint>(this);
+  }
+
   /**
    * Instantiation options for `Breakpoints`;
    */
   export interface IOptions extends Panel.IOptions {}
 }
+
+const MOCK_BREAKPOINTS = [
+  {
+    id: 0,
+    active: true,
+    verified: true,
+    source: {
+      name: 'untitled.py'
+    },
+    line: 6
+  },
+  {
+    id: 1,
+    verified: true,
+    active: false,
+    source: {
+      name: 'untitled.py'
+    },
+    line: 7
+  }
+];

+ 42 - 0
src/callstack/body.tsx

@@ -0,0 +1,42 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import React, { useState } from 'react';
+import { Callstack } from '.';
+import { ReactWidget } from '@jupyterlab/apputils';
+
+export class Body extends ReactWidget {
+  constructor(model: Callstack.IModel) {
+    super();
+    this.model = model;
+    this.addClass('jp-DebuggerCallstack-body');
+  }
+
+  render() {
+    return <FramesComponent model={this.model} />;
+  }
+
+  readonly model: Callstack.IModel;
+}
+
+const FramesComponent = ({ model }: { model: Callstack.IModel }) => {
+  const [frames] = useState(model.frames);
+  const [selected, setSelected] = useState();
+  const onSelected = (frame: any) => {
+    setSelected(frame);
+  };
+
+  return (
+    <ul>
+      {frames.map(ele => (
+        <li
+          key={ele.id}
+          onClick={() => onSelected(ele)}
+          className={selected === ele ? 'selected' : ''}
+        >
+          {ele.name} at {ele.source.name}:{ele.line}
+        </li>
+      ))}
+    </ul>
+  );
+};

+ 44 - 4
src/callstack/index.ts

@@ -4,19 +4,22 @@
 import { Toolbar, ToolbarButton } from '@jupyterlab/apputils';
 
 import { Widget, Panel, PanelLayout } from '@phosphor/widgets';
+import { Body } from './body';
+import { DebugProtocol } from 'vscode-debugprotocol';
 
 export class Callstack extends Panel {
   constructor(options: Callstack.IOptions = {}) {
     super();
 
-    this.model = {};
+    this.model = new Callstack.IModel(MOCK_FRAMES);
     this.addClass('jp-DebuggerCallstack');
     this.title.label = 'Callstack';
 
     const header = new CallstackHeader(this.title.label);
+    const body = new Body(this.model);
 
     this.addWidget(header);
-    this.addWidget(this.body);
+    this.addWidget(body);
 
     header.toolbar.addItem(
       'continue',
@@ -70,8 +73,6 @@ export class Callstack extends Panel {
     );
   }
 
-  readonly body = new Panel();
-
   readonly model: Callstack.IModel;
 }
 
@@ -92,7 +93,46 @@ class CallstackHeader extends Widget {
 }
 
 export namespace Callstack {
+  export interface IFrame extends DebugProtocol.StackFrame {}
+
   export interface IModel {}
 
+  export class IModel implements IModel {
+    constructor(model: IFrame[]) {
+      this.state = model;
+    }
+
+    set frames(newFrames: IFrame[]) {
+      this.state = newFrames;
+    }
+
+    get frames(): IFrame[] {
+      return this.state;
+    }
+
+    private state: IFrame[];
+  }
+
   export interface IOptions extends Panel.IOptions {}
 }
+
+const MOCK_FRAMES: Callstack.IFrame[] = [
+  {
+    id: 0,
+    name: 'test',
+    source: {
+      name: 'untitled.py'
+    },
+    line: 6,
+    column: 1
+  },
+  {
+    id: 1,
+    name: '<module>',
+    source: {
+      name: 'untitled.py'
+    },
+    line: 7,
+    column: 1
+  }
+];

+ 1 - 0
src/tokens.ts

@@ -114,6 +114,7 @@ export namespace IDebugger {
       terminateThreads: DebugProtocol.TerminateThreadsArguments;
       threads: {};
       updateCell: IUpdateCellArguments;
+      variables: DebugProtocol.VariablesArguments;
     };
 
     /**

+ 13 - 10
src/variables/body/index.ts

@@ -8,6 +8,7 @@ import { Variables } from '../index';
 import { Table } from './table';
 
 import { Search } from './search';
+import { Message } from '@phosphor/messaging';
 
 export class Body extends Panel {
   constructor(model: Variables.IModel) {
@@ -40,20 +41,22 @@ class Description extends Widget {
 
     this.model.currentChanged.connect(
       (model: Variables.IModel, variable: Variables.IVariable) => {
-        this.node.innerHTML = this.renderDescription(this.model.current);
+        this.currentVariable = variable;
+        this.update();
       }
     );
   }
 
-  model: Variables.IModel;
-
-  // Still in progres: rendering description
-
-  protected renderDescription(variable: Variables.IVariable) {
-    const descriptionElementDOM = `<b>name: ${variable.name}</b>
-                                       <p>type: ${variable.type} </p>
+  protected onUpdateRequest(msg: Message) {
+    if (!this.currentVariable) {
+      return;
+    }
+    this.node.innerHTML = `<b>name: ${this.currentVariable.name}</b>
+                                       <p>type: ${this.currentVariable.type} </p>
                                        Description:
-                                       <p>${variable.description}</p> `;
-    return descriptionElementDOM;
+                                       <p>${this.currentVariable.description}</p> `;
   }
+
+  model: Variables.IModel;
+  currentVariable: Variables.IVariable;
 }

+ 18 - 12
src/variables/body/search.tsx

@@ -10,14 +10,13 @@ import React, { useState, useRef, useEffect } from 'react';
 import { Variables } from '../index';
 
 export class Search extends Widget {
-  constructor(model: any) {
+  constructor(model: Variables.IModel) {
     super();
     this.addClass('jp-DebuggerVariables-search');
 
     const layout = new PanelLayout();
     this.layout = layout;
-    this.node.style.overflow = 'visible';
-    this.scope = new ScopeSearch();
+    this.scope = new ScopeSearch(model);
     this.search = new SearchInput(model);
 
     layout.addWidget(this.scope);
@@ -62,15 +61,18 @@ class SearchInput extends ReactWidget {
 }
 
 class ScopeSearch extends ReactWidget {
-  constructor() {
+  constructor(model: Variables.IModel) {
     super();
+    this.model = model;
     this.node.style.overflow = 'visible';
     this.node.style.width = '85px';
     this.addClass('jp-DebuggerVariables-scope');
   }
 
+  model: Variables.IModel;
+
   render() {
-    return <ScopeMenuComponent />;
+    return <ScopeMenuComponent model={this.model} />;
   }
 }
 
@@ -90,10 +92,11 @@ const useOutsideClick = (ref: any, callback: any) => {
   });
 };
 
-const ScopeMenuComponent = ({ config }: any) => {
+const ScopeMenuComponent = ({ model }: { model: Variables.IModel }) => {
   const [toggleState, setToggle] = useState(false);
-  const [scope, setScope] = useState('local');
+  const [scope, setScope] = useState(model.currentScope);
   const wrapperRef = useRef(null);
+  const scopes = model.scopes;
 
   const onClickOutSide = () => {
     setToggle(false);
@@ -105,25 +108,28 @@ const ScopeMenuComponent = ({ config }: any) => {
 
   useOutsideClick(wrapperRef, onClickOutSide);
 
-  const changeScope = (newScope: string) => {
+  const changeScope = (newScope: Variables.IScope) => {
     if (newScope === scope) {
       return;
     }
     setScope(newScope);
+    model.currentScope = newScope;
     setToggle(false);
   };
 
   const List = (
     <ul>
-      <li onClick={e => changeScope('local')}>local</li>
-      <li onClick={e => changeScope('global')}>global</li>
-      <li onClick={e => changeScope('built-in')}>built-in</li>
+      {scopes.map(scope => (
+        <li key={scope.name} onClick={e => changeScope(scope)}>
+          {scope.name}
+        </li>
+      ))}
     </ul>
   );
 
   return (
     <div onClick={e => toggle(e)} ref={wrapperRef}>
-      <span className="label">{scope}</span>
+      <span className="label">{scope.name}</span>
       <span className="fa fa-caret-down"></span>
       {toggleState ? List : null}
     </div>

+ 2 - 2
src/variables/body/table.tsx

@@ -58,7 +58,7 @@ export class Table extends ReactWidget {
 
 const TableComponent = ({ model }: { model: Variables.IModel }) => {
   const [variables, setVariables] = useState(model.variables);
-  const [variable, TableBody] = useTbody(variables, model.current);
+  const [variable, TableBody] = useTbody(variables, model.currentVariable);
 
   model.variablesChanged.connect((_: any, updates) => {
     if (ArrayExt.shallowEqual(variables, updates)) {
@@ -67,7 +67,7 @@ const TableComponent = ({ model }: { model: Variables.IModel }) => {
     setVariables(updates);
   });
 
-  model.current = variable;
+  model.currentVariable = variable;
 
   return (
     <div>

+ 101 - 49
src/variables/index.ts

@@ -13,7 +13,7 @@ export class Variables extends Panel {
   constructor(options: Variables.IOptions = {}) {
     super();
 
-    this.model = new Variables.IModel(MOCK_DATA_ROW.variables);
+    this.model = new Variables.IModel(MOCK_DATA_ROW.scopes);
     this.addClass('jp-DebuggerVariables');
     this.title.label = 'Variables';
 
@@ -46,25 +46,43 @@ export namespace Variables {
     description: string;
   }
 
+  export interface IScope {
+    name: string;
+    variables: IVariable[];
+  }
+
   export interface IModel {}
 
   export class IModel implements IModel {
-    constructor(model: IVariable[]) {
+    constructor(model: IScope[]) {
       this._state = model;
+      this._currentScope = this._state[0];
+    }
+
+    get currentScope(): IScope {
+      return this._currentScope;
+    }
+
+    set currentScope(value: IScope) {
+      if (this._currentScope === value) {
+        return;
+      }
+      this._currentScope = value;
+      this._variablesChanged.emit(value.variables);
     }
 
     get currentChanged(): ISignal<this, IVariable> {
       return this._currentChanged;
     }
 
-    get current(): IVariable {
-      return this._current;
+    get currentVariable(): IVariable {
+      return this._currentVariable;
     }
-    set current(variable: IVariable) {
-      if (this._current === variable) {
+    set currentVariable(variable: IVariable) {
+      if (this._currentVariable === variable) {
         return;
       }
-      this._current = variable;
+      this._currentVariable = variable;
       this._currentChanged.emit(variable);
     }
 
@@ -79,14 +97,18 @@ export namespace Variables {
       this._variablesChanged.emit(this._filterVariables());
     }
 
+    get scopes(): IScope[] {
+      return this._state;
+    }
+
     get variables(): IVariable[] {
       if (this._filterState) {
         return this._filterVariables();
       }
-      return this._state;
+      return this._currentScope.variables;
     }
     set variables(variables: IVariable[]) {
-      this._state = variables;
+      this._currentScope.variables = variables;
     }
 
     get variablesChanged(): ISignal<this, IVariable[]> {
@@ -98,7 +120,7 @@ export namespace Variables {
     }
 
     private _filterVariables(): IVariable[] {
-      return this._state.filter(
+      return this._currentScope.variables.filter(
         ele =>
           ele.name
             .toLocaleLowerCase()
@@ -106,59 +128,89 @@ export namespace Variables {
       );
     }
 
-    private _current: IVariable;
+    private _currentVariable: IVariable;
     private _currentChanged = new Signal<this, IVariable>(this);
     private _variablesChanged = new Signal<this, IVariable[]>(this);
     private _filterState: string = '';
-    private _state: IVariable[];
+    protected _state: IScope[];
+    private _currentScope: IScope;
   }
 
   export interface IOptions extends Panel.IOptions {}
 }
 
 const MOCK_DATA_ROW = {
-  variables: [
-    {
-      name: 'test 1',
-      value: 'function()',
-      type: 'function',
-      variablesReference: 0,
-      description: 'def test1(): return 0'
-    },
-    {
-      name: 'Classtest',
-      value: 'class',
-      type: 'class',
-      variablesReference: 1,
-      description: 'def test2(): return 0'
-    },
-    {
-      name: 'test 3',
-      value: 'function()',
-      type: 'function',
-      variablesReference: 0,
-      description: 'def test1(): return 0'
-    },
+  scopes: [
     {
-      name: 'test 4',
-      value: 'function()',
-      type: 'function',
-      variablesReference: 0,
-      description: 'def test2(): return 0'
+      name: 'local',
+      variables: [
+        {
+          name: 'test 1',
+          value: 'function()',
+          type: 'function',
+          variablesReference: 0,
+          description: 'def test1(): return 0'
+        },
+        {
+          name: 'Classtest',
+          value: 'class',
+          type: 'class',
+          variablesReference: 1,
+          description: 'def test2(): return 0'
+        },
+        {
+          name: 'test 3',
+          value: 'function()',
+          type: 'function',
+          variablesReference: 0,
+          description: 'def test1(): return 0'
+        },
+        {
+          name: 'test 4',
+          value: 'function()',
+          type: 'function',
+          variablesReference: 0,
+          description: 'def test2(): return 0'
+        },
+        {
+          name: 'test 5',
+          value: 'function()',
+          type: 'function',
+          variablesReference: 0,
+          description: 'def test1(): return 0'
+        },
+        {
+          name: 'test 6',
+          value: 'function()',
+          type: 'function',
+          variablesReference: 0,
+          description: 'def test2(): return 0'
+        }
+      ]
     },
     {
-      name: 'test 5',
-      value: 'function()',
-      type: 'function',
-      variablesReference: 0,
-      description: 'def test1(): return 0'
+      name: 'global',
+      variables: [
+        {
+          name: 'exampleGlobal',
+          value: 'function()',
+          type: 'function',
+          variablesReference: 0,
+          description: 'def exampleGlobal(): return 0'
+        }
+      ] as Variables.IVariable[]
     },
     {
-      name: 'test 6',
-      value: 'function()',
-      type: 'function',
-      variablesReference: 0,
-      description: 'def test2(): return 0'
+      name: 'built-in',
+      variables: [
+        {
+          name: 'exmapleBuiltIn',
+          value: 'function()',
+          type: 'function',
+          variablesReference: 0,
+          description: 'def texmapleBuiltIn(): return 0'
+        }
+      ] as Variables.IVariable[]
     }
   ]
 };

+ 4 - 0
style/breakpoints.css

@@ -2,3 +2,7 @@
 | Copyright (c) Jupyter Development Team.
 | Distributed under the terms of the Modified BSD License.
 |----------------------------------------------------------------------------*/
+
+.jp-DebuggerBreakpoints-body {
+  padding: 10px;
+}

+ 19 - 0
style/callstack.css

@@ -2,3 +2,22 @@
 | Copyright (c) Jupyter Development Team.
 | Distributed under the terms of the Modified BSD License.
 |----------------------------------------------------------------------------*/
+
+.jp-DebuggerCallstack-body ul {
+  list-style: none;
+  margin: 0;
+  padding: 0;
+  background: var(--jp-layout-color1);
+  color: var(--jp-ui-font-color1);
+  font-size: var(--jp-ui-font-size1);
+}
+
+.jp-DebuggerCallstack-body ul li {
+  padding: 5px;
+  padding-left: 8px;
+}
+
+.jp-DebuggerCallstack-body ul li.selected {
+  color: white;
+  background: var(--jp-brand-color1);
+}

+ 4 - 0
style/icons.css

@@ -54,3 +54,7 @@
 .jp-VariableIcon {
   background-image: url('icons/variable.svg');
 }
+
+.jp-CloseAllIcon {
+  background-image: url('icons/close-all.svg');
+}

+ 4 - 0
style/icons/close-all.svg

@@ -0,0 +1,4 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M8.62132 8.0858L7.91421 7.37869L6.5 8.7929L5.08579 7.37869L4.37868 8.0858L5.79289 9.50001L4.37868 10.9142L5.08579 11.6213L6.5 10.2071L7.91421 11.6213L8.62132 10.9142L7.20711 9.50001L8.62132 8.0858Z" fill="#424242"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M5 3L6 2H13L14 3V10L13 11H11V13L10 14H3L2 13V6L3 5H5V3ZM6 5H10L11 6V10H13V3H6V5ZM10 6H3V13H10V6Z" fill="#424242"/>
+</svg>

+ 1 - 0
style/variables.css

@@ -72,6 +72,7 @@
 
 .jp-DebuggerVariables-search {
   color: var(--jp-ui-font-color1);
+  overflow: visible;
   flex: 0 0 auto;
   display: flex;
   flex-direction: row;

+ 21 - 5
tests/run-test.py

@@ -2,13 +2,29 @@
 # Distributed under the terms of the Modified BSD License.
 
 import os
-from jupyterlab.tests.test_app import run_jest, JestApp
+
+from jupyterlab.tests.test_app import JestApp
 
 HERE = os.path.realpath(os.path.dirname(__file__))
 
-if __name__ == '__main__':
-    # xeus-python requires the xpython_debug_logs folder
+
+def run(jest_dir):
     jest_app = JestApp.instance()
-    os.mkdir(os.path.join(jest_app.notebook_dir, 'xpython_debug_logs'))
+    jest_app.jest_dir = jest_dir
+    jest_app.initialize()
+    jest_app.install_kernel(
+        kernel_name='xpython',
+        kernel_spec={
+            'argv': [
+                'xpython',
+                '-f', '{connection_file}'
+            ],
+            'display_name': 'xpython',
+            'language': 'python'
+        }
+    )
+    jest_app.start()
 
-    run_jest(HERE)
+
+if __name__ == '__main__':
+    run(HERE)

+ 164 - 2
tests/src/session.spec.ts

@@ -5,7 +5,15 @@ import { expect } from 'chai';
 
 import { ClientSession, IClientSession } from '@jupyterlab/apputils';
 
-import { createClientSession } from '@jupyterlab/testutils';
+import { createClientSession, sleep } from '@jupyterlab/testutils';
+
+import { find } from '@phosphor/algorithm';
+
+import { PromiseDelegate } from '@phosphor/coreutils';
+
+import { DebugProtocol } from 'vscode-debugprotocol';
+
+import { IDebugger } from '../../lib/tokens';
 
 import { DebugSession } from '../../lib/session';
 
@@ -71,7 +79,7 @@ describe('DebugSession', () => {
       expect(reply.body.sourcePath).to.contain('.py');
     });
 
-    it('should handle replies with success false', async () => {
+    it.skip('should handle replies with success false', async () => {
       const reply = await debugSession.sendRequest('evaluate', {
         expression: 'a'
       });
@@ -81,3 +89,157 @@ describe('DebugSession', () => {
     });
   });
 });
+
+describe('protocol', () => {
+  const code = [
+    'i = 0',
+    'i += 1',
+    'i += 1',
+    'j = i**2',
+    'j += 1',
+    'print(i, j)'
+  ].join('\n');
+
+  const breakpoints: DebugProtocol.SourceBreakpoint[] = [
+    { line: 3 },
+    { line: 5 }
+  ];
+
+  let client: IClientSession;
+  let debugSession: DebugSession;
+  let threadId: number = 1;
+
+  beforeEach(async () => {
+    client = await createClientSession({
+      kernelPreference: {
+        name: 'xpython'
+      }
+    });
+    await (client as ClientSession).initialize();
+    await client.kernel.ready;
+    debugSession = new DebugSession({ client });
+    await debugSession.start();
+
+    debugSession.eventMessage.connect(
+      (sender: DebugSession, event: IDebugger.ISession.Event) => {
+        const eventName = event.event;
+        if (eventName === 'thread') {
+          const msg = event as DebugProtocol.ThreadEvent;
+          threadId = msg.body.threadId;
+        }
+      }
+    );
+
+    const reply = await debugSession.sendRequest('updateCell', {
+      cellId: 0,
+      nextId: 1,
+      code
+    });
+    await debugSession.sendRequest('setBreakpoints', {
+      breakpoints,
+      source: { path: reply.body.sourcePath },
+      sourceModified: false
+    });
+    await debugSession.sendRequest('configurationDone', {});
+
+    // trigger an execute_request
+    client.kernel.requestExecute({ code });
+
+    // TODO: handle events instead
+    await sleep(2000);
+  });
+
+  afterEach(async () => {
+    await debugSession.stop();
+    debugSession.dispose();
+    await client.shutdown();
+    client.dispose();
+  });
+
+  describe('#stackTrace', () => {
+    it('should return the correct stackframes', async () => {
+      const reply = await debugSession.sendRequest('stackTrace', {
+        threadId
+      });
+      expect(reply.success).to.be.true;
+      const stackFrames = reply.body.stackFrames;
+      expect(stackFrames.length).to.equal(2);
+      const frame = stackFrames[0];
+      // first breakpoint
+      expect(frame.line).to.equal(3);
+    });
+  });
+
+  describe('#scopes', () => {
+    it('should return the correct scopes', async () => {
+      const stackFramesReply = await debugSession.sendRequest('stackTrace', {
+        threadId
+      });
+      const frameId = stackFramesReply.body.stackFrames[0].id;
+      const scopesReply = await debugSession.sendRequest('scopes', {
+        frameId
+      });
+      const scopes = scopesReply.body.scopes;
+      expect(scopes.length).to.equal(1);
+      expect(scopes[0].name).to.equal('Locals');
+    });
+  });
+
+  const getVariables = async () => {
+    const stackFramesReply = await debugSession.sendRequest('stackTrace', {
+      threadId
+    });
+    const frameId = stackFramesReply.body.stackFrames[0].id;
+    const scopesReply = await debugSession.sendRequest('scopes', {
+      frameId
+    });
+    const scopes = scopesReply.body.scopes;
+    const variablesReference = scopes[0].variablesReference;
+    const variablesReply = await debugSession.sendRequest('variables', {
+      variablesReference
+    });
+    return variablesReply.body.variables;
+  };
+
+  describe('#variables', () => {
+    it('should return the variables and their values', async () => {
+      const variables = await getVariables();
+      expect(variables.length).to.be.greaterThan(0);
+      const i = find(variables, variable => variable.name === 'i');
+      expect(i).to.exist;
+      expect(i.type).to.equal('int');
+      expect(i.value).to.equal('1');
+    });
+  });
+
+  describe('#continue', () => {
+    it.skip('should proceed to the next breakpoint', async () => {
+      let events: string[] = [];
+      const eventsFuture = new PromiseDelegate<string[]>();
+      debugSession.eventMessage.connect((sender, event) => {
+        events.push(event.event);
+        // aggregate the next 2 debug events
+        if (events.length === 2) {
+          eventsFuture.resolve(events);
+        }
+      });
+
+      await debugSession.sendRequest('continue', { threadId });
+
+      // wait for debug events
+      const debugEvents = await eventsFuture.promise;
+      expect(debugEvents).to.deep.equal(['continued', 'stopped']);
+
+      const variables = await getVariables();
+      const i = find(variables, variable => variable.name === 'i');
+      expect(i).to.exist;
+      expect(i.type).to.equal('int');
+      expect(i.value).to.equal('2');
+
+      const j = find(variables, variable => variable.name === 'j');
+      expect(j).to.exist;
+      expect(j.type).to.equal('int');
+      expect(j.value).to.equal('4');
+    });
+  });
+});