Skip to content

Commit

Permalink
Copy variable in kernel from debugger panel (jupyterlab#13476)
Browse files Browse the repository at this point in the history
* Add a 'copy variable to globals' context command in the variables explorers of the debugger

* Adds tests on the copy variable feature

* Avoid error on reading scope name

* Fix lint

* Update requests to 'copyToGlobls' one from kernels

* Fix lint

* Registers the new command in debugger.CommmandsIDs

* Fix UI debugger test

* Fix UI debugger test... 2nd

* Update packages/debugger-extension/src/index.ts

Co-authored-by: Michał Krassowski <[email protected]>

* Add docstring on ICopyToGlobalsArguments

* Adds a flag on the copyToGlobals kernel capability

* Adds integration tests independent of the kernel version

* Moves 'copy to clipboard' test from documentation to jupyterlab

* Fix galata/styles and yarn.lock

---------

Co-authored-by: foo <[email protected]>
Co-authored-by: Michał Krassowski <[email protected]>
  • Loading branch information
3 people authored Apr 15, 2023
1 parent ed7c879 commit 560c979
Show file tree
Hide file tree
Showing 19 changed files with 296 additions and 29 deletions.
2 changes: 2 additions & 0 deletions buildutils/src/ensure-repo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,13 +158,15 @@ const SKIP_CSS: Dict<string[]> = {
'@jupyterlab/galata': [
'@jupyterlab/application',
'@jupyterlab/apputils',
'@jupyterlab/debugger',
'@jupyterlab/docmanager',
'@jupyterlab/notebook'
],
'@jupyterlab/galata-extension': [
'@jupyterlab/application',
'@jupyterlab/apputils',
'@jupyterlab/cells',
'@jupyterlab/debugger',
'@jupyterlab/docmanager',
'@jupyterlab/notebook'
],
Expand Down
1 change: 1 addition & 0 deletions galata/extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"@jupyterlab/application": "^4.0.0-beta.1",
"@jupyterlab/apputils": "^4.0.0-beta.1",
"@jupyterlab/cells": "^4.0.0-beta.1",
"@jupyterlab/debugger": "^4.0.0-beta.1",
"@jupyterlab/docmanager": "^4.0.0-beta.1",
"@jupyterlab/nbformat": "^4.0.0-beta.1",
"@jupyterlab/notebook": "^4.0.0-beta.1",
Expand Down
2 changes: 2 additions & 0 deletions galata/extension/src/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
NotificationManager,
WidgetTracker
} from '@jupyterlab/apputils';
import type { IDebugger } from '@jupyterlab/debugger';
import type { IDocumentManager } from '@jupyterlab/docmanager';
import type { ISettingRegistry } from '@jupyterlab/settingregistry';
import { Token } from '@lumino/coreutils';
Expand Down Expand Up @@ -96,6 +97,7 @@ export interface IPluginNameToInterfaceMap {
'@jupyterlab/application-extension:router': IRouter;
'@jupyterlab/docmanager-extension:manager': IDocumentManager;
'@jupyterlab/apputils-extension:settings': ISettingRegistry;
'@jupyterlab/debugger-extension:service': IDebugger;
}

/**
Expand Down
3 changes: 3 additions & 0 deletions galata/extension/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
{
"path": "../../packages/cells"
},
{
"path": "../../packages/debugger"
},
{
"path": "../../packages/docmanager"
},
Expand Down
1 change: 1 addition & 0 deletions galata/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"@jupyterlab/application": "^4.0.0-beta.1",
"@jupyterlab/apputils": "^4.0.0-beta.1",
"@jupyterlab/coreutils": "^6.0.0-beta.1",
"@jupyterlab/debugger": "^4.0.0-beta.1",
"@jupyterlab/docmanager": "^4.0.0-beta.1",
"@jupyterlab/nbformat": "^4.0.0-beta.1",
"@jupyterlab/notebook": "^4.0.0-beta.1",
Expand Down
10 changes: 6 additions & 4 deletions galata/playwright.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@ module.exports = {
testIgnore: '**/.ipynb_checkpoints/**',
timeout: 90000,
use: {
contextOptions: {
permissions: ['clipboard-read', 'clipboard-write']
},
launchOptions: {
// Force slow motion
slowMo: 30
Expand All @@ -31,7 +28,12 @@ module.exports = {
{
name: 'jupyterlab',
testMatch: 'test/jupyterlab/**',
testIgnore: '**/.ipynb_checkpoints/**'
testIgnore: '**/.ipynb_checkpoints/**',
use: {
contextOptions: {
permissions: ['clipboard-read', 'clipboard-write']
}
}
}
],
// Switch to 'always' to keep raw assets for all tests
Expand Down
2 changes: 2 additions & 0 deletions galata/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
NotificationManager,
WidgetTracker
} from '@jupyterlab/apputils';
import type { IDebugger } from '@jupyterlab/debugger';
import type { IDocumentManager } from '@jupyterlab/docmanager';
import type { ISettingRegistry } from '@jupyterlab/settingregistry';

Expand Down Expand Up @@ -86,6 +87,7 @@ export interface IPluginNameToInterfaceMap {
'@jupyterlab/application-extension:router': IRouter;
'@jupyterlab/docmanager-extension:manager': IDocumentManager;
'@jupyterlab/apputils-extension:settings': ISettingRegistry;
'@jupyterlab/debugger-extension:service': IDebugger;
}

/**
Expand Down
29 changes: 4 additions & 25 deletions galata/test/documentation/debugger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,38 +229,17 @@ test.describe('Debugger', () => {
// Don't wait as it will be blocked
void page.notebook.runCell(1);

// Wait to be stopped on the breakpoint
// Wait to be stopped on the breakpoint and the local variables to be displayed
await page.debugger.waitForCallStack();

// Wait for the locals variables to be displayed
await expect(
page.locator('.jp-DebuggerVariables-toolbar select')
).toHaveValue('Locals');
await expect(page.locator('select[aria-label="Scope"]')).toHaveValue(
'Locals'
);

expect(
await page.screenshot({
clip: { y: 58, x: 998, width: 280, height: 138 }
})
).toMatchSnapshot('debugger_variables.png');

// Copy value to clipboard
await page
.locator('.jp-DebuggerVariables-body :text("b")')
.click({ button: 'right' });
await page.locator('.lm-Menu-itemLabel:text("Copy to Clipboard")').click();
expect(await page.evaluate(() => navigator.clipboard.readText())).toBe('2');

// Copy value entry is disabled for variables with empty value
await page
.locator('.jp-DebuggerVariables-toolbar select')
.selectOption('Globals');
await page
.locator('.jp-DebuggerVariables-body :text("special variables")')
.click({ button: 'right' });
await expect(
page.locator('li.lm-Menu-item[data-command="debugger:copy-to-clipboard"]')
).toHaveAttribute('aria-disabled', 'true');
await page.click('button[title^=Continue]');
});

test('Call Stack panel', async ({ page, tmpPath }) => {
Expand Down
161 changes: 161 additions & 0 deletions galata/test/jupyterlab/debugger.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
import { expect, IJupyterLabPageFixture, test } from '@jupyterlab/galata';
import { PromiseDelegate } from '@lumino/coreutils';
import * as path from 'path';

async function openNotebook(page: IJupyterLabPageFixture, tmpPath, fileName) {
Expand Down Expand Up @@ -166,3 +167,163 @@ test.describe('Debugger Tests', () => {
);
});
});

test.describe('Debugger Variables', () => {
test.use({ autoGoto: false });

const copyToGlobalsRequest = new PromiseDelegate<void>();

test.beforeEach(async ({ page, tmpPath }) => {
// Listener to the websocket, to catch the 'copyToGlobals' request.
page.on('websocket', ws => {
ws.on('framesent', event => {
let message = event.payload;
if (Buffer.isBuffer(event.payload)) {
message = event.payload.toString('binary');
}
if (message.includes('copyToGlobals')) {
copyToGlobalsRequest.resolve();
}
});
});

// Initialize the debugger.
await page.goto(`tree/${tmpPath}`);
await createNotebook(page);

await page.debugger.switchOn();
await page.waitForCondition(() => page.debugger.isOpen());

await setBreakpoint(page);
});

test('Copy to globals should work only for local variables', async ({
page
}) => {
// Kernel supports copyToGlobals.
await page.evaluate(async () => {
const debuggerService = await window.galata.getPlugin(
'@jupyterlab/debugger-extension:service'
);
debuggerService!.model.supportCopyToGlobals = true;
});

// Don't wait as it will be blocked.
void page.notebook.runCell(1);

// Wait to be stopped on the breakpoint and the local variables to be displayed.
await page.debugger.waitForCallStack();

// Expect the copy entry to be in the menu.
await page.locator('select[aria-label="Scope"]').selectOption('Locals');
await page.click('.jp-DebuggerVariables-body li span:text("local_var")', {
button: 'right'
});
await expect(
page.locator('.lm-Menu-content li div:text("Copy Variable to Globals")')
).toHaveCount(1);

await expect(
page.locator('.lm-Menu-content li div:text("Copy Variable to Globals")')
).toBeVisible();

// Request the copy of the local variable to globals scope.
await page.click(
'.lm-Menu-content li[data-command="debugger:copy-to-globals"]'
);

// Wait for the request to be sent.
await copyToGlobalsRequest.promise;

// Expect the context menu for global variables to not have the 'copy' entry.
await page.locator('select[aria-label="Scope"]').selectOption('Globals');
await page.click(`.jp-DebuggerVariables-body li span:text("global_var")`, {
button: 'right'
});
await expect(page.locator('.lm-Menu-content')).toBeVisible();
await expect(
page.locator('.lm-Menu-content li div:text("Copy Variable to Globals")')
).toHaveCount(0);
});

test('Copy to globals not available from kernel', async ({ page }) => {
// Kernel doesn't support copyToGlobals.
await page.evaluate(async () => {
const debuggerService = await window.galata.getPlugin(
'@jupyterlab/debugger-extension:service'
);
debuggerService!.model.supportCopyToGlobals = false;
});

// Don't wait as it will be blocked.
void page.notebook.runCell(1);

// Wait to be stopped on the breakpoint and the local variables to be displayed.
await page.debugger.waitForCallStack();

await page.locator('select[aria-label="Scope"]').selectOption('Locals');

// Expect the menu entry not to be visible.
await page.click('.jp-DebuggerVariables-body li span:text("local_var")', {
button: 'right'
});
await expect(
page.locator('.lm-Menu-content li div:text("Copy Variable to Globals")')
).not.toBeVisible();

// Close the contextual menu
await page.keyboard.press('Escape');
await expect(
page.locator('li.lm-Menu-item[data-command="debugger:copy-to-clipboard"]')
).toHaveCount(0);
});

test('Copy to clipboard', async ({ page }) => {
// Don't wait as it will be blocked.
void page.notebook.runCell(1);

// Wait to be stopped on the breakpoint and the local variables to be displayed.
await page.debugger.waitForCallStack();

// Copy value to clipboard
await page.locator('select[aria-label="Scope"]').selectOption('Locals');
await page.click('.jp-DebuggerVariables-body li span:text("local_var")', {
button: 'right'
});
await page.locator('.lm-Menu-itemLabel:text("Copy to Clipboard")').click();
expect(await page.evaluate(() => navigator.clipboard.readText())).toBe('3');

// Copy to clipboard disabled for variables with empty value
await page.locator('select[aria-label="Scope"]').selectOption('Globals');
await page
.locator('.jp-DebuggerVariables-body :text("special variables")')
.click({ button: 'right' });
await expect(
page.locator('li.lm-Menu-item[data-command="debugger:copy-to-clipboard"]')
).toHaveAttribute('aria-disabled', 'true');

// Close the contextual menu
await page.keyboard.press('Escape');
await expect(
page.locator('li.lm-Menu-item[data-command="debugger:copy-to-clipboard"]')
).toHaveCount(0);
});
});

async function createNotebook(page: IJupyterLabPageFixture) {
await page.notebook.createNew();

await page.waitForSelector('text=Python 3 (ipykernel) | Idle');
}

async function setBreakpoint(page: IJupyterLabPageFixture) {
await page.notebook.setCell(
0,
'code',
'global_var = 1\ndef add(a, b):\nlocal_var = a + b\nreturn local_var'
);
await page.notebook.run();
await page.notebook.addCell('code', 'result = add(1, 2)\nprint(result)');

await page.notebook.clickCellGutter(0, 4);
}
3 changes: 3 additions & 0 deletions galata/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
{
"path": "../packages/coreutils"
},
{
"path": "../packages/debugger"
},
{
"path": "../packages/docmanager"
},
Expand Down
4 changes: 4 additions & 0 deletions packages/debugger-extension/schema/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@
{
"command": "debugger:copy-to-clipboard",
"selector": ".jp-DebuggerVariables-body"
},
{
"command": "debugger:copy-to-globals",
"selector": ".jp-DebuggerVariables-body.jp-debuggerVariables-local"
}
]
},
Expand Down
13 changes: 13 additions & 0 deletions packages/debugger-extension/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,19 @@ const variables: JupyterFrontEndPlugin<void> = {
}
}
});

commands.addCommand(CommandIDs.copyToGlobals, {
label: trans.__('Copy Variable to Globals'),
caption: trans.__('Copy variable to globals scope'),
isEnabled: () => !!service.session?.isStarted,
isVisible: () =>
handler.activeWidget instanceof NotebookPanel &&
service.model.supportCopyToGlobals,
execute: async args => {
const name = service.model.variables.selectedVariable!.name;
await service.copyToGlobals(name);
}
});
}
};

Expand Down
2 changes: 2 additions & 0 deletions packages/debugger/src/debugger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ export namespace Debugger {
export const pauseOnExceptions = 'debugger:pause-on-exceptions';

export const copyToClipboard = 'debugger:copy-to-clipboard';

export const copyToGlobals = 'debugger:copy-to-globals';
}

/**
Expand Down
11 changes: 11 additions & 0 deletions packages/debugger/src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,16 @@ export class DebuggerModel implements IDebugger.Model.IService {
this._hasRichVariableRendering = v;
}

/**
* Whether the kernel supports the copyToGlobals request.
*/
get supportCopyToGlobals(): boolean {
return this._supportCopyToGlobals;
}
set supportCopyToGlobals(v: boolean) {
this._supportCopyToGlobals = v;
}

/**
* Whether the model is disposed.
*/
Expand Down Expand Up @@ -149,6 +159,7 @@ export class DebuggerModel implements IDebugger.Model.IService {
private _disposed = new Signal<this, void>(this);
private _isDisposed = false;
private _hasRichVariableRendering = false;
private _supportCopyToGlobals = false;
private _stoppedThreads = new Set<number>();
private _title = '-';
private _titleChanged = new Signal<this, string>(this);
Expand Down
5 changes: 5 additions & 0 deletions packages/debugger/src/panels/variables/grid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ export class VariablesBodyGrid extends Panel {
}
set scope(scope: string) {
this._scope = scope;
if (scope !== 'Globals') {
this.addClass('jp-debuggerVariables-local');
} else {
this.removeClass('jp-debuggerVariables-local');
}
this.update();
}

Expand Down
Loading

0 comments on commit 560c979

Please sign in to comment.