Ver Fonte

Backport PR #10299: Add debugger variable renderer based on mime type (#11871)

Frédéric Collonval há 3 anos atrás
pai
commit
8630063321

+ 2 - 0
.gitignore

@@ -28,6 +28,8 @@ jupyterlab/style.js
 jupyterlab/tests/**/static
 jupyterlab/tests/**/labextension
 
+ui-tests/test-output
+
 # Remove after next release
 jupyterlab/imports.css
 

+ 1 - 0
packages/apputils/src/mainareawidget.ts

@@ -88,6 +88,7 @@ export class MainAreaWidget<T extends Widget = Widget>
         .catch(e => {
           // Show a revealed promise error.
           const error = new Widget();
+          error.addClass('jp-MainAreaWidget-error');
           // Show the error to the user.
           const pre = document.createElement('pre');
           pre.textContent = String(e);

+ 16 - 0
packages/apputils/style/mainareawidget.css

@@ -7,3 +7,19 @@
 .jp-MainAreaWidget > :focus {
   outline: none;
 }
+
+.jp-MainAreaWidget .jp-MainAreaWidget-error {
+  padding: 6px;
+}
+
+.jp-MainAreaWidget .jp-MainAreaWidget-error > pre {
+  width: auto;
+  padding: 10px;
+  background: var(--jp-error-color3);
+  border: var(--jp-border-width) solid var(--jp-error-color1);
+  border-radius: var(--jp-border-radius);
+  color: var(--jp-ui-font-color1);
+  font-size: var(--jp-ui-font-size1);
+  white-space: pre-wrap;
+  word-wrap: break-word;
+}

+ 10 - 0
packages/debugger-extension/schema/main.json

@@ -21,6 +21,16 @@
           }
         ]
       }
