Skip to content

Commit 25a7315

Browse files
committed
Open the help in the down area (pager)
1 parent 346e30d commit 25a7315

File tree

8 files changed

+334
-6
lines changed

8 files changed

+334
-6
lines changed

app/package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
"@jupyterlab/hub-extension": "~4.5.0-alpha.3",
6363
"@jupyterlab/imageviewer": "~4.5.0-alpha.3",
6464
"@jupyterlab/imageviewer-extension": "~4.5.0-alpha.3",
65+
"@jupyterlab/inspector-extension": "~4.5.0-alpha.3",
6566
"@jupyterlab/javascript-extension": "~4.5.0-alpha.3",
6667
"@jupyterlab/json-extension": "~4.5.0-alpha.3",
6768
"@jupyterlab/logconsole-extension": "~4.5.0-alpha.3",
@@ -160,6 +161,7 @@
160161
"@jupyterlab/htmlviewer-extension": "~4.5.0-alpha.3",
161162
"@jupyterlab/hub-extension": "~4.5.0-alpha.3",
162163
"@jupyterlab/imageviewer-extension": "~4.5.0-alpha.3",
164+
"@jupyterlab/inspector-extension": "~4.5.0-alpha.3",
163165
"@jupyterlab/javascript-extension": "~4.5.0-alpha.3",
164166
"@jupyterlab/json-extension": "~4.5.0-alpha.3",
165167
"@jupyterlab/logconsole-extension": "~4.5.0-alpha.3",
@@ -292,6 +294,9 @@
292294
"@jupyterlab/htmlviewer-extension": true,
293295
"@jupyterlab/hub-extension": true,
294296
"@jupyterlab/imageviewer-extension": true,
297+
"@jupyterlab/inspector-extension": [
298+
"@jupyterlab/inspector-extension:inspector"
299+
],
295300
"@jupyterlab/lsp-extension": true,
296301
"@jupyterlab/mainmenu-extension": [
297302
"@jupyterlab/mainmenu-extension:plugin"
@@ -403,6 +408,7 @@
403408
"@jupyterlab/fileeditor",
404409
"@jupyterlab/htmlviewer",
405410
"@jupyterlab/imageviewer",
411+
"@jupyterlab/inspector",
406412
"@jupyterlab/lsp",
407413
"@jupyterlab/mainmenu",
408414
"@jupyterlab/markdownviewer",

packages/application-extension/schema/shell.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"title": "Customize shell widget positioning",
1010
"description": "Overrides default widget position in the application layout",
1111
"default": {
12+
"Inspector": { "area": "down" },
1213
"Markdown Preview": { "area": "right" },
1314
"Plugins": { "area": "left" }
1415
}
@@ -24,7 +25,7 @@
2425
"type": "object",
2526
"properties": {
2627
"area": {
27-
"enum": ["left", "right"]
28+
"enum": ["left", "right", "down"]
2829
}
2930
},
3031
"additionalProperties": false

packages/application/src/shell.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,12 @@ export class NotebookShell extends Widget implements JupyterFrontEnd.IShell {
305305
} else if (area === 'right') {
306306
this.expandRight(id);
307307
} else if (area === 'down') {
308+
const tabIndex = this._downPanel.tabBar.titles.findIndex(
309+
(title) => title.owner.id === id
310+
);
311+
if (tabIndex >= 0) {
312+
this._downPanel.currentIndex = tabIndex;
313+
}
308314
this._downPanel.show();
309315
widget.activate();
310316
} else {

packages/notebook-extension/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"@jupyterlab/apputils": "~4.6.0-alpha.3",
4444
"@jupyterlab/cells": "~4.5.0-alpha.3",
4545
"@jupyterlab/docmanager": "~4.5.0-alpha.3",
46+
"@jupyterlab/inspector": "~4.5.0-alpha.3",
4647
"@jupyterlab/notebook": "~4.5.0-alpha.3",
4748
"@jupyterlab/settingregistry": "~4.5.0-alpha.3",
4849
"@jupyterlab/translation": "~4.5.0-alpha.3",
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"title": "Jupyter Notebook Pager Settings",
3+
"description": "Settings for controlling pager/help display behavior",
4+
"properties": {
5+
"openHelpInDownArea": {
6+
"type": "boolean",
7+
"title": "Open Help in Down Area",
8+
"description": "Whether to open help/documentation in the inspector panel (down area) or display it inline in the cell output",
9+
"default": true
10+
}
11+
},
12+
"additionalProperties": false,
13+
"type": "object"
14+
}

packages/notebook-extension/src/index.ts

Lines changed: 250 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ import {
77
} from '@jupyterlab/application';
88

99
import {
10-
ISessionContext,
1110
DOMUtils,
12-
IToolbarWidgetRegistry,
1311
ICommandPalette,
12+
ISessionContext,
13+
IToolbarWidgetRegistry,
1414
} from '@jupyterlab/apputils';
1515

1616
import { Cell, CodeCell } from '@jupyterlab/cells';
@@ -21,14 +21,24 @@ import { IDocumentManager } from '@jupyterlab/docmanager';
2121

2222
import { DocumentRegistry } from '@jupyterlab/docregistry';
2323

24+
import {
25+
IInspector,
26+
InspectionHandler,
27+
KernelConnector,
28+
} from '@jupyterlab/inspector';
29+
2430
import { IMainMenu } from '@jupyterlab/mainmenu';
2531

2632
import {
27-
NotebookPanel,
28-
INotebookTracker,
2933
INotebookTools,
34+
INotebookTracker,
35+
NotebookPanel,
3036
} from '@jupyterlab/notebook';
3137

38+
import { Kernel, KernelMessage } from '@jupyterlab/services';
39+
40+
import { MimeModel } from '@jupyterlab/rendermime';
41+
3242
import { ISettingRegistry } from '@jupyterlab/settingregistry';
3343

3444
import { ITranslator, nullTranslator } from '@jupyterlab/translation';
@@ -37,6 +47,8 @@ import { INotebookShell } from '@jupyter-notebook/application';
3747

3848
import { Poll } from '@lumino/polling';
3949

50+
import { Signal } from '@lumino/signaling';
51+
4052
import { Widget } from '@lumino/widgets';
4153

4254
import { TrustedComponent } from './trusted';
@@ -749,6 +761,239 @@ const editNotebookMetadata: JupyterFrontEndPlugin<void> = {
749761
},
750762
};
751763

