Skip to content

Commit a4e0393

Browse files
authored
Fix issues with nested widgets (#16327)
* Fix issues with nested widgets * Fixes * Fix linter issues * Skip failing test * Fix formatting
1 parent dca1dad commit a4e0393

File tree

11 files changed

+237
-29
lines changed

11 files changed

+237
-29
lines changed

src/kernels/execution/cellExecutionMessageHandler.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ import {
2121
NotebookEdit,
2222
NotebookCellOutputItem,
2323
Disposable,
24-
window
24+
window,
25+
extensions
2526
} from 'vscode';
2627

2728
import type { Kernel } from '@jupyterlab/services';
@@ -41,9 +42,10 @@ import { swallowExceptions } from '../../platform/common/utils/decorators';
4142
import { noop } from '../../platform/common/utils/misc';
4243
import { IKernelController, ITracebackFormatter } from '../../kernels/types';
4344
import { handleTensorBoardDisplayDataOutput } from './executionHelpers';
44-
import { Identifiers, WIDGET_MIMETYPE } from '../../platform/common/constants';
45+
import { Identifiers, RendererExtension, WIDGET_MIMETYPE } from '../../platform/common/constants';
4546
import { CellOutputDisplayIdTracker } from './cellDisplayIdTracker';
4647
import { createDeferred } from '../../platform/common/utils/async';
48+
import { coerce, SemVer } from 'semver';
4749

4850
// Helper interface for the set_next_input execute reply payload
4951
interface ISetNextInputPayload {
@@ -714,13 +716,15 @@ export class CellExecutionMessageHandler implements IDisposable {
714716
// Jupyter Output widgets cannot be rendered properly by the widget manager,
715717
// We need to render that.
716718
if (typeof data.model_id === 'string' && this.commIdsMappedToWidgetOutputModels.has(data.model_id)) {
717-
return false;
719+
// New version of renderer supports this.
720+
return doesNotebookRendererSupportRenderingNestedOutputsInWidgets();
718721
}
719722
return true;
720723
}
721724
if (mime.startsWith('application/vnd')) {
722-
// Custom vendored mimetypes cannot be rendered by the widget manager, it relies on the output renderers.
723-
return false;
725+
// Custom vendored mimetypes cannot be rendered by the widget manager in older versions, it relies on the output renderers.
726+
// New version of renderer supports this.
727+
return doesNotebookRendererSupportRenderingNestedOutputsInWidgets();
724728
}
725729
// Everything else can be rendered by the Jupyter Lab widget manager.
726730
return true;
@@ -1209,3 +1213,16 @@ export class CellExecutionMessageHandler implements IDisposable {
12091213
this.endTemporaryTask();
12101214
}
12111215
}
1216+
1217+
function doesNotebookRendererSupportRenderingNestedOutputsInWidgets() {
1218+
const rendererExtension = extensions.getExtension(RendererExtension);
1219+
if (!rendererExtension) {
1220+
return false;
1221+
}
1222+
1223+
const version = coerce(rendererExtension.packageJSON.version);
1224+
if (!version) {
1225+
return false;
1226+
}
1227+
return version.compare(new SemVer('1.0.23')) >= 0;
1228+
}

src/notebooks/controllers/ipywidgets/message/ipyWidgetMessageDispatcher.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { IKernel, IKernelProvider, type IKernelSocket } from '../../../../kernel
1616
import { IIPyWidgetMessageDispatcher, IPyWidgetMessage } from '../types';
1717
import { shouldMessageBeMirroredWithRenderer } from '../../../../kernels/kernel';
1818
import { KernelSocketMap } from '../../../../kernels/kernelSocket';
19+
import type { IDisplayDataMsg } from '@jupyterlab/services/lib/kernel/messages';
1920

2021
type PendingMessage = {
2122
resultPromise: Deferred<void>;
@@ -52,6 +53,8 @@ export class IPyWidgetMessageDispatcher implements IIPyWidgetMessageDispatcher {
5253
private pendingTargetNames = new Set<string>();
5354
private kernel?: IKernel;
5455
private _postMessageEmitter = new EventEmitter<IPyWidgetMessage>();
56+
private _onDisplayMessage = new EventEmitter<IDisplayDataMsg>();
57+
public readonly onDisplayMessage = this._onDisplayMessage.event;
5558
private messageHooks = new Map<string, (msg: KernelMessage.IIOPubMessage) => boolean | PromiseLike<boolean>>();
5659
private pendingHookRemovals = new Map<string, string>();
5760
private messageHookRequests = new Map<string, Deferred<boolean>>();
@@ -367,10 +370,17 @@ export class IPyWidgetMessageDispatcher implements IIPyWidgetMessageDispatcher {
367370
const msgUuid = uuid();
368371
const promise = createDeferred<void>();
369372
this.waitingMessageIds.set(msgUuid, { startTime: Date.now(), resultPromise: promise });
370-
373+
let deserializedMessage: KernelMessage.IMessage | undefined = undefined;
371374
if (typeof data === 'string') {
372375
if (shouldMessageBeMirroredWithRenderer(data)) {
373376
this.raisePostMessage(IPyWidgetMessages.IPyWidgets_msg, { id: msgUuid, data });
377+
if (data.includes('display_data')) {
378+
deserializedMessage = this.deserialize(data as any, protocol);
379+
const jupyterLab = require('@jupyterlab/services') as typeof import('@jupyterlab/services');
380+
if (jupyterLab.KernelMessage.isDisplayDataMsg(deserializedMessage)) {
381+
this._onDisplayMessage.fire(deserializedMessage);
382+
}
383+
}
374384
}
375385
} else {
376386
const dataToSend = serializeDataViews([data as any]);
@@ -392,7 +402,7 @@ export class IPyWidgetMessageDispatcher implements IIPyWidgetMessageDispatcher {
392402
data.includes('comm_close') ||
393403
data.includes('comm_msg');
394404
if (mustDeserialize) {
395-
const message = this.deserialize(data as any, protocol) as any;
405+
const message = deserializedMessage || (this.deserialize(data as any, protocol) as any);
396406
if (!shouldMessageBeMirroredWithRenderer(message)) {
397407
return;
398408
}

src/notebooks/controllers/ipywidgets/message/ipyWidgetMessageDispatcherFactory.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { IPyWidgetMessages } from '../../../../messageTypes';
88
import { IKernel, IKernelProvider } from '../../../../kernels/types';
99
import { IPyWidgetMessageDispatcher } from './ipyWidgetMessageDispatcher';
1010
import { IIPyWidgetMessageDispatcher, IPyWidgetMessage } from '../types';
11+
import type { IDisplayDataMsg } from '@jupyterlab/services/lib/kernel/messages';
1112

1213
/**
1314
* This just wraps the iPyWidgetMessageDispatcher class.
@@ -19,12 +20,21 @@ class IPyWidgetMessageDispatcherWithOldMessages implements IIPyWidgetMessageDisp
1920
return this._postMessageEmitter.event;
2021
}
2122
private _postMessageEmitter = new EventEmitter<IPyWidgetMessage>();
23+
private _onDisplayMessage = new EventEmitter<IDisplayDataMsg>();
24+
public readonly onDisplayMessage = this._onDisplayMessage.event;
2225
private readonly disposables: IDisposable[] = [];
2326
constructor(
2427
private readonly baseMulticaster: IPyWidgetMessageDispatcher,
2528
private oldMessages: ReadonlyArray<IPyWidgetMessage>
2629
) {
2730
baseMulticaster.postMessage(this.raisePostMessage, this, this.disposables);
31+
baseMulticaster.onDisplayMessage(
32+
(e) => {
33+
this._onDisplayMessage.fire(e);
34+
},
35+
this,
36+
this.disposables
37+
);
2838
}
2939

3040
public dispose() {

src/notebooks/controllers/ipywidgets/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Event, Uri } from 'vscode';
55
import { IDisposable } from '../../../platform/common/types';
66
import { IPyWidgetMessages } from '../../../messageTypes';
77
import { IKernel } from '../../../kernels/types';
8+
import type { IDisplayDataMsg } from '@jupyterlab/services/lib/kernel/messages';
89

910
export interface IPyWidgetMessage {
1011
message: IPyWidgetMessages;
@@ -16,6 +17,7 @@ export interface IPyWidgetMessage {
1617
* Used to send/receive messages related to IPyWidgets
1718
*/
1819
export interface IIPyWidgetMessageDispatcher extends IDisposable {
20+
onDisplayMessage: Event<IDisplayDataMsg>;
1921
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2022
postMessage: Event<IPyWidgetMessage>;
2123
// eslint-disable-next-line @typescript-eslint/no-explicit-any

src/test/datascience/widgets/standardWidgets.vscode.common.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,7 @@ suite('Standard IPyWidget Tests @widgets', function () {
349349
await assertOutputContainsHtml(cell1, comms, ['>Widgets are linked an get updated<'], '.widget-output');
350350
assert.strictEqual(cell3.outputs.length, 0, 'Cell 3 should not have any output');
351351
});
352-
test('More Nested Output Widgets', async () => {
352+
test.skip('More Nested Output Widgets', async () => {
353353
await initializeNotebookForWidgetTest(
354354
disposables,
355355
{

src/webviews/extension-side/ipywidgets/rendererComms.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { dispose } from '../../../platform/common/utils/lifecycle';
1414
import { IDisposable } from '../../../platform/common/types';
1515
import { noop } from '../../../platform/common/utils/misc';
1616
import { logger } from '../../../platform/logging';
17+
import { IPyWidgetMessageDispatcherFactory } from '../../../notebooks/controllers/ipywidgets/message/ipyWidgetMessageDispatcherFactory';
1718

1819
type WidgetData = {
1920
model_id: string;
@@ -27,7 +28,9 @@ export class IPyWidgetRendererComms implements IExtensionSyncActivationService {
2728
private readonly disposables: IDisposable[] = [];
2829
constructor(
2930
@inject(IKernelProvider) private readonly kernelProvider: IKernelProvider,
30-
@inject(IControllerRegistration) private readonly controllers: IControllerRegistration
31+
@inject(IControllerRegistration) private readonly controllers: IControllerRegistration,
32+
@inject(IPyWidgetMessageDispatcherFactory)
33+
private readonly ipywidgetMessageDispatcher: IPyWidgetMessageDispatcherFactory
3134
) {}
3235
private readonly widgetOutputsPerNotebook = new WeakMap<NotebookDocument, Set<string>>();
3336
public dispose() {
@@ -51,6 +54,21 @@ export class IPyWidgetRendererComms implements IExtensionSyncActivationService {
5154
return;
5255
}
5356

57+
// If we have an output widget nested within another output widget.
58+
// Then the output output widget will be displayed by us.
59+
// However nested outputs (any) widgets will be displayed by widget manager.
60+
// And in this case, its possible the display_data message is sent to the webview,
61+
// Sooner than we get the messages from the IKernel above.
62+
// Hence we need to hook into the lower level kernel socket messages to see if that happens.
63+
// Else what happens is the display_data is sent to the webview, but the widget manager doesn't know about it.
64+
// Thats because we have not tracked this model and we don't know about it.
65+
const ipyWidgetMessageDispatcher = this.ipywidgetMessageDispatcher.create(kernel.notebook);
66+
this.disposables.push(
67+
ipyWidgetMessageDispatcher.onDisplayMessage((msg) => {
68+
this.trackModelId(kernel.notebook, msg);
69+
})
70+
);
71+
5472
// eslint-disable-next-line @typescript-eslint/no-require-imports
5573
const jupyterLab = require('@jupyterlab/services') as typeof import('@jupyterlab/services');
5674
const handler = (kernelConnection: IKernelConnection, msg: IIOPubMessage<IOPubMessageType>) => {

src/webviews/webview-side/ipywidgets/kernel/kernel.ts

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,14 @@ class ProxyKernel implements IMessageHandler, Kernel.IKernelConnection {
104104
private hookResults = new Map<string, boolean | PromiseLike<boolean>>();
105105
private websocket: WebSocketWS & { sendEnabled: boolean };
106106
private messageHook: (msg: KernelMessage.IIOPubMessage) => boolean | PromiseLike<boolean>;
107-
private messageHooks: Map<string, (msg: KernelMessage.IIOPubMessage) => boolean | PromiseLike<boolean>>;
108-
private lastHookedMessageId: string | undefined;
107+
private readonly messageHooks = new Map<
108+
string,
109+
{
110+
current: (msg: KernelMessage.IIOPubMessage) => boolean | PromiseLike<boolean>;
111+
previous: ((msg: KernelMessage.IIOPubMessage) => boolean | PromiseLike<boolean>)[];
112+
}
113+
>();
114+
private readonly lastHookedMessageId: string[] = [];
109115
private _options: KernelSocketOptions;
110116
// Messages that are awaiting extension messages to be fully handled
111117
private awaitingExtensionMessage: Map<string, Deferred<void>>;
@@ -169,7 +175,6 @@ class ProxyKernel implements IMessageHandler, Kernel.IKernelConnection {
169175
postOffice.addHandler(this);
170176
this.websocket = proxySocketInstance;
171177
this.messageHook = this.messageHookInterceptor.bind(this);
172-
this.messageHooks = new Map<string, (msg: KernelMessage.IIOPubMessage) => boolean | PromiseLike<boolean>>();
173178
this.fakeOpenSocket();
174179
}
175180

@@ -343,7 +348,14 @@ class ProxyKernel implements IMessageHandler, Kernel.IKernelConnection {
343348
this.postOffice.sendMessage<IInteractiveWindowMapping>(IPyWidgetMessages.IPyWidgets_RegisterMessageHook, msgId);
344349

345350
// Save the real hook so we can call it
346-
this.messageHooks.set(msgId, hook);
351+
const item = this.messageHooks.get(msgId);
352+
if (item) {
353+
// Preserve the previous hook and setup a new hook for the same comm msg.
354+
item.previous.push(item.current);
355+
item.current = hook;
356+
} else {
357+
this.messageHooks.set(msgId, { current: hook, previous: [] });
358+
}
347359

348360
// Wrap the hook and send it to the real kernel
349361
this.realKernel.registerMessageHook(msgId, this.messageHook);
@@ -363,15 +375,20 @@ class ProxyKernel implements IMessageHandler, Kernel.IKernelConnection {
363375

364376
this.postOffice.sendMessage<IInteractiveWindowMapping>(IPyWidgetMessages.IPyWidgets_RemoveMessageHook, {
365377
hookMsgId: msgId,
366-
lastHookedMsgId: this.lastHookedMessageId
378+
lastHookedMsgId: this.lastHookedMessageId.length ? this.lastHookedMessageId.pop() : undefined
367379
});
368380

369381
// Remove our mapping
370-
this.messageHooks.delete(msgId);
371-
this.lastHookedMessageId = undefined;
372-
373-
// Remove from the real kernel
374-
this.realKernel.removeMessageHook(msgId, this.messageHook);
382+
const item = this.messageHooks.get(msgId);
383+
if (item) {
384+
if (item.previous.length > 0) {
385+
item.current = item.previous.pop()!;
386+
} else {
387+
this.messageHooks.delete(msgId);
388+
// Remove from the real kernel
389+
this.realKernel.removeMessageHook(msgId, this.messageHook);
390+
}
391+
}
375392
}
376393

377394
// Called when the extension has finished an operation that we are waiting for in message processing
@@ -415,12 +432,12 @@ class ProxyKernel implements IMessageHandler, Kernel.IKernelConnection {
415432
try {
416433
// Save the active message that is currently being hooked. The Extension
417434
// side needs this information during removeMessageHook so it can delay removal until after a message is called
418-
this.lastHookedMessageId = msg.header.msg_id;
435+
this.lastHookedMessageId.push(msg.header.msg_id);
419436

420437
const hook = this.messageHooks.get((msg.parent_header as any).msg_id);
421438
if (hook) {
422439
// When the kernel calls the hook, save the result for this message. The other side will ask for it
423-
const result = hook(msg);
440+
const result = hook.current(msg);
424441
this.hookResults.set(msg.header.msg_id, result);
425442
if ((result as any).then) {
426443
return (result as any).then((r: boolean) => {

0 commit comments

Comments
 (0)