Переглянути джерело

Merge pull request #36 from KsavinN/debugger-ui

Connect Breakpoints widget with NoteBook
KsavinN 5 роки тому
батько
коміт
302c3f3e0e

+ 1 - 0
package.json

@@ -52,6 +52,7 @@
     "@phosphor/coreutils": "^1.3.1",
     "@phosphor/disposable": "^1.2.0",
     "@phosphor/widgets": "^1.8.0",
+    "@types/codemirror": "0.0.76",
     "vscode-debugprotocol": "1.35.0"
   },
   "devDependencies": {

+ 8 - 8
src/breakpoints/body.tsx

@@ -8,7 +8,7 @@ import { ArrayExt } from '@phosphor/algorithm';
 import { ISignal } from '@phosphor/signaling';
 
 export class Body extends ReactWidget {
-  constructor(model: Breakpoints.IModel) {
+  constructor(model: Breakpoints.Model) {
     super();
     this.model = model;
     this.addClass('jp-DebuggerBreakpoints-body');
@@ -18,14 +18,14 @@ export class Body extends ReactWidget {
     return <BreakpointsComponent model={this.model} />;
   }
 
-  readonly model: Breakpoints.IModel;
+  readonly model: Breakpoints.Model;
 }
 
-const BreakpointsComponent = ({ model }: { model: Breakpoints.IModel }) => {
+const BreakpointsComponent = ({ model }: { model: Breakpoints.Model }) => {
   const [breakpoints, setBreakpoints] = useState(model.breakpoints);
 
   model.breakpointsChanged.connect(
-    (_: Breakpoints.IModel, updates: Breakpoints.IBreakpoint[]) => {
+    (_: Breakpoints.Model, updates: Breakpoints.IBreakpoint[]) => {
       if (ArrayExt.shallowEqual(breakpoints, updates)) {
         return;
       }
@@ -35,9 +35,9 @@ const BreakpointsComponent = ({ model }: { model: Breakpoints.IModel }) => {
 
   return (
     <div>
-      {breakpoints.map((breakpoint: any) => (
+      {breakpoints.map((breakpoint: Breakpoints.IBreakpoint) => (
         <BreakpointComponent
-          key={breakpoint.id}
+          key={breakpoint.line}
           breakpoint={breakpoint}
           breakpointChanged={model.breakpointChanged}
         />
@@ -51,13 +51,13 @@ const BreakpointComponent = ({
   breakpointChanged
 }: {
   breakpoint: Breakpoints.IBreakpoint;
-  breakpointChanged: ISignal<Breakpoints.IModel, Breakpoints.IBreakpoint>;
+  breakpointChanged: ISignal<Breakpoints.Model, Breakpoints.IBreakpoint>;
 }) => {
   const [active, setActive] = useState(breakpoint.active);
   breakpoint.active = active;
 
   breakpointChanged.connect(
-    (_: Breakpoints.IModel, updates: Breakpoints.IBreakpoint) => {
+    (_: Breakpoints.Model, updates: Breakpoints.IBreakpoint) => {
       setActive(updates.active);
     }
   );

+ 78 - 45
src/breakpoints/index.ts

@@ -6,13 +6,15 @@ 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';
+import { Signal } from '@phosphor/signaling';
+import { LineInfo } from '../handlers/cell';
+
+// import { BreakpointsService } from '../breakpointsService';
 
 export class Breakpoints extends Panel {
-  constructor(options: Breakpoints.IOptions = {}) {
+  constructor(options: Breakpoints.IOptions) {
     super();
-
-    this.model = new Breakpoints.IModel(MOCK_BREAKPOINTS);
+    this.model = new Breakpoints.Model([]);
     this.addClass('jp-DebuggerBreakpoints');
     this.title.label = 'Breakpoints';
 
@@ -42,7 +44,7 @@ export class Breakpoints extends Panel {
       new ToolbarButton({
         iconClassName: 'jp-CloseAllIcon',
         onClick: () => {
-          this.model.breakpoints = [];
+          this.model.clearSelectedBreakpoints();
         },
         tooltip: 'Remove All Breakpoints'
       })
@@ -51,7 +53,7 @@ export class Breakpoints extends Panel {
 
   private isAllActive = true;
   readonly body: Widget;
-  readonly model: Breakpoints.IModel;
+  readonly model: Breakpoints.Model;
 }
 
 class BreakpointsHeader extends Widget {
@@ -75,44 +77,94 @@ export namespace Breakpoints {
     active: boolean;
   }
 
-  /**
-   * The breakpoints UI model.
-   */
-  export interface IModel {}
-
-  export class IModel implements IModel {
+  export class Model {
     constructor(model: IBreakpoint[]) {
-      this._state = model;
-    }
-
-    get breakpointsChanged(): ISignal<this, IBreakpoint[]> {
-      return this._breakpointsChanged;
+      this._breakpoints = model;
     }
 
     get breakpoints(): IBreakpoint[] {
-      return this._state;
+      return this._breakpoints;
     }
 
-    get breakpointChanged(): ISignal<this, IBreakpoint> {
+    get breakpointChanged(): Signal<this, IBreakpoint> {
       return this._breakpointChanged;
     }
 
     set breakpoints(breakpoints: IBreakpoint[]) {
-      this._state = breakpoints;
-      this._breakpointsChanged.emit(this._state);
+      this._breakpoints = [...breakpoints];
+      this.breakpointsChanged.emit(this._breakpoints);
     }
 
     set breakpoint(breakpoint: IBreakpoint) {
-      const index = this._state.findIndex(ele => ele.id === breakpoint.id);
+      const index = this._breakpoints.findIndex(
+        ele => ele.line === breakpoint.line
+      );
       if (index !== -1) {
-        this._state[index] = breakpoint;
+        this._breakpoints[index] = breakpoint;
         this._breakpointChanged.emit(breakpoint);
+      } else {
+        this.breakpoints = [...this.breakpoints, breakpoint];
+      }
+    }
+
+    addBreakpoint(session: string, type: string, lineInfo: LineInfo) {
+      const breakpoint: Breakpoints.IBreakpoint = {
+        line: lineInfo.line + 1,
+        active: true,
+        verified: true,
+        source: {
+          name: session
+        }
+      };
+      this.breakpoints = [...this._breakpoints, breakpoint];
+    }
+
+    set type(newType: SessionTypes) {
+      if (newType === this.selectedType) {
+        return;
       }
+      this.state[this.selectedType] = this.breakpoints;
+      this.selectedType = newType;
+      this.breakpoints = this.state[newType];
+    }
+
+    removeBreakpoint(lineInfo: any) {
+      const breakpoints = this.breakpoints.filter(
+        ele => ele.line !== lineInfo.line + 1
+      );
+      this.breakpoints = breakpoints;
+    }
+
+    clearSelectedBreakpoints() {
+      this.breakpoints = [];
+      this.clearedBreakpoints.emit(this.selectedType);
     }
 
-    private _state: IBreakpoint[];
-    private _breakpointsChanged = new Signal<this, IBreakpoint[]>(this);
+    changeLines(linesInfo: LineInfo[]) {
+      if (!linesInfo && this.breakpoints.length === 0) {
+        return;
+      }
+      if (linesInfo.length === 0) {
+        this.breakpoints = [];
+      } else {
+        const breakpoint = { ...this.breakpoints[0] };
+        let breakpoints: Breakpoints.IBreakpoint[] = [];
+        linesInfo.forEach(ele => {
+          breakpoints.push({ ...breakpoint, line: ele.line + 1 });
+        });
+        this.breakpoints = [...breakpoints];
+      }
+    }
+
+    private _breakpoints: IBreakpoint[];
+    breakpointsChanged = new Signal<this, IBreakpoint[]>(this);
+    clearedBreakpoints = new Signal<this, SessionTypes | null>(this);
+    private selectedType: SessionTypes;
     private _breakpointChanged = new Signal<this, IBreakpoint>(this);
+    private state = {
+      console: [] as Breakpoints.IBreakpoint[],
+      notebook: [] as Breakpoints.IBreakpoint[]
+    };
   }
 
   /**
@@ -121,23 +173,4 @@ export namespace 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
-  }
-];
+export type SessionTypes = 'console' | 'notebook';

+ 53 - 14
src/debugger.ts

@@ -3,33 +3,35 @@
 
 import { IDataConnector } from '@jupyterlab/coreutils';
 
-import { BoxPanel, TabPanel } from '@phosphor/widgets';
+import { ReadonlyJSONValue } from '@phosphor/coreutils';
 
-import { ReadonlyJSONValue, UUID } from '@phosphor/coreutils';
+import { INotebookTracker } from '@jupyterlab/notebook';
+
+import { IClientSession } from '@jupyterlab/apputils';
 
 import { IDisposable } from '@phosphor/disposable';
 
-import { DebuggerSidebar } from './sidebar';
+import { ISignal, Signal } from '@phosphor/signaling';
 
-export class Debugger extends BoxPanel {
-  readonly model: Debugger.Model;
+import { BoxPanel } from '@phosphor/widgets';
 
-  readonly tabs = new TabPanel();
+import { DebugSession } from './session';
 
-  readonly sidebar: DebuggerSidebar;
+import { IDebuggerSidebar } from './tokens';
 
+export class Debugger extends BoxPanel {
   constructor(options: Debugger.IOptions) {
     super({ direction: 'left-to-right' });
-
     this.model = new Debugger.Model(options);
-    this.sidebar = new DebuggerSidebar(this.model);
-    this.title.label = 'Debugger';
+    // this.sidebar = new DebuggerSidebar(this.model);
+    this.title.label = 'Debugger-' + options.id;
 
     this.addClass('jp-Debugger');
-    this.addWidget(this.tabs);
-    this.addWidget(this.sidebar);
+    // this.addWidget(this.sidebar);
   }
 
+  readonly model: Debugger.Model;
+
   dispose(): void {
     if (this.isDisposed) {
       return;
@@ -45,14 +47,16 @@ export class Debugger extends BoxPanel {
 export namespace Debugger {
   export interface IOptions {
     connector?: IDataConnector<ReadonlyJSONValue>;
-
     id?: string;
+    session?: IClientSession;
+    sidebar?: IDebuggerSidebar;
   }
 
   export class Model implements IDisposable {
     constructor(options: Debugger.Model.IOptions) {
       this.connector = options.connector || null;
-      this.id = options.id || UUID.uuid4();
+      this.session = new DebugSession({ client: options.session });
+      this.id = options.id;
       void this._populate();
     }
 
@@ -60,10 +64,41 @@ export namespace Debugger {
 
     readonly id: string;
 
+    get sidebar() {
+      return this._sidebar;
+    }
+
+    set sidebar(newSidebar: IDebuggerSidebar) {
+      this._sidebar = newSidebar;
+    }
+
+    get session() {
+      return this._session;
+    }
+
+    set session(session: DebugSession | null) {
+      if (this._session === session) {
+        return;
+      }
+      if (this._session) {
+        this._session.dispose();
+      }
+      this._session = session;
+      this._sessionChanged.emit(undefined);
+    }
+
+    get sessionChanged(): ISignal<this, void> {
+      return this._sessionChanged;
+    }
+
     get isDisposed(): boolean {
       return this._isDisposed;
     }
 
+    get notebookTracker() {
+      return this._notebook;
+    }
+
     dispose(): void {
       this._isDisposed = true;
     }
@@ -77,6 +112,10 @@ export namespace Debugger {
     }
 
     private _isDisposed = false;
+    private _notebook: INotebookTracker;
+    private _session: DebugSession | null;
+    private _sessionChanged = new Signal<this, void>(this);
+    private _sidebar: IDebuggerSidebar;
   }
 
   export namespace Model {

+ 180 - 0
src/handlers/cell.ts

@@ -0,0 +1,180 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import { CodeCell } from '@jupyterlab/cells';
+
+import { CodeMirrorEditor } from '@jupyterlab/codemirror';
+
+import { Editor, Doc } from 'codemirror';
+
+import { Breakpoints, SessionTypes } from '../breakpoints';
+import { Debugger } from '../debugger';
+
+export class CellManager {
+  constructor(options: CellManager.IOptions) {
+    this._debuggerModel = options.debuggerModel;
+    this.breakpointsModel = options.breakpointsModel;
+    this.activeCell = options.activeCell;
+    this._type = options.type;
+    this.onActiveCellChanged();
+
+    this.breakpointsModel.clearedBreakpoints.connect((_, type) => {
+      if (type !== this._type) {
+        return;
+      }
+      this.clearGutter(this.activeCell);
+    });
+  }
+
+  private _previousCell: CodeCell;
+  private previousLineCount: number;
+  private _debuggerModel: Debugger.Model;
+  private _type: SessionTypes;
+  private breakpointsModel: Breakpoints.Model;
+  private _activeCell: CodeCell;
+
+  set previousCell(cell: CodeCell) {
+    this._previousCell = cell;
+  }
+
+  get previousCell() {
+    return this._previousCell;
+  }
+
+  set activeCell(cell: CodeCell) {
+    this._activeCell = cell;
+    this.onActiveCellChanged();
+  }
+
+  get activeCell(): CodeCell {
+    return this._activeCell;
+  }
+
+  protected clearGutter(cell: CodeCell) {
+    const editor = cell.editor as CodeMirrorEditor;
+    editor.doc.eachLine(line => {
+      if ((line as LineInfo).gutterMarkers) {
+        editor.editor.setGutterMarker(line, 'breakpoints', null);
+      }
+    });
+  }
+
+  onActiveCellChanged() {
+    if (
+      this.activeCell &&
+      this.activeCell.editor &&
+      this._debuggerModel &&
+      this._debuggerModel.session
+    ) {
+      if (this.previousCell && !this.previousCell.isDisposed) {
+        this.removeListner(this.previousCell);
+        this.clearGutter(this.previousCell);
+        this.breakpointsModel.breakpoints = [];
+      }
+      this.previousCell = this.activeCell;
+      this.setEditor(this.activeCell);
+    }
+  }
+
+  protected setEditor(cell: CodeCell) {
+    if (!cell || !cell.editor) {
+      return;
+    }
+
+    const editor = cell.editor as CodeMirrorEditor;
+
+    this.previousLineCount = editor.lineCount;
+
+    editor.setOption('lineNumbers', true);
+    editor.editor.setOption('gutters', [
+      'CodeMirror-linenumbers',
+      'breakpoints'
+    ]);
+
+    editor.editor.on('gutterClick', this.onGutterClick);
+    editor.editor.on('renderLine', this.onNewRenderLine);
+  }
+
+  protected removeListner(cell: CodeCell) {
+    const editor = cell.editor as CodeMirrorEditor;
+    editor.setOption('lineNumbers', false);
+    editor.editor.off('gutterClick', this.onGutterClick);
+    editor.editor.off('renderLine', this.onNewRenderLine);
+  }
+
+  protected getEditorId(): string {
+    return this.activeCell.editor.uuid;
+  }
+
+  protected onGutterClick = (editor: Editor, lineNumber: number) => {
+    const info = editor.lineInfo(lineNumber);
+    if (!info) {
+      return;
+    }
+
+    const isRemoveGutter = !!info.gutterMarkers;
+    if (isRemoveGutter) {
+      this.breakpointsModel.removeBreakpoint(info as LineInfo);
+    } else {
+      this.breakpointsModel.addBreakpoint(
+        this._debuggerModel.session.id,
+        this.getEditorId(),
+        info as LineInfo
+      );
+    }
+
+    editor.setGutterMarker(
+      lineNumber,
+      'breakpoints',
+      isRemoveGutter ? null : this.createMarkerNode()
+    );
+  };
+
+  protected onNewRenderLine = (editor: Editor, line: any) => {
+    const lineInfo = editor.lineInfo(line);
+    if (lineInfo.handle && lineInfo.handle.order === false) {
+      return;
+    }
+    const doc: Doc = editor.getDoc();
+    const linesNumber = doc.lineCount();
+    if (this.previousLineCount !== linesNumber) {
+      var lines: LineInfo[] = [];
+      doc.eachLine(line => {
+        if ((line as LineInfo).gutterMarkers) {
+          lines.push(editor.lineInfo(line));
+        }
+      });
+      this.breakpointsModel.changeLines(lines);
+      this.previousLineCount = linesNumber;
+    }
+  };
+
+  private createMarkerNode() {
+    var marker = document.createElement('div');
+    marker.className = 'jp-breakpoint-marker';
+    marker.innerHTML = '●';
+    return marker;
+  }
+}
+
+export namespace CellManager {
+  export interface IOptions {
+    debuggerModel: Debugger.Model;
+    breakpointsModel: Breakpoints.Model;
+    activeCell?: CodeCell;
+    type: SessionTypes;
+  }
+}
+
+export interface LineInfo {
+  line: any;
+  handle: any;
+  text: string;
+  /** Object mapping gutter IDs to marker elements. */
+  gutterMarkers: any;
+  textClass: string;
+  bgClass: string;
+  wrapClass: string;
+  /** Array of line widgets attached to this line. */
+  widgets: any;
+}

+ 118 - 0
src/handlers/console.ts

@@ -0,0 +1,118 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import {
+  IConsoleTracker,
+  ConsolePanel,
+  CodeConsole
+} from '@jupyterlab/console';
+
+import { DebugSession } from '../session';
+
+import { IClientSession, WidgetTracker } from '@jupyterlab/apputils';
+
+import { CellManager } from '../handlers/cell';
+
+import { CodeCell } from '@jupyterlab/cells';
+
+import { Breakpoints } from '../breakpoints';
+
+import { Debugger } from '../debugger';
+
+export class DebuggerConsoleHandler {
+  constructor(options: DebuggerNotebookHandler.IOptions) {
+    this.debuggerModel = options.debuggerModel;
+    this.consoleTracker = options.consoleTracker;
+    this.breakpoints = this.debuggerModel.sidebar.breakpoints.model;
+    this.consoleTracker.currentChanged.connect(
+      (sender: WidgetTracker<ConsolePanel>, consolePanel: ConsolePanel) => {
+        this.newDebuggerSession(consolePanel);
+      }
+    );
+    this.debuggerModel.sessionChanged.connect(() => {
+      const consolePanel: ConsolePanel = this.consoleTracker.currentWidget;
+      if (
+        consolePanel &&
+        consolePanel.session.name === this.debuggerModel.session.id
+      ) {
+        if (!this.cellManager) {
+          consolePanel.console.promptCellCreated.connect(
+            this.promptCellCreated,
+            this
+          );
+          this.previousConsole = consolePanel;
+          this.cellManager = new CellManager({
+            breakpointsModel: this.breakpoints,
+            activeCell: consolePanel.console.promptCell as CodeCell,
+            debuggerModel: this.debuggerModel,
+            type: 'notebook'
+          });
+        }
+      }
+    });
+  }
+
+  private consoleTracker: IConsoleTracker;
+  private debuggerModel: Debugger.Model;
+  private breakpoints: Breakpoints.Model;
+  private cellManager: CellManager;
+  private previousConsole: ConsolePanel;
+
+  protected newDebuggerSession(consolePanel: ConsolePanel) {
+    const session = consolePanel ? consolePanel.session : null;
+    if (this.debuggerModel.session && session) {
+      const newSession = new DebugSession({
+        client: session as IClientSession
+      });
+      this.debuggerModel.session = newSession;
+      if (this.cellManager && this.previousConsole) {
+        this.previousConsole.console.promptCellCreated.disconnect(
+          this.promptCellCreated,
+          this
+        );
+        if (consolePanel.console.promptCell) {
+          this.cellManager.previousCell = this.cellManager.activeCell;
+          this.cellManager.activeCell = consolePanel.console.promptCell;
+        }
+      }
+    } else if (session) {
+      const newSession = new DebugSession({
+        client: session as IClientSession
+      });
+      this.debuggerModel.session = newSession;
+    }
+
+    if (session) {
+      consolePanel.console.promptCellCreated.connect(
+        this.promptCellCreated,
+        this
+      );
+      this.previousConsole = consolePanel;
+    }
+  }
+
+  protected promptCellCreated(sender: CodeConsole, update: CodeCell) {
+    if (
+      this.cellManager &&
+      this.debuggerModel.session.id ===
+        (this.consoleTracker.currentWidget.session as IClientSession).name
+    ) {
+      this.cellManager.previousCell = this.cellManager.activeCell;
+      this.cellManager.activeCell = update;
+    } else if (!this.cellManager) {
+      this.cellManager = new CellManager({
+        activeCell: update,
+        breakpointsModel: this.breakpoints,
+        debuggerModel: this.debuggerModel,
+        type: 'console'
+      });
+    }
+  }
+}
+
+export namespace DebuggerNotebookHandler {
+  export interface IOptions {
+    debuggerModel: Debugger.Model;
+    consoleTracker: IConsoleTracker;
+  }
+}

+ 94 - 0
src/handlers/notebook.ts

@@ -0,0 +1,94 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import {
+  INotebookTracker,
+  NotebookPanel,
+  NotebookTracker
+} from '@jupyterlab/notebook';
+
+import { DebugSession } from './../session';
+
+import { IClientSession } from '@jupyterlab/apputils';
+
+import { CodeCell } from '@jupyterlab/cells';
+
+import { CellManager } from './cell';
+
+import { Debugger } from '../debugger';
+
+import { Breakpoints } from '../breakpoints';
+
+export class DebuggerNotebookHandler {
+  constructor(options: DebuggerNotebookHandler.IOptions) {
+    this.debuggerModel = options.debuggerModel;
+    this.notebookTracker = options.notebookTracker;
+    this.breakpoints = this.debuggerModel.sidebar.breakpoints.model;
+    this.notebookTracker.currentChanged.connect(
+      (sender, notebookPanel: NotebookPanel) => {
+        const session = notebookPanel ? notebookPanel.session : null;
+        this.newDebuggerSession(session, sender);
+      }
+    );
+    this.debuggerModel.sessionChanged.connect(() => {
+      const notebookPanel: NotebookPanel = this.notebookTracker.currentWidget;
+      if (
+        notebookPanel &&
+        notebookPanel.session.name === this.debuggerModel.session.id
+      ) {
+        this.notebookTracker.activeCellChanged.connect(this.onNewCell, this);
+        if (!this.cellManager) {
+          this.cellManager = new CellManager({
+            breakpointsModel: this.breakpoints,
+            activeCell: this.notebookTracker.activeCell as CodeCell,
+            debuggerModel: this.debuggerModel,
+            type: 'notebook'
+          });
+        }
+      }
+    });
+  }
+
+  private notebookTracker: INotebookTracker;
+  private debuggerModel: Debugger.Model;
+  private breakpoints: Breakpoints.Model;
+  private cellManager: CellManager;
+
+  protected onNewCell(noteTracker: NotebookTracker, codeCell: CodeCell) {
+    setTimeout(() => {
+      if (this.cellManager) {
+        this.cellManager.activeCell = codeCell;
+        this.cellManager.onActiveCellChanged();
+      } else {
+        this.cellManager = new CellManager({
+          breakpointsModel: this.breakpoints,
+          activeCell: codeCell,
+          debuggerModel: this.debuggerModel,
+          type: 'notebook'
+        });
+      }
+    });
+  }
+
+  protected newDebuggerSession(
+    client: IClientSession | null,
+    note: INotebookTracker
+  ) {
+    if (this.debuggerModel.session) {
+      note.activeCellChanged.disconnect(this.onNewCell, this);
+    }
+    if (client) {
+      const newSession = new DebugSession({
+        client: client as IClientSession
+      });
+      this.debuggerModel.session = newSession;
+    }
+  }
+}
+
+export namespace DebuggerNotebookHandler {
+  export interface IOptions {
+    debuggerModel: Debugger.Model;
+    notebookTracker: INotebookTracker;
+  }
+}

+ 119 - 41
src/index.ts

@@ -4,24 +4,34 @@
 import {
   ILayoutRestorer,
   JupyterFrontEnd,
-  JupyterFrontEndPlugin
+  JupyterFrontEndPlugin,
+  LabShell
 } from '@jupyterlab/application';
 
+import { ICommandPalette } from '@jupyterlab/apputils';
+
 import { WidgetTracker, MainAreaWidget } from '@jupyterlab/apputils';
 
-import { IConsoleTracker } from '@jupyterlab/console';
+import { IConsoleTracker, ConsolePanel } from '@jupyterlab/console';
 
 import { IStateDB } from '@jupyterlab/coreutils';
 
 import { IEditorTracker } from '@jupyterlab/fileeditor';
 
-import { INotebookTracker } from '@jupyterlab/notebook';
+import { INotebookTracker, NotebookPanel } from '@jupyterlab/notebook';
 
 import { Debugger } from './debugger';
 
+import { IDebugger, IDebuggerSidebar } from './tokens';
+
+import { DebuggerNotebookHandler } from './handlers/notebook';
+
+import { DebuggerConsoleHandler } from './handlers/console';
+
 import { DebuggerSidebar } from './sidebar';
 
-import { IDebugger, IDebuggerSidebar } from './tokens';
+import { SessionTypes } from './breakpoints';
+import { DebugSession } from './session';
 
 /**
  * The command IDs used by the debugger plugin.
@@ -42,14 +52,22 @@ export namespace CommandIDs {
 const consoles: JupyterFrontEndPlugin<void> = {
   id: '@jupyterlab/debugger:consoles',
   autoStart: true,
-  requires: [IDebugger],
-  optional: [IConsoleTracker],
-  activate: (_, debug, tracker: IConsoleTracker | null) => {
-    if (!tracker) {
-      console.log(`${consoles.id} load failed. There is no console tracker.`);
-      return;
-    }
-    console.log(`${consoles.id} has not been implemented.`, debug);
+  requires: [IDebugger, IDebuggerSidebar, IConsoleTracker],
+  activate: (
+    _,
+    debug: IDebugger,
+    sidebar: IDebuggerSidebar,
+    tracker: IConsoleTracker
+  ) => {
+    debug.currentChanged.connect((_, update) => {
+      if (update) {
+        update.content.model.sidebar = sidebar;
+        new DebuggerConsoleHandler({
+          debuggerModel: update.content.model,
+          consoleTracker: tracker
+        });
+      }
+    });
   }
 };
 
@@ -59,8 +77,27 @@ const consoles: JupyterFrontEndPlugin<void> = {
 const files: JupyterFrontEndPlugin<void> = {
   id: '@jupyterlab/debugger:files',
   autoStart: true,
-  optional: [IEditorTracker],
-  activate: (app: JupyterFrontEnd, tracker: IEditorTracker | null) => {
+  requires: [IEditorTracker, IDebugger, INotebookTracker],
+  activate: (
+    app: JupyterFrontEnd,
+    tracker: IEditorTracker | null,
+    debug: IDebugger,
+    notebook: INotebookTracker
+  ) => {
+    const shell = app.shell;
+    (shell as LabShell).currentChanged.connect((sender, update) => {
+      const newWidget = update.newValue;
+      const session =
+        newWidget && (newWidget as NotebookPanel | ConsolePanel).session
+          ? (newWidget as NotebookPanel | ConsolePanel).session
+          : false;
+      if (session && debug.currentWidget) {
+        const debugModel: Debugger.Model = debug.currentWidget.content.model;
+        debugModel.session = new DebugSession({ client: session });
+        debugModel.sidebar.breakpoints.model.type = session.type as SessionTypes;
+      }
+    });
+
     app.commands.addCommand(CommandIDs.debugFile, {
       execute: async _ => {
         if (!tracker || !tracker.currentWidget) {
@@ -68,8 +105,8 @@ const files: JupyterFrontEndPlugin<void> = {
         }
         if (tracker.currentWidget) {
           // TODO: Find if the file is backed by a kernel or attach it to one.
-          const widget = await app.commands.execute(CommandIDs.create);
-          app.shell.add(widget, 'main');
+          // const widget = await app.commands.execute(CommandIDs.create);
+          // app.shell.add(widget, 'main');
         }
       }
     });
@@ -82,14 +119,43 @@ const files: JupyterFrontEndPlugin<void> = {
 const notebooks: JupyterFrontEndPlugin<void> = {
   id: '@jupyterlab/debugger:notebooks',
   autoStart: true,
-  requires: [IDebugger],
-  optional: [INotebookTracker],
-  activate: (_, debug, tracker: INotebookTracker | null) => {
-    if (!tracker) {
-      console.log(`${notebooks.id} load failed. There is no notebook tracker.`);
-      return;
-    }
-    console.log(`${notebooks.id} has not been implemented.`, debug);
+  requires: [IDebugger, IDebuggerSidebar],
+  optional: [INotebookTracker, ICommandPalette],
+  activate: (
+    app: JupyterFrontEnd,
+    debug: IDebugger,
+    sidebar: IDebuggerSidebar,
+    notebook: INotebookTracker,
+    palette: ICommandPalette
+  ) => {
+    // 1. Keep track of any new notebook that is created.
+    // 2. When the *active* notebook changes, hook it up to the debugger.
+    // 3. If a notebook is closed, dispose the debugger session.
+
+    debug.currentChanged.connect((_, update) => {
+      if (update) {
+        update.content.model.sidebar = sidebar;
+        new DebuggerNotebookHandler({
+          debuggerModel: update.content.model,
+          notebookTracker: notebook
+        });
+      }
+    });
+
+    //   // Debugger model:
+    //   // LIST of editors that it currently cares about.
+    //   // Manages life cycle signal connections.
+    //   // Manages variables
+    // });
+
+    // this exist only for my test in futre will be removed
+    const command: string = CommandIDs.debugNotebook;
+    app.commands.addCommand(command, {
+      label: 'test',
+      execute: () => {}
+    });
+
+    palette.addItem({ command, category: 'dev test' });
   }
 };
 
@@ -99,6 +165,7 @@ const notebooks: JupyterFrontEndPlugin<void> = {
 const sidebar: JupyterFrontEndPlugin<IDebuggerSidebar> = {
   id: '@jupyterlab/debugger:sidebar',
   optional: [ILayoutRestorer],
+  provides: IDebuggerSidebar,
   autoStart: true,
   activate: (
     app: JupyterFrontEnd,
@@ -108,11 +175,9 @@ const sidebar: JupyterFrontEndPlugin<IDebuggerSidebar> = {
     const label = 'Environment';
     const namespace = 'jp-debugger-sidebar';
     const sidebar = new DebuggerSidebar(null);
-
     sidebar.id = namespace;
     sidebar.title.label = label;
     shell.add(sidebar, 'right', { activate: false });
-
     if (restorer) {
       restorer.add(sidebar, sidebar.id);
     }
@@ -126,7 +191,7 @@ const sidebar: JupyterFrontEndPlugin<IDebuggerSidebar> = {
  */
 const tracker: JupyterFrontEndPlugin<IDebugger> = {
   id: '@jupyterlab/debugger:tracker',
-  optional: [ILayoutRestorer, IDebuggerSidebar],
+  optional: [ILayoutRestorer, IDebuggerSidebar, ICommandPalette],
   requires: [IStateDB],
   provides: IDebugger,
   autoStart: true,
@@ -134,49 +199,61 @@ const tracker: JupyterFrontEndPlugin<IDebugger> = {
     app: JupyterFrontEnd,
     state: IStateDB,
     restorer: ILayoutRestorer | null,
-    sidebar: IDebuggerSidebar | null
+    sidebar: IDebuggerSidebar | null,
+    palette: ICommandPalette
   ): IDebugger => {
     const tracker = new WidgetTracker<MainAreaWidget<Debugger>>({
       namespace: 'debugger'
     });
 
-    app.commands.addCommand(CommandIDs.create, {
+    tracker.widgetUpdated.connect((_, update) => {
+      update;
+    });
+
+    const command = CommandIDs.create;
+
+    app.commands.addCommand(command, {
+      label: 'Debugger',
       execute: args => {
         const id = (args.id as string) || '';
-
         if (id) {
           console.log('Debugger ID: ', id);
         }
 
-        if (tracker.find(widget => id === widget.content.model.id)) {
+        const existedWidget = tracker.find(
+          widget => id === widget.content.model.id
+        );
+
+        if (existedWidget) {
+          app.shell.add(existedWidget, 'main');
           return;
         }
 
         const widget = new MainAreaWidget({
-          content: new Debugger({ connector: state, id })
+          content: new Debugger({
+            connector: state,
+            id: id
+          })
         });
 
         void tracker.add(widget);
+        app.shell.add(widget, 'main');
 
         return widget;
       }
     });
 
+    palette.addItem({ command, category: 'Debugger' });
+
     if (restorer) {
       // Handle state restoration.
       void restorer.restore(tracker, {
-        command: CommandIDs.create,
+        command: command,
         args: widget => ({ id: widget.content.model.id }),
         name: widget => widget.content.model.id
       });
     }
 
-    if (sidebar) {
-      tracker.currentChanged.connect((_, current) => {
-        sidebar.model = current ? current.content.model : null;
-      });
-    }
-
     return tracker;
   }
 };
@@ -184,6 +261,7 @@ const tracker: JupyterFrontEndPlugin<IDebugger> = {
 /**
  * Export the plugins as default.
  */
+
 const plugins: JupyterFrontEndPlugin<any>[] = [
   consoles,
   files,
@@ -191,5 +269,5 @@ const plugins: JupyterFrontEndPlugin<any>[] = [
   sidebar,
   tracker
 ];
-const plugin: JupyterFrontEndPlugin<any> = plugins[3];
-export default plugin;
+
+export default plugins;

+ 29 - 2
src/session.ts

@@ -21,13 +21,40 @@ export class DebugSession implements IDebugger.ISession {
    */
   constructor(options: DebugSession.IOptions) {
     this.client = options.client;
-    this.client.iopubMessage.connect(this._handleEvent, this);
   }
 
   /**
    * The client session to connect to a debugger.
    */
-  client: IClientSession;
+  private _client: IClientSession;
+
+  get id() {
+    return this.client.name;
+  }
+
+  get type() {
+    return this.client.type;
+  }
+
+  get client(): IClientSession {
+    return this._client;
+  }
+
+  set client(client: IClientSession | null) {
+    if (this._client === client) {
+      return;
+    }
+
+    if (this._client) {
+      Signal.clearData(this._client);
+    }
+
+    this._client = client;
+
+    if (client) {
+      this._client.iopubMessage.connect(this._handleEvent, this);
+    }
+  }
 
   /**
    * The code editors in a debugger session.

+ 1 - 1
src/sidebar.ts

@@ -20,7 +20,7 @@ export class DebuggerSidebar extends SplitPanel {
 
     this.variables = new Variables();
     this.callstack = new Callstack();
-    this.breakpoints = new Breakpoints();
+    this.breakpoints = new Breakpoints({});
 
     this.addWidget(this.variables);
     this.addWidget(this.callstack);

+ 7 - 0
style/breakpoints.css

@@ -6,3 +6,10 @@
 .jp-DebuggerBreakpoints-body {
   padding: 10px;
 }
+
+.jp-breakpoint-marker {
+  position: absolute;
+  left: -35px;
+  top: -1px;
+  color: #f22;
+}