764+
/**
765+
* A plugin providing a pager widget to display help and documentation
766+
* in the down panel, similar to classic notebook behavior.
767+
*/
768+
const pager: JupyterFrontEndPlugin<void> = {
769+
id: '@jupyter-notebook/notebook-extension:pager',
770+
description:
771+
'A plugin to toggle the inspector when a pager payload is received.',
772+
autoStart: true,
773+
requires: [INotebookTracker, IInspector],
774+
optional: [ISettingRegistry, ITranslator],
775+
activate: (
776+
app: JupyterFrontEnd,
777+
notebookTracker: INotebookTracker,
778+
inspector: IInspector,
779+
settingRegistry: ISettingRegistry | null,
780+
translator: ITranslator | null
781+
) => {
782+
translator = translator ?? nullTranslator;
783+
784+
let openHelpInDownArea = true;
785+
786+
const kernelMessageHandlers: {
787+
[sessionId: string]: {
788+
kernel: Kernel.IKernelConnection;
789+
handler: (
790+
sender: Kernel.IKernelConnection,
791+
args: Kernel.IAnyMessageArgs
792+
) => void;
793+
};
794+
} = {};
795+
796+
if (settingRegistry) {
797+
const loadSettings = settingRegistry.load(pager.id);
798+
const updateSettings = (settings: ISettingRegistry.ISettings): void => {
799+
openHelpInDownArea = settings.get('openHelpInDownArea')
800+
.composite as boolean;
801+
};
802+
803+
Promise.all([loadSettings, app.restored])
804+
.then(([settings]) => {
805+
updateSettings(settings);
806+
settings.changed.connect(updateSettings);
807+
})
808+
.catch((reason: Error) => {
809+
console.error(
810+
`Failed to load settings for ${pager.id}: ${reason.message}`
811+
);
812+
});
813+
}
814+
815+
const setupPagerListener = (sessionContext: ISessionContext) => {
816+
const sessionId = sessionContext.session?.id;
817+
if (!sessionId) {
818+
return;
819+
}
820+
821+
if (kernelMessageHandlers[sessionId]) {
822+
const { kernel, handler } = kernelMessageHandlers[sessionId];
823+
kernel.anyMessage.disconnect(handler);
824+
delete kernelMessageHandlers[sessionId];
825+
}
826+
827+
// Listen for kernel messages that may contain pager payloads
828+
const kernelMessageHandler = async (
829+
sender: Kernel.IKernelConnection,
830+
args: Kernel.IAnyMessageArgs
831+
) => {
832+
if (!openHelpInDownArea) {
833+
// If false, do nothing - let the pager payload pass through
834+
// so it displays inline in the cell output area like in JupyterLab
835+
return;
836+
}
837+
const { msg, direction } = args;
838+
839+
// only check 'execute_reply' from the shell channel for pager data
840+
if (
841+
direction === 'recv' &&
842+
msg.channel === 'shell' &&
843+
msg.header.msg_type === 'execute_reply'
844+
) {
845+
const content = msg.content as KernelMessage.IExecuteReply;
846+
if (
847+
content.status === 'ok' &&
848+
content.payload &&
849+
content.payload.length > 0
850+
) {
851+
const pagePayload = content.payload.find(
852+
(item) => item.source === 'page'
853+
);
854+
855+
if (pagePayload && pagePayload.data) {
856+
const text = (pagePayload.data as any)['text/plain'];
857+
858+
// Remove the 'page' payload from the message to prevent it from also appearing in the cell's output area
859+
content.payload = content.payload.filter(
860+
(item) => item.source !== 'page'
861+
);
862+
if (content.payload.length === 0) {
863+
// If no other payloads remain, delete the payload array from the content.
864+
delete content.payload;
865+
}
866+
867+
await app.commands.execute('inspector:open', {
868+
text,
869+
refresh: true,
870+
});
871+
}
872+
}
873+
}
874+
};
875+
876+
// Connect to the kernel's anyMessage signal to catch
877+
// pager payloads before the output area by cleaning up and reconnecting
878+
if (sessionContext.session?.kernel) {
879+
if (kernelMessageHandlers[sessionId]) {
880+
const { kernel, handler: oldHandler } =
881+
kernelMessageHandlers[sessionId];
882+
kernel.anyMessage.disconnect(oldHandler);
883+
}
884+
885+
sessionContext.session.kernel.anyMessage.connect(kernelMessageHandler);
886+
kernelMessageHandlers[sessionId] = {
887+
kernel: sessionContext.session.kernel,
888+
handler: kernelMessageHandler,
889+
};
890+
}
891+
};
892+
893+
let inspectionHandler: InspectionHandler;
894+
895+
notebookTracker.widgetAdded.connect((_sender, panel) => {
896+
if (panel.sessionContext) {
897+
setupPagerListener(panel.sessionContext);
898+
}
899+
900+
panel.sessionContext.sessionChanged.connect(() => {
901+
if (panel.sessionContext) {
902+
setupPagerListener(panel.sessionContext);
903+
}
904+
});
905+
906+
panel.sessionContext.kernelChanged.connect(() => {
907+
if (panel.sessionContext) {
908+
setupPagerListener(panel.sessionContext);
909+
}
910+
});
911+
912+
const sessionContext = panel.sessionContext;
913+
const rendermime = panel.content.rendermime;
914+
const connector = new (class extends KernelConnector {
915+
async fetch(request: InspectionHandler.IRequest): Promise<any> {
916+
console.log('Custom fetch called with:', request);
917+
}
918+
})({ sessionContext });
919+
920+
// Define a custom inspection handler so we can persist the pager data even after
921+
// switching cells or moving the cursor position
922+
inspectionHandler = new (class extends InspectionHandler {
923+
get inspected() {
924+
return this._notebookInspected;
925+
}
926+
927+
onEditorChange(text: string): void {
928+
if (text && text.trim()) {
929+
this._previousInspectData = text;
930+
}
931+
932+
// Use the current text or fall back to previous data
933+
const dataToShow =
934+
text && text.trim() ? text : this._previousInspectData;
935+
936+
const update: IInspector.IInspectorUpdate = { content: null };
937+
938+
if (dataToShow) {
939+
const data = {
940+
'text/plain': dataToShow,
941+
};
942+
943+
const mimeType = rendermime.preferredMimeType(data);
944+
if (mimeType) {
945+
const widget = rendermime.createRenderer(mimeType);
946+
const model = new MimeModel({ data });
947+
void widget.renderModel(model);
948+
update.content = widget;
949+
}
950+
}
951+
952+
// Emit the inspection update signal
953+
this._notebookInspected.emit(update);
954+
}
955+
956+
private _previousInspectData = '';
957+
private _notebookInspected: Signal<
958+
InspectionHandler,
959+
IInspector.IInspectorUpdate
960+
> = new Signal<InspectionHandler, IInspector.IInspectorUpdate>(this);
961+
})({ connector, rendermime });
962+
963+
// Listen for parent disposal.
964+
panel.disposed.connect(() => {
965+
inspectionHandler.dispose();
966+
});
967+
});
968+
969+
// Handle current notebook if already open
970+
if (notebookTracker.currentWidget) {
971+
const panel = notebookTracker.currentWidget;
972+
if (panel.sessionContext) {
973+
setupPagerListener(panel.sessionContext);
974+
975+
panel.sessionContext.sessionChanged.connect(() => {
976+
if (panel.sessionContext) {
977+
setupPagerListener(panel.sessionContext);
978+
}
979+
});
980+
981+
panel.sessionContext.kernelChanged.connect(() => {
982+
if (panel.sessionContext) {
983+
setupPagerListener(panel.sessionContext);
984+
}
985+
});
986+
}
987+
}
988+
989+
// Keep track of notebook instances and set inspector source.
990+
const setSource = (_widget: Widget | null): void => {
991+
inspector.source = inspectionHandler;
992+
};
993+
void app.restored.then(() => setSource(app.shell.currentWidget));
994+
},
995+
};
996+
752997
/**
753998
* Export the plugins as default.
754999
*/
@@ -761,6 +1006,7 @@ const plugins: JupyterFrontEndPlugin<any>[] = [
7611006
kernelLogo,
7621007
kernelStatus,
7631008
notebookToolsWidget,
1009+
pager,
7641010
scrollOutput,
7651011
tabIcon,
7661012
trusted,

0 commit comments

Comments
 (0)