瀏覽代碼

Merge pull request #15 from jupyterlab/master

merge iteration
KsavinN 5 年之前
父節點
當前提交
5a458ccc04
共有 13 個文件被更改,包括 312 次插入48 次删除
  1. 5 0
      .prettierignore
  2. 2 0
      README.md
  3. 1 1
      package.json
  4. 29 11
      src/breakpoints/body.tsx
  5. 14 3
      src/debugger.ts
  6. 124 0
      src/editors/index.ts
  7. 10 2
      src/index.ts
  8. 29 20
      src/session.ts
  9. 72 0
      style/editors.css
  10. 2 1
      style/index.css
  11. 2 0
      style/variables.css
  12. 6 1
      tests/src/debugger.spec.ts
  13. 16 9
      tests/src/session.spec.ts

+ 5 - 0
.prettierignore

@@ -0,0 +1,5 @@
+node_modules
+**/node_modules
+**/lib
+**/build
+**/static

+ 2 - 0
README.md

@@ -1,5 +1,7 @@
 # @jupyterlab/debugger
 
+[![Build Status](https://dev.azure.com/jupyterlab/jupyterlab/_apis/build/status/jupyterlab.debugger?branchName=master)](https://dev.azure.com/jupyterlab/jupyterlab/_build/latest?definitionId=3&branchName=master)
+
 A JupyterLab debugger UI extension
 
 This extension is under active development and is not yet available.

+ 1 - 1
package.json

@@ -89,7 +89,7 @@
       "jlpm run prettier",
       "git add"
     ],
-    "**/*{.ts,.tsx}": [
+    "src/**/*{.ts,.tsx}": [
       "jlpm run tslint",
       "git add"
     ]

+ 29 - 11
src/breakpoints/body.tsx

@@ -1,7 +1,7 @@
 // Copyright (c) Jupyter Development Team.
 // Distributed under the terms of the Modified BSD License.
 
-import React, { useState } from 'react';
+import React, { useEffect, useState } from 'react';
 import { Breakpoints } from '.';
 import { ReactWidget } from '@jupyterlab/apputils';
 import { ArrayExt } from '@phosphor/algorithm';
@@ -24,14 +24,23 @@ export class Body extends ReactWidget {
 const BreakpointsComponent = ({ model }: { model: Breakpoints.Model }) => {
   const [breakpoints, setBreakpoints] = useState(model.breakpoints);
 
-  model.breakpointsChanged.connect(
-    (_: Breakpoints.Model, updates: Breakpoints.IBreakpoint[]) => {
+  useEffect(() => {
+    const updateBreakpoints = (
+      _: Breakpoints.Model,
+      updates: Breakpoints.IBreakpoint[]
+    ) => {
       if (ArrayExt.shallowEqual(breakpoints, updates)) {
         return;
       }
       setBreakpoints(updates);
-    }
-  );
+    };
+
+    model.breakpointsChanged.connect(updateBreakpoints);
+
+    return () => {
+      model.breakpointsChanged.disconnect(updateBreakpoints);
+    };
+  });
 
   return (
     <div>
@@ -56,16 +65,25 @@ const BreakpointComponent = ({
   const [active, setActive] = useState(breakpoint.active);
   breakpoint.active = active;
 
-  breakpointChanged.connect(
-    (_: Breakpoints.Model, updates: Breakpoints.IBreakpoint) => {
-      setActive(updates.active);
-    }
-  );
-
   const setBreakpointEnabled = (state: boolean) => {
     setActive(state);
   };
 
+  useEffect(() => {
+    const updateBreakpoints = (
+      _: Breakpoints.Model,
+      updates: Breakpoints.IBreakpoint
+    ) => {
+      setBreakpointEnabled(updates.active);
+    };
+
+    breakpointChanged.connect(updateBreakpoints);
+
+    return () => {
+      breakpointChanged.disconnect(updateBreakpoints);
+    };
+  });
+
   return (
     <div className={`breakpoint`}>
       <input

+ 14 - 3
src/debugger.ts

@@ -1,6 +1,8 @@
 // Copyright (c) Jupyter Development Team.
 // Distributed under the terms of the Modified BSD License.
 
+import { CodeEditor } from '@jupyterlab/codeeditor';
+
 import { IDataConnector } from '@jupyterlab/coreutils';
 
 import { ReadonlyJSONValue } from '@phosphor/coreutils';
@@ -13,23 +15,31 @@ import { ISignal, Signal } from '@phosphor/signaling';
 
 import { BoxPanel } from '@phosphor/widgets';
 
-import { IDebugger } from './tokens';
+import { DebuggerEditors } from './editors';
 
 import { DebuggerSidebar } from './sidebar';
 
+import { IDebugger } from './tokens';
+
 export class Debugger extends BoxPanel {
   constructor(options: Debugger.IOptions) {
     super({ direction: 'left-to-right' });
+    this.title.label = 'Debugger';
+    this.title.iconClass = 'jp-BugIcon';
+
     this.model = new Debugger.Model(options);
 
     this.sidebar = new DebuggerSidebar(this.model);
-
-    this.title.label = 'Debugger';
     this.model.sidebar = this.sidebar;
 
+    const { editorFactory } = options;
+    this.editors = new DebuggerEditors({ editorFactory });
+    this.addWidget(this.editors);
+
     this.addClass('jp-Debugger');
   }
 
+  readonly editors: DebuggerEditors;
   readonly model: Debugger.Model;
   readonly sidebar: DebuggerSidebar;
 
@@ -47,6 +57,7 @@ export class Debugger extends BoxPanel {
  */
 export namespace Debugger {
   export interface IOptions {
+    editorFactory: CodeEditor.Factory;
     connector?: IDataConnector<ReadonlyJSONValue>;
     id?: string;
     session?: IClientSession;

+ 124 - 0
src/editors/index.ts

@@ -0,0 +1,124 @@
+/*-----------------------------------------------------------------------------
+| Copyright (c) Jupyter Development Team.
+| Distributed under the terms of the Modified BSD License.
+|----------------------------------------------------------------------------*/
+
+import { CodeEditorWrapper, CodeEditor } from '@jupyterlab/codeeditor';
+
+import { ISignal, Signal } from '@phosphor/signaling';
+
+import { TabPanel } from '@phosphor/widgets';
+
+export class DebuggerEditors extends TabPanel {
+  constructor(options: DebuggerEditors.IOptions) {
+    super();
+
+    this.tabsMovable = true;
+
+    this.model = new DebuggerEditors.IModel();
+    this.model.editorAdded.connect((sender, data) => {
+      let editor = new CodeEditorWrapper({
+        model: new CodeEditor.Model({
+          value: data.code,
+          mimeType: data.mimeType
+        }),
+        factory: options.editorFactory,
+        config: {
+          readOnly: true,
+          lineNumbers: true
+        }
+      });
+
+      editor.title.label = data.title;
+      editor.title.closable = true;
+
+      this.addWidget(editor);
+    });
+
+    MOCK_EDITORS.forEach(editor => this.model.addEditor(editor));
+
+    this.addClass('jp-DebuggerEditors');
+  }
+
+  /**
+   * The debugger editors model.
+   */
+  model: DebuggerEditors.IModel;
+
+  /**
+   * Dispose the debug editors.
+   */
+  dispose(): void {
+    if (this.isDisposed) {
+      return;
+    }
+    Signal.clearData(this);
+  }
+}
+
+/**
+ * A namespace for `DebuggerEditors` statics.
+ */
+export namespace DebuggerEditors {
+  /**
+   * The options used to create a DebuggerEditors.
+   */
+  export interface IOptions {
+    editorFactory: CodeEditor.Factory;
+  }
+
+  /**
+   * An interface for read only editors.
+   */
+  export interface IEditor {
+    title: string;
+    code: string;
+    mimeType: string;
+  }
+
+  export interface IModel {}
+
+  export class IModel implements IModel {
+    /**
+     * A signal emitted when a new editor is added.
+     */
+    get editorAdded(): ISignal<
+      DebuggerEditors.IModel,
+      DebuggerEditors.IEditor
+    > {
+      return this._editorAdded;
+    }
+
+    /**
+     * Get all the editors currently opened.
+     */
+    get editors() {
+      return this._state;
+    }
+
+    /**
+     * Add a new editor to the editor TabPanel.
+     * @param editor The read-only editor info to add.
+     */
+    addEditor(editor: DebuggerEditors.IEditor) {
+      this._state.push(editor);
+      this._editorAdded.emit(editor);
+    }
+
+    private _state: DebuggerEditors.IEditor[] = [];
+    private _editorAdded = new Signal<this, DebuggerEditors.IEditor>(this);
+  }
+}
+
+const MOCK_EDITORS = [
+  {
+    title: 'untitled.py',
+    mimeType: 'text/x-ipython',
+    code: 'import math\nprint(math.pi)'
+  },
+  {
+    title: 'test.py',
+    mimeType: 'text/x-ipython',
+    code: 'import sys\nprint(sys.version)'
+  }
+];

+ 10 - 2
src/index.ts

@@ -33,6 +33,7 @@ import { DebuggerNotebookHandler } from './handlers/notebook';
 import { DebuggerConsoleHandler } from './handlers/console';
 
 import { Kernel } from '@jupyterlab/services';
+import { IEditorServices } from '@jupyterlab/codeeditor';
 
 /**
  * The command IDs used by the debugger plugin.
@@ -208,19 +209,23 @@ const notebooks: JupyterFrontEndPlugin<void> = {
 const main: JupyterFrontEndPlugin<IDebugger> = {
   id: '@jupyterlab/debugger:main',
   optional: [ILayoutRestorer, ICommandPalette],
-  requires: [IStateDB],
+  requires: [IStateDB, IEditorServices],
   provides: IDebugger,
   autoStart: true,
   activate: (
     app: JupyterFrontEnd,
     state: IStateDB,
+    editorServices: IEditorServices,
     restorer: ILayoutRestorer | null,
     palette: ICommandPalette | null
   ): IDebugger => {
     const tracker = new WidgetTracker<MainAreaWidget<Debugger>>({
       namespace: 'debugger'
     });
+
     const { commands, shell } = app;
+    const editorFactory = editorServices.factoryService.newInlineEditor;
+
     let widget: MainAreaWidget<Debugger>;
 
     const getModel = () => {
@@ -279,6 +284,7 @@ const main: JupyterFrontEndPlugin<IDebugger> = {
       execute: async () => {
         const debuggerModel = getModel();
         await debuggerModel.session.stop();
+        commands.notifyCommandChanged();
       }
     });
 
@@ -293,6 +299,7 @@ const main: JupyterFrontEndPlugin<IDebugger> = {
       execute: async () => {
         const debuggerModel = getModel();
         await debuggerModel.session.start();
+        commands.notifyCommandChanged();
       }
     });
 
@@ -333,7 +340,8 @@ const main: JupyterFrontEndPlugin<IDebugger> = {
           widget = new MainAreaWidget({
             content: new Debugger({
               connector: state,
-              id: id
+              editorFactory,
+              id
             })
           });
 

+ 29 - 20
src/session.ts

@@ -98,32 +98,41 @@ export class DebugSession implements IDebugger.ISession {
    * Start a new debug session
    */
   async start(): Promise<void> {
-    this._isStarted = true;
-    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'
-    });
-
-    await this.sendRequest('attach', {});
+    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);
+    }
   }
 
   /**
    * Stop the running debug session.
    */
   async stop(): Promise<void> {
-    this._isStarted = false;
-    await this.sendRequest('disconnect', {
-      restart: false,
-      terminateDebuggee: true
-    });
+    try {
+      await this.sendRequest('disconnect', {
+        restart: false,
+        terminateDebuggee: true
+      });
+      this._isStarted = false;
+    } catch (err) {
+      console.error('Error: ', err.message);
+    }
   }
 
   /**

+ 72 - 0
style/editors.css

@@ -0,0 +1,72 @@
+/*-----------------------------------------------------------------------------
+| Copyright (c) Jupyter Development Team.
+| Distributed under the terms of the Modified BSD License.
+|----------------------------------------------------------------------------*/
+
+/* Values inspired by http://phosphorjs.github.io/examples/tabs/ */
+
+.jp-DebuggerEditors .p-TabBar {
+  min-height: 24px;
+  max-height: 24px;
+}
+
+.jp-DebuggerEditors .p-TabBar-content {
+  min-width: 0;
+  min-height: 0;
+  align-items: flex-end;
+  border-bottom: var(--jp-border-width) solid var(--jp-border-color1);
+}
+
+.jp-DebuggerEditors .p-TabBar-tab {
+  background: var(--jp-layout-color2);
+  color: var(--jp-ui-font-color1);
+  padding: 0px 10px;
+  border: var(--jp-border-width) solid var(--jp-border-color1);
+  border-bottom: none;
+  font: 12px Helvetica, Arial, sans-serif;
+  flex: 0 1 125px;
+  min-height: 24px;
+  max-height: 24px;
+  min-width: 35px;
+  margin-left: -1px;
+  line-height: 24px;
+}
+
+.jp-DebuggerEditors .p-TabBar-tab:first-child {
+  margin-left: 0;
+}
+
+.jp-DebuggerEditors .p-TabBar-tab.p-mod-current {
+  min-height: 24px;
+  max-height: 24px;
+  background: var(--jp-layout-color1);
+  color: var(--jp-ui-font-color1);
+}
+
+.jp-DebuggerEditors .p-TabBar-tab:hover:not(.p-mod-current) {
+  background: var(--jp-layout-color1);
+}
+
+.jp-DebuggerEditors .p-TabBar-tabIcon,
+.jp-DebuggerEditors .p-TabBar-tabText,
+.jp-DebuggerEditors .p-TabBar-tabCloseIcon {
+  line-height: 24px;
+}
+
+.jp-DebuggerEditors .p-TabBar-tab.p-mod-closable > .p-TabBar-tabCloseIcon {
+  margin-left: 4px;
+  padding-top: 8px;
+  background-size: 16px;
+  height: 16px;
+  width: 16px;
+  background-image: var(--jp-icon-close);
+  background-position: center;
+  background-repeat: no-repeat;
+}
+
+.jp-DebuggerEditors
+  .p-TabBar-tab.p-mod-closable
+  > .p-TabBar-tabCloseIcon:hover {
+  background-size: 16px;
+  background-image: var(--jp-icon-close-circle);
+}

+ 2 - 1
style/index.css

@@ -8,6 +8,7 @@
 
 @import './icons.css';
 @import './sidebar.css';
+@import './editors.css';
 
 .jp-Debugger {
   background: var(--jp-layout-color1);
@@ -15,7 +16,7 @@
   bottom: 0;
 }
 
-/* font awsome */
+/* font awesome */
 
 .fa {
   display: inline-block;

+ 2 - 0
style/variables.css

@@ -99,6 +99,8 @@
 }
 
 .jp-DebuggerVariables-search input {
+  background: var(--jp-layout-color1);
+  color: var(--jp-ui-font-color1);
   min-height: 22px;
   vertical-align: top;
   width: 100%;

+ 6 - 1
tests/src/debugger.spec.ts

@@ -1,14 +1,19 @@
 import { expect } from 'chai';
 
+import { CodeMirrorEditorFactory } from '@jupyterlab/codemirror';
+
 import { Debugger } from '../../lib/debugger';
 
 class TestPanel extends Debugger {}
 
 describe('Debugger', () => {
+  const editorServices = new CodeMirrorEditorFactory();
+  const editorFactory = editorServices.newInlineEditor;
+
   let panel: TestPanel;
 
   beforeEach(() => {
-    panel = new TestPanel({});
+    panel = new TestPanel({ editorFactory });
   });
 
   afterEach(() => {

+ 16 - 9
tests/src/session.spec.ts

@@ -5,7 +5,7 @@ import { expect } from 'chai';
 
 import { ClientSession, IClientSession } from '@jupyterlab/apputils';
 
-import { createClientSession, sleep } from '@jupyterlab/testutils';
+import { createClientSession } from '@jupyterlab/testutils';
 
 import { find } from '@phosphor/algorithm';
 
@@ -77,7 +77,7 @@ describe('DebugSession', () => {
       expect(reply.body.sourcePath).to.contain('.py');
     });
 
-    it.skip('should handle replies with success false', async () => {
+    it('should handle replies with success false', async () => {
       const reply = await debugSession.sendRequest('evaluate', {
         expression: 'a'
       });
@@ -118,12 +118,19 @@ describe('protocol', () => {
     debugSession = new DebugSession({ client });
     await debugSession.start();
 
+    const stoppedFuture = new PromiseDelegate<void>();
     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;
+        switch (event.event) {
+          case 'thread':
+            const msg = event as DebugProtocol.ThreadEvent;
+            threadId = msg.body.threadId;
+            break;
+          case 'stopped':
+            stoppedFuture.resolve();
+            break;
+          default:
+            break;
         }
       }
     );
@@ -141,8 +148,8 @@ describe('protocol', () => {
     // trigger an execute_request
     client.kernel.requestExecute({ code });
 
-    // TODO: handle events instead
-    await sleep(2000);
+    // wait for the first stopped event
+    await stoppedFuture.promise;
   });
 
   afterEach(async () => {
@@ -209,7 +216,7 @@ describe('protocol', () => {
   });
 
   describe('#continue', () => {
-    it.skip('should proceed to the next breakpoint', async () => {
+    it('should proceed to the next breakpoint', async () => {
       let events: string[] = [];
       const eventsFuture = new PromiseDelegate<string[]>();
       debugSession.eventMessage.connect((sender, event) => {