+    ],
+    "context": [
+      {
+        "command": "debugger:inspect-variable",
+        "selector": ".jp-DebuggerVariables-body .jp-DebuggerVariables-grid"
+      },
+      {
+        "command": "debugger:render-mime-variable",
+        "selector": ".jp-DebuggerVariables-body"
+      }
     ]
   },
   "jupyter.lab.shortcuts": [

+ 118 - 20
packages/debugger-extension/src/index.ts

@@ -26,6 +26,7 @@ import {
   Debugger,
   IDebugger,
   IDebuggerConfig,
+  IDebuggerHandler,
   IDebuggerSidebar,
   IDebuggerSources
 } from '@jupyterlab/debugger';
@@ -39,6 +40,7 @@ import {
 } from '@jupyterlab/notebook';
 import {
   standardRendererFactories as initialFactories,
+  IRenderMimeRegistry,
   RenderMimeRegistry
 } from '@jupyterlab/rendermime';
 import { Session } from '@jupyterlab/services';
@@ -168,11 +170,12 @@ const files: JupyterFrontEndPlugin<void> = {
 /**
  * A plugin that provides visual debugging support for notebooks.
  */
-const notebooks: JupyterFrontEndPlugin<void> = {
+const notebooks: JupyterFrontEndPlugin<IDebugger.IHandler> = {
   id: '@jupyterlab/debugger-extension:notebooks',
   autoStart: true,
   requires: [IDebugger, INotebookTracker, ITranslator],
   optional: [ILabShell, ICommandPalette],
+  provides: IDebuggerHandler,
   activate: (
     app: JupyterFrontEnd,
     service: IDebugger,
@@ -180,7 +183,7 @@ const notebooks: JupyterFrontEndPlugin<void> = {
     translator: ITranslator,
     labShell: ILabShell | null,
     palette: ICommandPalette | null
-  ) => {
+  ): Debugger.Handler => {
     const handler = new Debugger.Handler({
       type: 'notebook',
       shell: app.shell,
@@ -231,7 +234,12 @@ const notebooks: JupyterFrontEndPlugin<void> = {
         }
         await updateHandlerAndCommands(widget);
       });
-      return;
+    } else {
+      notebookTracker.currentChanged.connect(
+        async (_, notebookPanel: NotebookPanel) => {
+          await updateHandlerAndCommands(notebookPanel);
+        }
+      );
     }
 
     if (palette) {
@@ -246,6 +254,8 @@ const notebooks: JupyterFrontEndPlugin<void> = {
         await updateHandlerAndCommands(notebookPanel);
       }
     );
+
+    return handler;
   }
 };
 
@@ -313,60 +323,148 @@ const sources: JupyterFrontEndPlugin<IDebugger.ISources> = {
 const variables: JupyterFrontEndPlugin<void> = {
   id: '@jupyterlab/debugger-extension:variables',
   autoStart: true,
-  requires: [IDebugger, ITranslator],
-  optional: [IThemeManager],
+  requires: [IDebugger, IDebuggerHandler, ITranslator, IDebuggerSidebar],
+  optional: [IThemeManager, IRenderMimeRegistry],
   activate: (
     app: JupyterFrontEnd,
     service: IDebugger,
+    handler: Debugger.Handler,
     translator: ITranslator,
-    themeManager: IThemeManager | null
+    sidebar: IDebugger.ISidebar,
+    themeManager: IThemeManager | null,
+    rendermime: IRenderMimeRegistry | null
   ) => {
     const trans = translator.load('jupyterlab');
     const { commands, shell } = app;
     const tracker = new WidgetTracker<MainAreaWidget<Debugger.VariablesGrid>>({
       namespace: 'debugger/inspect-variable'
     });
+    const trackerMime = new WidgetTracker<Debugger.VariableRenderer>({
+      namespace: 'debugger/render-variable'
+    });
     const CommandIDs = Debugger.CommandIDs;
 
+    // Add commands
     commands.addCommand(CommandIDs.inspectVariable, {
       label: trans.__('Inspect Variable'),
       caption: trans.__('Inspect Variable'),
+      isEnabled: args =>
+        !!service.session?.isStarted &&
+        (args.variableReference ??
+          sidebar.variables.latestSelection?.variablesReference ??
+          0) > 0,
       execute: async args => {
-        const { variableReference } = args;
-        if (!variableReference || variableReference === 0) {
-          return;
+        let { variableReference, name } = args as {
+          variableReference?: number;
+          name?: string;
+        };
+
+        if (!variableReference) {
+          variableReference =
+            sidebar.variables.latestSelection?.variablesReference;
+        }
+        if (!name) {
+          name = sidebar.variables.latestSelection?.name;
         }
-        const variables = await service.inspectVariable(
-          variableReference as number
-        );
 
-        const title = args.title as string;
-        const id = `jp-debugger-variable-${title}`;
+        const id = `jp-debugger-variable-${name}`;
         if (
-          !variables ||
-          variables.length === 0 ||
+          !name ||
+          !variableReference ||
           tracker.find(widget => widget.id === id)
         ) {
           return;
         }
 
+        const variables = await service.inspectVariable(
+          variableReference as number
+        );
+        if (!variables || variables.length === 0) {
+          return;
+        }
+
         const model = service.model.variables;
         const widget = new MainAreaWidget<Debugger.VariablesGrid>({
           content: new Debugger.VariablesGrid({
             model,
             commands,
-            scopes: [{ name: title, variables }],
+            scopes: [{ name, variables }],
             themeManager
           })
         });
         widget.addClass('jp-DebuggerVariables');
         widget.id = id;
         widget.title.icon = Debugger.Icons.variableIcon;
-        widget.title.label = `${service.session?.connection?.name} - ${title}`;
+        widget.title.label = `${service.session?.connection?.name} - ${name}`;
         void tracker.add(widget);
-        model.changed.connect(() => widget.dispose());
+        const disposeWidget = () => {
+          widget.dispose();
+          model.changed.disconnect(disposeWidget);
+        };
+        model.changed.connect(disposeWidget);
+        shell.add(widget, 'main', {
+          mode: tracker.currentWidget ? 'split-right' : 'split-bottom',
+          activate: false
+        });
+      }
+    });
+
+    commands.addCommand(CommandIDs.renderMimeVariable, {
+      label: trans.__('Render Variable'),
+      caption: trans.__('Render variable according to its mime type'),
+      isEnabled: () => !!service.session?.isStarted,
+      isVisible: () =>
+        service.model.hasRichVariableRendering &&
+        (rendermime !== null || handler.activeWidget instanceof NotebookPanel),
+      execute: args => {
+        let { name, frameId } = args as {
+          frameId?: number;
+          name?: string;
+        };
+
+        if (!name) {
+          name = sidebar.variables.latestSelection?.name;
+        }
+        if (!frameId) {
+          frameId = service.model.callstack.frame?.id;
+        }
+
+        const activeWidget = handler.activeWidget;
+        let activeRendermime =
+          activeWidget instanceof NotebookPanel
+            ? activeWidget.content.rendermime
+            : rendermime;
+
+        if (!activeRendermime) {
+          return;
+        }
+
+        const id = `jp-debugger-variable-mime-${name}`;
+        if (
+          !name || // Name is mandatory
+          trackerMime.find(widget => widget.id === id) || // Widget already exists
+          (!frameId && service.hasStoppedThreads()) // frame id missing on breakpoint
+        ) {
+          return;
+        }
+
+        const widget = new Debugger.VariableRenderer({
+          dataLoader: service.inspectRichVariable(name, frameId),
+          rendermime: activeRendermime
+        });
+        widget.addClass('jp-DebuggerVariables');
+        widget.id = id;
+        widget.title.icon = Debugger.Icons.variableIcon;
+        widget.title.label = `${service.session?.connection?.name} - ${name}`;
+        void trackerMime.add(widget);
+        const disposeWidget = () => {
+          widget.dispose();
+          service.model.variables.changed.disconnect(disposeWidget);
+        };
+        service.model.variables.changed.connect(disposeWidget);
         shell.add(widget, 'main', {
-          mode: tracker.currentWidget ? 'split-right' : 'split-bottom'
+          mode: trackerMime.currentWidget ? 'split-right' : 'split-bottom',
+          activate: false
         });
       }
     });

+ 11 - 2
packages/debugger/src/debugger.ts

@@ -3,8 +3,6 @@
 
 import { codeIcon, runIcon, stopIcon } from '@jupyterlab/ui-components';
 
-import { EditorHandler as DebuggerEditorHandler } from './handlers/editor';
-
 import { DebuggerConfig } from './config';
 
 import { DebuggerEvaluateDialog } from './dialogs/evaluate';
@@ -13,6 +11,8 @@ import { ReadOnlyEditorFactory as EditorFactory } from './factory';
 
 import { DebuggerHandler } from './handler';
 
+import { EditorHandler as DebuggerEditorHandler } from './handlers/editor';
+
 import {
   closeAllIcon as closeAll,
   stepIntoIcon as stepInto,
@@ -26,6 +26,8 @@ import { DebuggerModel } from './model';
 
 import { VariablesBodyGrid } from './panels/variables/grid';
 
+import { VariableMimeRenderer } from './panels/variables/mimerenderer';
+
 import { DebuggerService } from './service';
 
 import { DebuggerSession } from './session';
@@ -88,6 +90,11 @@ export namespace Debugger {
    */
   export class VariablesGrid extends VariablesBodyGrid {}
 
+  /**
+   * A widget to display data according to its mime type
+   */
+  export class VariableRenderer extends VariableMimeRenderer {}
+
   /**
    * The command IDs used by the debugger plugin.
    */
@@ -104,6 +111,8 @@ export namespace Debugger {
 
     export const inspectVariable = 'debugger:inspect-variable';
 
+    export const renderMimeVariable = 'debugger:render-mime-variable';
+
     export const evaluate = 'debugger:evaluate';
 
     export const restartDebug = 'debugger:restart-debug';

+ 50 - 1
packages/debugger/src/handler.ts

@@ -98,7 +98,7 @@ function updateIconButtonState(
 /**
  * A handler for debugging a widget.
  */
-export class DebuggerHandler {
+export class DebuggerHandler implements DebuggerHandler.IHandler {
   /**
    * Instantiate a new DebuggerHandler.
    *
@@ -110,6 +110,15 @@ export class DebuggerHandler {
     this._service = options.service;
   }
 
+  /**
+   * Get the active widget.
+   */
+  get activeWidget():
+    | DebuggerHandler.SessionWidget[DebuggerHandler.SessionType]
+    | null {
+    return this._activeWidget;
+  }
+
   /**
    * Update a debug handler for the given widget, and
    * handle kernel changed events.
@@ -175,6 +184,7 @@ export class DebuggerHandler {
     }
     connection.iopubMessage.connect(iopubMessage);
     this._iopubMessageHandlers[widget.id] = iopubMessage;
+    this._activeWidget = widget;
 
     return this.updateWidget(widget, connection);
   }
@@ -393,6 +403,9 @@ export class DebuggerHandler {
   private _shell: JupyterFrontEnd.IShell;
   private _service: IDebugger;
   private _previousConnection: Session.ISessionConnection | null;
+  private _activeWidget:
+    | DebuggerHandler.SessionWidget[DebuggerHandler.SessionType]
+    | null;
   private _handlers: {
     [id: string]: DebuggerHandler.SessionHandler[DebuggerHandler.SessionType];
   } = {};
@@ -458,6 +471,42 @@ export namespace DebuggerHandler {
     service: IDebugger;
   }
 
+  /**
+   * An interface for debugger handler.
+   */
+  export interface IHandler {
+    /**
+     * Get the active widget.
+     */
+    activeWidget:
+      | DebuggerHandler.SessionWidget[DebuggerHandler.SessionType]
+      | null;
+
+    /**
+     * Update a debug handler for the given widget, and
+     * handle kernel changed events.
+     *
+     * @param widget The widget to update.
+     * @param connection The session connection.
+     */
+    update(
+      widget: DebuggerHandler.SessionWidget[DebuggerHandler.SessionType],
+      connection: Session.ISessionConnection | null
+    ): Promise<void>;
+
+    /**
+     * Update a debug handler for the given widget, and
+     * handle connection kernel changed events.
+     *
+     * @param widget The widget to update.
+     * @param sessionContext The session context.
+     */
+    updateContext(
+      widget: DebuggerHandler.SessionWidget[DebuggerHandler.SessionType],
+      sessionContext: ISessionContext
+    ): Promise<void>;
+  }
+
   /**
    * The types of sessions that can be debugged.
    */

+ 2 - 1
packages/debugger/src/index.ts

@@ -11,5 +11,6 @@ export {
   IDebugger,
   IDebuggerConfig,
   IDebuggerSources,
-  IDebuggerSidebar
+  IDebuggerSidebar,
+  IDebuggerHandler
 } from './tokens';

+ 12 - 1
packages/debugger/src/model.ts

@@ -16,7 +16,7 @@ import { VariablesModel } from './panels/variables/model';
 /**
  * A model for a debugger.
  */
-export class DebuggerModel {
+export class DebuggerModel implements IDebugger.Model.IService {
   /**
    * Instantiate a new DebuggerModel
    */
@@ -56,6 +56,16 @@ export class DebuggerModel {
     return this._disposed;
   }
 
+  /**
+   * Whether the kernel support rich variable rendering based on mime type.
+   */
+  get hasRichVariableRendering(): boolean {
+    return this._hasRichVariableRendering;
+  }
+  set hasRichVariableRendering(v: boolean) {
+    this._hasRichVariableRendering = v;
+  }
+
   /**
    * Whether the model is disposed.
    */
@@ -128,6 +138,7 @@ export class DebuggerModel {
 
   private _disposed = new Signal<this, void>(this);
   private _isDisposed = false;
+  private _hasRichVariableRendering = false;
   private _stoppedThreads = new Set<number>();
   private _title = '-';
   private _titleChanged = new Signal<this, string>(this);

+ 10 - 6
packages/debugger/src/panels/callstack/model.ts

@@ -21,8 +21,10 @@ export class CallstackModel implements IDebugger.Model.ICallstack {
    */
   set frames(newFrames: IDebugger.IStackFrame[]) {
     this._state = newFrames;
+    const currentFrameId =
+      this.frame !== null ? Private.getFrameId(this.frame) : '';
     const frame = newFrames.find(
-      frame => Private.getFrameId(frame) === Private.getFrameId(this.frame)
+      frame => Private.getFrameId(frame) === currentFrameId
     );
     // Default to the first frame if the previous one can't be found.
     // Otherwise keep the current frame selected.
@@ -35,14 +37,14 @@ export class CallstackModel implements IDebugger.Model.ICallstack {
   /**
    * Get the current frame.
    */
-  get frame(): IDebugger.IStackFrame {
+  get frame(): IDebugger.IStackFrame | null {
     return this._currentFrame;
   }
 
   /**
    * Set the current frame.
    */
-  set frame(frame: IDebugger.IStackFrame) {
+  set frame(frame: IDebugger.IStackFrame | null) {
     this._currentFrame = frame;
     this._currentFrameChanged.emit(frame);
   }
@@ -57,14 +59,16 @@ export class CallstackModel implements IDebugger.Model.ICallstack {
   /**
    * Signal emitted when the current frame has changed.
    */
-  get currentFrameChanged(): ISignal<this, IDebugger.IStackFrame> {
+  get currentFrameChanged(): ISignal<this, IDebugger.IStackFrame | null> {
     return this._currentFrameChanged;
   }
 
   private _state: IDebugger.IStackFrame[] = [];
-  private _currentFrame: IDebugger.IStackFrame;
+  private _currentFrame: IDebugger.IStackFrame | null = null;
   private _framesChanged = new Signal<this, IDebugger.IStackFrame[]>(this);
-  private _currentFrameChanged = new Signal<this, IDebugger.IStackFrame>(this);
+  private _currentFrameChanged = new Signal<this, IDebugger.IStackFrame | null>(
+    this
+  );
 }
 
 /**

+ 2 - 2
packages/debugger/src/panels/sources/model.ts

@@ -23,7 +23,7 @@ export class SourcesModel implements IDebugger.Model.ISources {
    */
   readonly currentFrameChanged: ISignal<
     IDebugger.Model.ICallstack,
-    IDebugger.IStackFrame
+    IDebugger.IStackFrame | null
   >;
 
   /**
@@ -88,7 +88,7 @@ export namespace SourcesModel {
      */
     currentFrameChanged: ISignal<
       IDebugger.Model.ICallstack,
-      IDebugger.IStackFrame
+      IDebugger.IStackFrame | null
     >;
   }
 }

+ 80 - 1
packages/debugger/src/panels/variables/grid.ts

@@ -47,6 +47,13 @@ export class VariablesBodyGrid extends Panel {
     this.addClass('jp-DebuggerVariables-body');
   }
 
+  /**
+   * Get the latest hit variable
+   */
+  get latestSelection(): IDebugger.IVariableSelection | null {
+    return this._grid.latestSelection;
+  }
+
   /**
    * Set the variable filter list.
    *
@@ -126,9 +133,18 @@ class Grid extends Panel {
     mouseHandler.doubleClicked.connect((_, hit) =>
       commands.execute(Debugger.CommandIDs.inspectVariable, {
         variableReference: dataModel.getVariableReference(hit.row),
-        title: dataModel.getVariableName(hit.row)
+        name: dataModel.getVariableName(hit.row)
       })
     );
+    mouseHandler.selected.connect((_, hit) => {
+      const { row } = hit;
+      this._latestSelection = {
+        name: dataModel.getVariableName(row),
+        value: dataModel.data('body', row, 1),
+        type: dataModel.data('body', row, 2),
+        variablesReference: dataModel.getVariableReference(row)
+      };
+    });
     grid.dataModel = dataModel;
     grid.keyHandler = new BasicKeyHandler();
     grid.mouseHandler = mouseHandler;
@@ -173,6 +189,13 @@ class Grid extends Panel {
     return this._grid.dataModel as GridModel;
   }
 
+  /**
+   * Get the latest hit variable
+   */
+  get latestSelection(): IDebugger.IVariableSelection | null {
+    return this._latestSelection;
+  }
+
   /**
    * Handle `after-attach` messages.
    *
@@ -193,6 +216,7 @@ class Grid extends Panel {
   }
 
   private _grid: DataGrid;
+  private _latestSelection: IDebugger.IVariableSelection | null = null;
 }
 
 /**
@@ -434,6 +458,26 @@ namespace Private {
       return this._doubleClicked;
     }
 
+    /**
+     * A signal emitted when the variables grid received mouse down or context menu event.
+     */
+    get selected(): ISignal<this, DataGrid.HitTestResult> {
+      return this._selected;
+    }
+
+    /**
+     * Dispose of the resources held by the mouse handler.
+     */
+    dispose(): void {
+      if (this.isDisposed) {
+        return;
+      }
+
+      Signal.disconnectSender(this);
+
+      super.dispose();
+    }
+
     /**
      * Handle a mouse double-click event.
      *
@@ -445,6 +489,41 @@ namespace Private {
       this._doubleClicked.emit(hit);
     }
 
+    /**
+     * Handle the mouse down event for the data grid.
+     *
+     * @param grid - The data grid of interest.
+     *
+     * @param event - The mouse down event of interest.
+     */
+    onMouseDown(grid: DataGrid, event: MouseEvent): void {
+      // Unpack the event.
+      let { clientX, clientY } = event;
+
+      // Hit test the grid.
+      let hit = grid.hitTest(clientX, clientY);
+
+      this._selected.emit(hit);
+    }
+
+    /**
+     * Handle the context menu event for the data grid.
+     *
+     * @param grid - The data grid of interest.
+     *
+     * @param event - The context menu event of interest.
+     */
+    onContextMenu(grid: DataGrid, event: MouseEvent): void {
+      // Unpack the event.
+      let { clientX, clientY } = event;
+
+      // Hit test the grid.
+      let hit = grid.hitTest(clientX, clientY);
+
+      this._selected.emit(hit);
+    }
+
     private _doubleClicked = new Signal<this, DataGrid.HitTestResult>(this);
+    private _selected = new Signal<this, DataGrid.HitTestResult>(this);
   }
 }

+ 24 - 3
packages/debugger/src/panels/variables/index.ts

@@ -24,7 +24,7 @@ import { VariablesBodyTree } from './tree';
 /**
  * A Panel to show a variable explorer.
  */
-export class Variables extends Panel {
+export class Variables extends Panel implements IDebugger.IVariablesPanel {
   /**
    * Instantiate a new Variables Panel.
    *
@@ -37,7 +37,12 @@ export class Variables extends Panel {
     const translator = options.translator || nullTranslator;
     const trans = translator.load('jupyterlab');
     this._header = new VariablesHeader(translator);
-    this._tree = new VariablesBodyTree({ model, service });
+    this._tree = new VariablesBodyTree({
+      model,
+      service,
+      commands,
+      translator
+    });
     this._table = new VariablesBodyGrid({ model, commands, themeManager });
     this._table.hide();
 
@@ -92,7 +97,7 @@ export class Variables extends Panel {
       }
     };
 
-    markViewButtonSelection(this._table.isHidden ? 'tree' : 'table');
+    markViewButtonSelection(this.viewMode);
 
     this._header.toolbar.addItem('view-VariableTreeView', treeViewButton);
 
@@ -104,6 +109,22 @@ export class Variables extends Panel {
     this.addClass('jp-DebuggerVariables');
   }
 
+  /**
+   * Latest variable selected.
+   */
+  get latestSelection(): IDebugger.IVariableSelection | null {
+    return this._table.isHidden
+      ? this._tree.latestSelection
+      : this._table.latestSelection;
+  }
+
+  /**
+   * Get the variable explorer view mode
+   */
+  get viewMode(): 'tree' | 'table' {
+    return this._table.isHidden ? 'tree' : 'table';
+  }
+
   /**
    * Set the variable filter for both the tree and table views.
    */

+ 65 - 0
packages/debugger/src/panels/variables/mimerenderer.ts

@@ -0,0 +1,65 @@
+import { MainAreaWidget } from '@jupyterlab/apputils';
+import { IRenderMimeRegistry, MimeModel } from '@jupyterlab/rendermime';
+import { PromiseDelegate } from '@lumino/coreutils';
+import { Panel } from '@lumino/widgets';
+import { IDebugger } from '../../tokens';
+
+/**
+ * Debugger variable mime type renderer
+ */
+export class VariableMimeRenderer extends MainAreaWidget<Panel> {
+  /**
+   * Instantiate a new VariableMimeRenderer.
+   */
+  constructor(options: VariableMimeRenderer.IOptions) {
+    const { dataLoader, rendermime } = options;
+    const content = new Panel();
+    const loaded = new PromiseDelegate<void>();
+    super({
+      content,
+      reveal: Promise.all([dataLoader, loaded.promise])
+    });
+
+    dataLoader
+      .then(async data => {
+        if (data.data) {
+          const mimeType = rendermime.preferredMimeType(data.data, 'any');
+
+          if (mimeType) {
+            const widget = rendermime.createRenderer(mimeType);
+            const model = new MimeModel(data);
+            await widget.renderModel(model);
+
+            content.addWidget(widget);
+            loaded.resolve();
+          } else {
+            loaded.reject('Unable to determine the preferred mime type.');
+          }
+        } else {
+          loaded.reject('Unable to get a view on the variable.');
+        }
+      })
+      .catch(reason => {
+        loaded.reject(reason);
+      });
+  }
+}
+
+/**
+ * Debugger variable mime type renderer namespace
+ */
+export namespace VariableMimeRenderer {
+  /**
+   * Constructor options
+   */
+  export interface IOptions {
+    /**
+     * Variable to be rendered
+     */
+    dataLoader: Promise<IDebugger.IRichVariable>;
+    /**
+     * Render mime type registry
+     */
+    rendermime: IRenderMimeRegistry;
+  }
+}

+ 134 - 28
packages/debugger/src/panels/variables/tree.tsx

@@ -2,21 +2,17 @@
 // Distributed under the terms of the Modified BSD License.
 
 import { ReactWidget } from '@jupyterlab/apputils';
-
-import { caretDownEmptyIcon } from '@jupyterlab/ui-components';
-
+import { ITranslator, nullTranslator } from '@jupyterlab/translation';
+import { caretDownEmptyIcon, searchIcon } from '@jupyterlab/ui-components';
 import { ArrayExt } from '@lumino/algorithm';
-
+import { CommandRegistry } from '@lumino/commands';
 import React, { useEffect, useState } from 'react';
-
 import { DebugProtocol } from 'vscode-debugprotocol';
-
+import { convertType } from '.';
+import { Debugger } from '../../debugger';
 import { IDebugger } from '../../tokens';
-
 import { VariablesModel } from './model';
 
-import { convertType } from '.';
-
 /**
  * The body for tree of variables.
  */
@@ -28,7 +24,9 @@ export class VariablesBodyTree extends ReactWidget {
    */
   constructor(options: VariablesBodyTree.IOptions) {
     super();
+    this._commands = options.commands;
     this._service = options.service;
+    this._translator = options.translator;
 
     const model = options.model;
     model.changed.connect(this._updateScopes, this);
@@ -46,15 +44,27 @@ export class VariablesBodyTree extends ReactWidget {
     return scope ? (
       <VariablesComponent
         key={scope.name}
+        commands={this._commands}
         service={this._service}
         data={scope.variables}
         filter={this._filter}
+        translator={this._translator}
+        handleSelectVariable={variable => {
+          this._latestSelection = variable;
+        }}
       />
     ) : (
       <div></div>
     );
   }
 
+  /**
+   * Get the latest hit variable
+   */
+  get latestSelection(): IDebugger.IVariableSelection | null {
+    return this._latestSelection;
+  }
+
   /**
    * Set the variable filter list.
    */
@@ -84,10 +94,31 @@ export class VariablesBodyTree extends ReactWidget {
     this.update();
   }
 
+  private _commands: CommandRegistry;
   private _scope = '';
   private _scopes: IDebugger.IScope[] = [];
   private _filter = new Set<string>();
+  private _latestSelection: IDebugger.IVariableSelection | null = null;
   private _service: IDebugger;
+  private _translator: ITranslator | undefined;
+}
+
+interface IVariablesComponentProps {
+  /**
+   * The commands registry.
+   */
+  commands: CommandRegistry;
+  data: IDebugger.IVariable[];
+  service: IDebugger;
+  filter?: Set<string>;
+  /**
+   * The application language translator
+   */
+  translator?: ITranslator;
+  /**
+   * Callback on variable selection
+   */
+  handleSelectVariable?: (variable: IDebugger.IVariable) => void;
 }
 
 /**
@@ -98,15 +129,15 @@ export class VariablesBodyTree extends ReactWidget {
  * @param props.service The debugger service.
  * @param props.filter Optional variable filter list.
  */
-const VariablesComponent = ({
-  data,
-  service,
-  filter
-}: {
-  data: IDebugger.IVariable[];
-  service: IDebugger;
-  filter?: Set<string>;
-}): JSX.Element => {
+const VariablesComponent = (props: IVariablesComponentProps): JSX.Element => {
+  const {
+    commands,
+    data,
+    service,
+    filter,
+    translator,
+    handleSelectVariable
+  } = props;
   const [variables, setVariables] = useState(data);
 
   useEffect(() => {
@@ -124,9 +155,12 @@ const VariablesComponent = ({
           return (
             <VariableComponent
               key={key}
+              commands={commands}
               data={variable}
               service={service}
               filter={filter}
+              translator={translator}
+              onSelect={handleSelectVariable}
             />
           );
         })}
@@ -134,6 +168,36 @@ const VariablesComponent = ({
   );
 };
 
+/**
+ * VariableComponent properties
+ */
+interface IVariableComponentProps {
+  /**
+   * The commands registry.
+   */
+  commands: CommandRegistry;
+  /**
+   * Variable description
+   */
+  data: IDebugger.IVariable;
+  /**
+   * Filter applied on the variable list
+   */
+  filter?: Set<string>;
+  /**
+   * The Debugger service
+   */
+  service: IDebugger;
+  /**
+   * The application language translator
+   */
+  translator?: ITranslator;
+  /**
+   * Callback on selection
+   */
+  onSelect?: (variable: IDebugger.IVariable) => void;
+}
+
 /**
  * A React component to display one node variable in tree.
  *
@@ -142,15 +206,8 @@ const VariablesComponent = ({
  * @param props.service The debugger service.
  * @param props.filter Optional variable filter list.
  */
-const VariableComponent = ({
-  data,
-  service,
-  filter
-}: {
-  data: IDebugger.IVariable;
-  service: IDebugger;
-  filter?: Set<string>;
-}): JSX.Element => {
+const VariableComponent = (props: IVariableComponentProps): JSX.Element => {
+  const { commands, data, service, filter, translator, onSelect } = props;
   const [variable] = useState(data);
   const [expanded, setExpanded] = useState<boolean>();
   const [variables, setVariables] = useState<DebugProtocol.Variable[]>();
@@ -161,10 +218,13 @@ const VariableComponent = ({
   const styleType = {
     color: 'var(--jp-mirror-editor-string-color)'
   };
+  const onSelection = onSelect ?? (() => void 0);
 
   const expandable =
     variable.variablesReference !== 0 || variable.type === 'function';
 
+  const trans = (translator ?? nullTranslator).load('jupyterlab');
+
   const onVariableClicked = async (e: React.MouseEvent): Promise<void> => {
     if (!expandable) {
       return;
@@ -178,7 +238,12 @@ const VariableComponent = ({
   };
 
   return (
-    <li onClick={(e): Promise<void> => onVariableClicked(e)}>
+    <li
+      onClick={(e): Promise<void> => onVariableClicked(e)}
+      onMouseDown={() => {
+        onSelection(variable);
+      }}
+    >
       <caretDownEmptyIcon.react
         visibility={expandable ? 'visible' : 'hidden'}
         stylesheet="menuItem"
@@ -188,12 +253,45 @@ const VariableComponent = ({
       <span style={styleName}>{variable.name}</span>
       <span>: </span>
       <span style={styleType}>{convertType(variable)}</span>
+      <span className="jp-DebuggerVariables-hspacer"></span>
+      {service.model.hasRichVariableRendering && (
+        <button
+          className="jp-DebuggerVariables-renderVariable"
+          disabled={
+            !commands.isEnabled(Debugger.CommandIDs.renderMimeVariable, {
+              name: variable.name,
+              variablesReference: variable.variablesReference
+            } as any)
+          }
+          onClick={e => {
+            e.stopPropagation();
+            onSelection(variable);
+            commands
+              .execute(Debugger.CommandIDs.renderMimeVariable, {
+                name: variable.name,
+                variablesReference: variable.variablesReference
+              } as any)
+              .catch(reason => {
+                console.error(
+                  `Failed to render variable ${variable.name}`,
+                  reason
+                );
+              });
+          }}
+          title={trans.__('Render variable')}
+        >
+          <searchIcon.react stylesheet="menuItem" tag="span" />
+        </button>
+      )}
+
       {expanded && variables && (
         <VariablesComponent
           key={variable.name}
+          commands={commands}
           data={variables}
           service={service}
           filter={filter}
+          translator={translator}
         />
       )}
     </li>
@@ -216,5 +314,13 @@ namespace VariablesBodyTree {
      * The debugger service.
      */
     service: IDebugger;
+    /**
+     * The commands registry.
+     */
+    commands: CommandRegistry;
+    /**
+     * The application language translator
+     */
+    translator?: ITranslator;
   }
 }

+ 33 - 3
packages/debugger/src/service.ts

@@ -259,6 +259,31 @@ export class DebuggerService implements IDebugger, IDisposable {
     }
   }
 
+  /**
+   * Request rich representation of a variable.
+   *
+   * @param variableName The variable name to request
+   * @param frameId The current frame id in which to request the variable
+   * @returns The mime renderer data model
+   */
+  async inspectRichVariable(
+    variableName: string,
+    frameId?: number
+  ): Promise<IDebugger.IRichVariable> {
+    if (!this.session) {
+      throw new Error('No active debugger session');
+    }
+    const reply = await this.session.sendRequest('richInspectVariables', {
+      variableName,
+      frameId
+    });
+    if (reply.success) {
+      return reply.body;
+    } else {
+      throw new Error(reply.message);
+    }
+  }
+
   /**
    * Request variables for a given variable reference.
    *
@@ -273,7 +298,11 @@ export class DebuggerService implements IDebugger, IDisposable {
     const reply = await this.session.sendRequest('variables', {
       variablesReference
     });
-    return reply.body.variables;
+    if (reply.success) {
+      return reply.body.variables;
+    } else {
+      throw new Error(reply.message);
+    }
   }
 
   /**
@@ -318,9 +347,10 @@ export class DebuggerService implements IDebugger, IDisposable {
 
     const reply = await this.session.restoreState();
     const { body } = reply;
-    const breakpoints = this._mapBreakpoints(reply.body.breakpoints);
-    const stoppedThreads = new Set(reply.body.stoppedThreads);
+    const breakpoints = this._mapBreakpoints(body.breakpoints);
+    const stoppedThreads = new Set(body.stoppedThreads);
 
+    this._model.hasRichVariableRendering = body.richRendering === true;
     this._config.setHashParams({
       kernel: this.session?.connection?.kernel?.name ?? '',
       method: body.hashMethod,

+ 117 - 7
packages/debugger/src/tokens.ts

@@ -5,7 +5,7 @@ import { CodeEditor, CodeEditorWrapper } from '@jupyterlab/codeeditor';
 
 import { KernelMessage, Session } from '@jupyterlab/services';
 
-import { Token } from '@lumino/coreutils';
+import { ReadonlyJSONObject, Token } from '@lumino/coreutils';
 
 import { IObservableDisposable } from '@lumino/disposable';
 
@@ -15,6 +15,8 @@ import { Widget } from '@lumino/widgets';
 
 import { DebugProtocol } from 'vscode-debugprotocol';
 
+import { DebuggerHandler } from './handler';
+
 /**
  * An interface describing an application's visual debugger.
  */
@@ -87,6 +89,18 @@ export interface IDebugger {
     variablesReference: number
   ): Promise<DebugProtocol.Variable[]>;
 
+  /**
+   * Request rich representation of a variable.
+   *
+   * @param variableName The variable name to request
+   * @param variablesReference The variable reference to request
+   * @returns The mime renderer data model
+   */
+  inspectRichVariable(
+    variableName: string,
+    variablesReference?: number
+  ): Promise<IDebugger.IRichVariable>;
+
   /**
    * Requests all the defined variables and display them in the
    * table view.
@@ -248,6 +262,11 @@ export namespace IDebugger {
     getTmpFileParams(kernel: string): IConfig.FileParams;
   }
 
+  /**
+   * An interface for debugger handler.
+   */
+  export interface IHandler extends DebuggerHandler.IHandler {}
+
   /**
    * An interface for a scope.
    */
@@ -335,6 +354,21 @@ export namespace IDebugger {
    */
   export interface IStackFrame extends DebugProtocol.StackFrame {}
 
+  /**
+   * A reply to an rich inspection request.
+   */
+  export interface IRichVariable {
+    /**
+     * The MIME bundle data returned from an rich inspection request.
+     */
+    data: ReadonlyJSONObject;
+
+    /**
+     * Any metadata that accompanies the MIME bundle returning from a rich inspection request.
+     */
+    metadata: ReadonlyJSONObject;
+  }
+
   /**
    * An interface for a variable.
    */
@@ -421,6 +455,7 @@ export namespace IDebugger {
       restart: DebugProtocol.RestartArguments;
       restartFrame: DebugProtocol.RestartFrameArguments;
       reverseContinue: DebugProtocol.ReverseContinueArguments;
+      richInspectVariables: IRichVariablesArguments;
       scopes: DebugProtocol.ScopesArguments;
       setBreakpoints: DebugProtocol.SetBreakpointsArguments;
       setExceptionBreakpoints: DebugProtocol.SetExceptionBreakpointsArguments;
@@ -464,6 +499,7 @@ export namespace IDebugger {
       restart: DebugProtocol.RestartResponse;
       restartFrame: DebugProtocol.RestartFrameResponse;
       reverseContinue: DebugProtocol.ReverseContinueResponse;
+      richInspectVariables: IRichVariablesResponse;
       scopes: DebugProtocol.ScopesResponse;
       setBreakpoints: DebugProtocol.SetBreakpointsResponse;
       setExceptionBreakpoints: DebugProtocol.SetExceptionBreakpointsResponse;
@@ -497,13 +533,17 @@ export namespace IDebugger {
      */
     export interface IDebugInfoResponse extends DebugProtocol.Response {
       body: {
-        isStarted: boolean;
+        breakpoints: IDebugInfoBreakpoints[];
         hashMethod: string;
         hashSeed: number;
-        breakpoints: IDebugInfoBreakpoints[];
+        isStarted: boolean;
+        /**
+         * Whether the kernel supports variable rich rendering or not.
+         */
+        richRendering?: boolean;
+        stoppedThreads: number[];
         tmpFilePrefix: string;
         tmpFileSuffix: string;
-        stoppedThreads: number[];
       };
     }
 
@@ -533,6 +573,36 @@ export namespace IDebugger {
       };
     }
 
+    /**
+     * Arguments for 'richVariables' request
+     *
+     * This is an addition to the Debug Adapter Protocol to support
+     * render rich variable representation.
+     */
+    export interface IRichVariablesArguments {
+      /**
+       * Variable name
+       */
+      variableName: string;
+      /**
+       * Frame Id
+       */
+      frameId?: number;
+    }
+
+    /**
+     * Arguments for 'richVariables' request
+     *
+     * This is an addition to the Debug Adapter Protocol to support
+     * rich rendering of variables.
+     */
+    export interface IRichVariablesResponse extends DebugProtocol.Response {
+      /**
+       * Variable mime type data
+       */
+      body: IRichVariable;
+    }
+
     /**
      * Response to the 'kernel_info_request' request.
      * This interface extends the IInfoReply by adding the `debugger` key
@@ -544,10 +614,38 @@ export namespace IDebugger {
     }
   }
 
+  /**
+   * Select variable in the variables explorer.
+   */
+  export interface IVariableSelection
+    extends Pick<
+      DebugProtocol.Variable,
+      'name' | 'type' | 'variablesReference' | 'value'
+    > {}
+
+  /**
+   * Debugger variables explorer interface.
+   */
+  export interface IVariablesPanel {
+    /**
+     * Select variable in the variables explorer.
+     */
+    latestSelection: IVariableSelection | null;
+    /**
+     * Variable view mode.
+     */
+    viewMode: 'tree' | 'table';
+  }
+
   /**
    * Debugger sidebar interface.
    */
   export interface ISidebar extends Widget {
+    /**
+     * Debugger variables explorer.
+     */
+    variables: IVariablesPanel;
+
     /**
      * Add item at the end of the sidebar.
      */
@@ -672,12 +770,12 @@ export namespace IDebugger {
       /**
        * Signal emitted when the current frame has changed.
        */
-      readonly currentFrameChanged: ISignal<this, IDebugger.IStackFrame>;
+      readonly currentFrameChanged: ISignal<this, IDebugger.IStackFrame | null>;
 
       /**
        * The current frame.
        */
-      frame: IDebugger.IStackFrame;
+      frame: IDebugger.IStackFrame | null;
 
       /**
        * The frames for the callstack.
@@ -704,6 +802,11 @@ export namespace IDebugger {
        */
       readonly callstack: ICallstack;
 
+      /**
+       * Whether the kernel support rich variable rendering based on mime type.
+       */
+      hasRichVariableRendering: boolean;
+
       /**
        * The variables UI model.
        */
@@ -744,7 +847,7 @@ export namespace IDebugger {
        */
       readonly currentFrameChanged: ISignal<
         IDebugger.Model.ICallstack,
-        IDebugger.IStackFrame
+        IDebugger.IStackFrame | null
       >;
 
       /**
@@ -828,3 +931,10 @@ export const IDebuggerSources = new Token<IDebugger.ISources>(
 export const IDebuggerSidebar = new Token<IDebugger.ISidebar>(
   '@jupyterlab/debugger:IDebuggerSidebar'
 );
+
+/**
+ * The debugger handler token.
+ */
+export const IDebuggerHandler = new Token<IDebugger.IHandler>(
+  '@jupyterlab/debugger:IDebuggerHandler'
+);

+ 32 - 0
packages/debugger/style/variables.css

@@ -30,6 +30,38 @@
   padding: 5px 0 0 0;
   cursor: pointer;
   color: var(--jp-content-font-color1);
+  display: flex;
+  align-items: center;
+}
+
+.jp-DebuggerVariables-body ul li:hover .jp-DebuggerVariables-renderVariable {
+  display: block;
+}
+
+.jp-DebuggerVariables-body .jp-DebuggerVariables-hspacer {
+  flex: 1 1 auto;
+}
+
+.jp-DebuggerVariables-body .jp-DebuggerVariables-renderVariable {
+  flex: 0 0 auto;
+  border: none;
+  background: none;
+  cursor: pointer;
+  display: none;
+  padding: 0px 8px;
+}
+
+.jp-DebuggerVariables-body .jp-DebuggerVariables-renderVariable:active {
+  transform: scale(1.272) translateX(-4px);
+  overflow: hidden;
+}
+
+.jp-DebuggerVariables-body .jp-DebuggerVariables-renderVariable:hover {
+  background-color: var(--jp-layout-color2);
+}
+
+.jp-DebuggerVariables-body .jp-DebuggerVariables-renderVariable:disabled {
+  cursor: default;
 }
 
 .jp-DebuggerVariables-body ul li > ul {