@jupyterlab/notebook-extension 4.7.0-alpha.0 → 4.7.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.d.ts +18 -0
- package/lib/index.js +246 -76
- package/lib/index.js.map +1 -1
- package/lib/nboutput.js +7 -2
- package/lib/nboutput.js.map +1 -1
- package/lib/tool-widgets/activeCellToolWidget.d.ts +58 -2
- package/lib/tool-widgets/activeCellToolWidget.js +236 -29
- package/lib/tool-widgets/activeCellToolWidget.js.map +1 -1
- package/lib/tool-widgets/metadataEditorFields.js +6 -0
- package/lib/tool-widgets/metadataEditorFields.js.map +1 -1
- package/package.json +39 -39
- package/schema/tools.json +8 -0
- package/schema/tracker.json +2 -0
- package/src/index.ts +311 -88
- package/src/nboutput.ts +9 -2
- package/src/tool-widgets/activeCellToolWidget.tsx +326 -36
- package/src/tool-widgets/metadataEditorFields.tsx +6 -0
package/src/index.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { ILabShell, ILayoutRestorer, IRouter } from '@jupyterlab/application';
|
|
|
16
16
|
import type { ISessionContext } from '@jupyterlab/apputils';
|
|
17
17
|
import {
|
|
18
18
|
Clipboard,
|
|
19
|
+
CommandLinker,
|
|
19
20
|
createToolbarFactory,
|
|
20
21
|
Dialog,
|
|
21
22
|
ICommandPalette,
|
|
@@ -135,7 +136,7 @@ import type {
|
|
|
135
136
|
ReadonlyJSONValue,
|
|
136
137
|
ReadonlyPartialJSONObject
|
|
137
138
|
} from '@lumino/coreutils';
|
|
138
|
-
import { JSONExt, UUID } from '@lumino/coreutils';
|
|
139
|
+
import { JSONExt, Token, UUID } from '@lumino/coreutils';
|
|
139
140
|
import type { IDisposable } from '@lumino/disposable';
|
|
140
141
|
import { DisposableSet } from '@lumino/disposable';
|
|
141
142
|
import type { Message } from '@lumino/messaging';
|
|
@@ -145,7 +146,10 @@ import { Panel } from '@lumino/widgets';
|
|
|
145
146
|
import { CellBarExtension } from '@jupyterlab/cell-toolbar';
|
|
146
147
|
import { cellExecutor } from './cellexecutor';
|
|
147
148
|
import { logNotebookOutput } from './nboutput';
|
|
148
|
-
import {
|
|
149
|
+
import {
|
|
150
|
+
ActiveCellTool,
|
|
151
|
+
CellIdField
|
|
152
|
+
} from './tool-widgets/activeCellToolWidget';
|
|
149
153
|
import {
|
|
150
154
|
CellMetadataField,
|
|
151
155
|
NotebookMetadataField
|
|
@@ -189,6 +193,8 @@ namespace CommandIDs {
|
|
|
189
193
|
|
|
190
194
|
export const exportToFormat = 'notebook:export-to-format';
|
|
191
195
|
|
|
196
|
+
export const showExportGuidance = 'notebook:show-export-guidance';
|
|
197
|
+
|
|
192
198
|
export const run = 'notebook:run-cell';
|
|
193
199
|
|
|
194
200
|
export const runAndAdvance = 'notebook:run-cell-and-select-next';
|
|
@@ -372,6 +378,30 @@ const FACTORY = 'Notebook';
|
|
|
372
378
|
*/
|
|
373
379
|
const FORMAT_EXCLUDE = ['notebook', 'python', 'custom'];
|
|
374
380
|
|
|
381
|
+
/**
|
|
382
|
+
* Documentation page describing notebook export support.
|
|
383
|
+
*/
|
|
384
|
+
const NOTEBOOK_EXPORT_DOCS_URL =
|
|
385
|
+
'https://jupyterlab.readthedocs.io/en/stable/user/export.html';
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* An interface describing how export guidance is presented to users.
|
|
389
|
+
*/
|
|
390
|
+
export interface INotebookExportGuidance {
|
|
391
|
+
/**
|
|
392
|
+
* Show guidance for enabling notebook exports.
|
|
393
|
+
*/
|
|
394
|
+
showExportHelp(): Promise<void>;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* A token describing a service for showing notebook export guidance.
|
|
399
|
+
*/
|
|
400
|
+
export const INotebookExportGuidance = new Token<INotebookExportGuidance>(
|
|
401
|
+
'@jupyterlab/notebook-extension:INotebookExportGuidance',
|
|
402
|
+
'A service for showing notebook export guidance.'
|
|
403
|
+
);
|
|
404
|
+
|
|
375
405
|
/**
|
|
376
406
|
* Setting Id storing the customized toolbar definition.
|
|
377
407
|
*/
|
|
@@ -648,13 +678,14 @@ export const exportPlugin: JupyterFrontEndPlugin<void> = {
|
|
|
648
678
|
description: 'Adds the export notebook commands.',
|
|
649
679
|
autoStart: true,
|
|
650
680
|
requires: [ITranslator, INotebookTracker],
|
|
651
|
-
optional: [IMainMenu, ICommandPalette],
|
|
681
|
+
optional: [IMainMenu, ICommandPalette, INotebookExportGuidance],
|
|
652
682
|
activate: (
|
|
653
683
|
app: JupyterFrontEnd,
|
|
654
684
|
translator: ITranslator,
|
|
655
685
|
tracker: INotebookTracker,
|
|
656
686
|
mainMenu: IMainMenu | null,
|
|
657
|
-
palette: ICommandPalette | null
|
|
687
|
+
palette: ICommandPalette | null,
|
|
688
|
+
exportGuidance: INotebookExportGuidance | null
|
|
658
689
|
) => {
|
|
659
690
|
const trans = translator.load('jupyterlab');
|
|
660
691
|
const { commands, shell } = app;
|
|
@@ -745,21 +776,91 @@ export const exportPlugin: JupyterFrontEndPlugin<void> = {
|
|
|
745
776
|
}
|
|
746
777
|
});
|
|
747
778
|
|
|
779
|
+
commands.addCommand(CommandIDs.showExportGuidance, {
|
|
780
|
+
label: trans.__('Enable notebook exports'),
|
|
781
|
+
execute: () => {
|
|
782
|
+
if (exportGuidance) {
|
|
783
|
+
return exportGuidance.showExportHelp();
|
|
784
|
+
}
|
|
785
|
+
window.open(NOTEBOOK_EXPORT_DOCS_URL, '_blank', 'noopener,noreferrer');
|
|
786
|
+
return undefined;
|
|
787
|
+
},
|
|
788
|
+
describedBy: {
|
|
789
|
+
args: {
|
|
790
|
+
type: 'object',
|
|
791
|
+
properties: {}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
});
|
|
795
|
+
|
|
748
796
|
// Add a notebook group to the File menu.
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
item
|
|
755
|
-
|
|
756
|
-
|
|
797
|
+
const fileMenu = mainMenu?.fileMenu as Menu | undefined;
|
|
798
|
+
|
|
799
|
+
const getExportMenu = (): Menu | undefined => {
|
|
800
|
+
return (
|
|
801
|
+
fileMenu?.items.find(
|
|
802
|
+
item =>
|
|
803
|
+
item.type === 'submenu' &&
|
|
804
|
+
item.submenu?.id === 'jp-mainmenu-file-notebookexport'
|
|
805
|
+
)?.submenu ?? undefined
|
|
806
|
+
);
|
|
807
|
+
};
|
|
757
808
|
|
|
758
809
|
let formatsInitialized = false;
|
|
810
|
+
let paletteInitialized = false;
|
|
811
|
+
let exportFormats: Array<{ format: string; label: string }> | null = null;
|
|
812
|
+
|
|
813
|
+
const updateExportMenu = () => {
|
|
814
|
+
const exportTo = getExportMenu();
|
|
815
|
+
if (!exportTo || !exportFormats) {
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
exportTo.clearItems();
|
|
820
|
+
|
|
821
|
+
if (exportFormats.length === 0) {
|
|
822
|
+
exportTo.addItem({
|
|
823
|
+
command: CommandIDs.showExportGuidance
|
|
824
|
+
});
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
exportFormats.forEach(({ format, label }) => {
|
|
829
|
+
exportTo.addItem({
|
|
830
|
+
command: CommandIDs.exportToFormat,
|
|
831
|
+
args: {
|
|
832
|
+
format,
|
|
833
|
+
label,
|
|
834
|
+
isPalette: false
|
|
835
|
+
}
|
|
836
|
+
});
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
if (palette && !paletteInitialized) {
|
|
840
|
+
const category = trans.__('Notebook Operations');
|
|
841
|
+
exportFormats.forEach(({ format, label }) => {
|
|
842
|
+
palette.addItem({
|
|
843
|
+
command: CommandIDs.exportToFormat,
|
|
844
|
+
category,
|
|
845
|
+
args: {
|
|
846
|
+
format,
|
|
847
|
+
label,
|
|
848
|
+
isPalette: true
|
|
849
|
+
}
|
|
850
|
+
});
|
|
851
|
+
});
|
|
852
|
+
paletteInitialized = true;
|
|
853
|
+
}
|
|
854
|
+
};
|
|
759
855
|
|
|
760
856
|
/** Request formats only when a notebook might use them. */
|
|
761
857
|
const maybeInitializeFormats = async () => {
|
|
762
858
|
if (formatsInitialized) {
|
|
859
|
+
updateExportMenu();
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
if (tracker.size === 0) {
|
|
763
864
|
return;
|
|
764
865
|
}
|
|
765
866
|
|
|
@@ -767,53 +868,99 @@ export const exportPlugin: JupyterFrontEndPlugin<void> = {
|
|
|
767
868
|
|
|
768
869
|
formatsInitialized = true;
|
|
769
870
|
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
871
|
+
let response: NbConvert.IExportFormats | null = null;
|
|
872
|
+
try {
|
|
873
|
+
response = await services.nbconvert.getExportFormats(false);
|
|
874
|
+
} catch {
|
|
875
|
+
// Ignore fetch errors and fallback to export guidance.
|
|
774
876
|
}
|
|
775
877
|
|
|
776
|
-
const formatLabels
|
|
878
|
+
const formatLabels = Private.getFormatLabels(translator);
|
|
777
879
|
|
|
778
|
-
// Convert export list to
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
}
|
|
796
|
-
if (palette) {
|
|
797
|
-
args = {
|
|
798
|
-
format: key,
|
|
799
|
-
label: labelStr,
|
|
800
|
-
isPalette: true
|
|
801
|
-
};
|
|
802
|
-
const category = trans.__('Notebook Operations');
|
|
803
|
-
palette.addItem({
|
|
804
|
-
command: CommandIDs.exportToFormat,
|
|
805
|
-
category,
|
|
806
|
-
args
|
|
807
|
-
});
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
});
|
|
880
|
+
// Convert export list to menu and palette items.
|
|
881
|
+
exportFormats = Object.keys(response ?? {})
|
|
882
|
+
.filter(key => !FORMAT_EXCLUDE.includes(key))
|
|
883
|
+
.map(key => {
|
|
884
|
+
const formattedKey = key[0].toLocaleUpperCase() + key.slice(1);
|
|
885
|
+
// Fallback for export formats the server offers but `formatLabels`
|
|
886
|
+
// does not know about, so there is no literal to extract.
|
|
887
|
+
// eslint-disable-next-line jupyter/no-dynamic-translation
|
|
888
|
+
const capCaseKey = trans.__(formattedKey);
|
|
889
|
+
const labelStr = formatLabels[key] ? formatLabels[key] : capCaseKey;
|
|
890
|
+
return {
|
|
891
|
+
format: key,
|
|
892
|
+
label: labelStr
|
|
893
|
+
};
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
updateExportMenu();
|
|
811
897
|
};
|
|
812
898
|
|
|
813
899
|
tracker.widgetAdded.connect(maybeInitializeFormats);
|
|
900
|
+
if (tracker.size > 0) {
|
|
901
|
+
void maybeInitializeFormats();
|
|
902
|
+
}
|
|
903
|
+
void app.restored.then(() => {
|
|
904
|
+
void maybeInitializeFormats();
|
|
905
|
+
});
|
|
814
906
|
}
|
|
815
907
|
};
|
|
816
908
|
|
|
909
|
+
/**
|
|
910
|
+
* A plugin providing the default notebook export guidance service.
|
|
911
|
+
*/
|
|
912
|
+
export const exportGuidanceDialogPlugin: JupyterFrontEndPlugin<INotebookExportGuidance> =
|
|
913
|
+
{
|
|
914
|
+
id: '@jupyterlab/notebook-extension:export-guidance-dialog',
|
|
915
|
+
description:
|
|
916
|
+
'Provides the default notebook export guidance shown when no exporters are available.',
|
|
917
|
+
provides: INotebookExportGuidance,
|
|
918
|
+
autoStart: true,
|
|
919
|
+
requires: [ITranslator],
|
|
920
|
+
activate: (
|
|
921
|
+
app: JupyterFrontEnd,
|
|
922
|
+
translator: ITranslator
|
|
923
|
+
): INotebookExportGuidance => {
|
|
924
|
+
const trans = translator.load('jupyterlab');
|
|
925
|
+
const { commands } = app;
|
|
926
|
+
|
|
927
|
+
const openExportDocs = (url: string, docsLabel: string) => {
|
|
928
|
+
if (commands.hasCommand('help:open')) {
|
|
929
|
+
return commands.execute('help:open', {
|
|
930
|
+
url,
|
|
931
|
+
text: docsLabel,
|
|
932
|
+
newBrowserTab: true
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
window.open(url, '_blank', 'noopener,noreferrer');
|
|
937
|
+
return undefined;
|
|
938
|
+
};
|
|
939
|
+
|
|
940
|
+
return {
|
|
941
|
+
showExportHelp: async () => {
|
|
942
|
+
const result = await showDialog({
|
|
943
|
+
title: trans.__('Notebook exports are unavailable'),
|
|
944
|
+
body: trans.__(
|
|
945
|
+
'No notebook export formats are currently available. To enable exports, install nbconvert in the server environment or use another exporter supported by your deployment.'
|
|
946
|
+
),
|
|
947
|
+
buttons: [
|
|
948
|
+
Dialog.cancelButton({ label: trans.__('Close') }),
|
|
949
|
+
Dialog.okButton({ label: trans.__('Open Documentation') })
|
|
950
|
+
]
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
if (result.button.accept) {
|
|
954
|
+
void openExportDocs(
|
|
955
|
+
NOTEBOOK_EXPORT_DOCS_URL,
|
|
956
|
+
trans.__('Notebook Export Documentation')
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
};
|
|
963
|
+
|
|
817
964
|
/**
|
|
818
965
|
* A plugin that adds a notebook trust status item to the status bar.
|
|
819
966
|
*/
|
|
@@ -1139,6 +1286,9 @@ const updateRawMimetype: JupyterFrontEndPlugin<void> = {
|
|
|
1139
1286
|
).length > 0;
|
|
1140
1287
|
if (!mimetypeExists) {
|
|
1141
1288
|
const formattedKey = key[0].toLocaleUpperCase() + key.slice(1);
|
|
1289
|
+
// Fallback for export formats the server offers but `formatLabels`
|
|
1290
|
+
// does not know about, so there is no literal to extract.
|
|
1291
|
+
// eslint-disable-next-line jupyter/no-dynamic-translation
|
|
1142
1292
|
const altOption = trans.__(formattedKey);
|
|
1143
1293
|
const option = formatLabels[key] ? formatLabels[key] : altOption;
|
|
1144
1294
|
const mimeTypeValue = response[key].output_mimetype;
|
|
@@ -1176,15 +1326,20 @@ const customMetadataEditorFields: JupyterFrontEndPlugin<void> = {
|
|
|
1176
1326
|
) => {
|
|
1177
1327
|
const editorFactory: CodeEditor.Factory = options =>
|
|
1178
1328
|
editorServices.factoryService.newInlineEditor(options);
|
|
1179
|
-
// Register the custom fields.
|
|
1329
|
+
// Register the custom fields. As with the active cell tool below, the
|
|
1330
|
+
// field renderers are used by rjsf as React components, so they run on
|
|
1331
|
+
// every rebuild of the metadata form. Each field is created once and
|
|
1332
|
+
// reused: constructing one per render abandons an editor, its host node
|
|
1333
|
+
// and its listeners on every keystroke that rebuilds the form.
|
|
1334
|
+
const cellMetadataField = new CellMetadataField({
|
|
1335
|
+
editorFactory,
|
|
1336
|
+
tracker,
|
|
1337
|
+
label: 'Cell metadata',
|
|
1338
|
+
translator: translator
|
|
1339
|
+
});
|
|
1180
1340
|
const cellComponent: IFormRenderer = {
|
|
1181
1341
|
fieldRenderer: (props: FieldProps) => {
|
|
1182
|
-
return
|
|
1183
|
-
editorFactory,
|
|
1184
|
-
tracker,
|
|
1185
|
-
label: 'Cell metadata',
|
|
1186
|
-
translator: translator
|
|
1187
|
-
}).render(props);
|
|
1342
|
+
return cellMetadataField.render(props);
|
|
1188
1343
|
}
|
|
1189
1344
|
};
|
|
1190
1345
|
formRegistry.addRenderer(
|
|
@@ -1192,14 +1347,15 @@ const customMetadataEditorFields: JupyterFrontEndPlugin<void> = {
|
|
|
1192
1347
|
cellComponent
|
|
1193
1348
|
);
|
|
1194
1349
|
|
|
1350
|
+
const notebookMetadataField = new NotebookMetadataField({
|
|
1351
|
+
editorFactory,
|
|
1352
|
+
tracker,
|
|
1353
|
+
label: 'Notebook metadata',
|
|
1354
|
+
translator: translator
|
|
1355
|
+
});
|
|
1195
1356
|
const notebookComponent: IFormRenderer = {
|
|
1196
1357
|
fieldRenderer: (props: FieldProps) => {
|
|
1197
|
-
return
|
|
1198
|
-
editorFactory,
|
|
1199
|
-
tracker,
|
|
1200
|
-
label: 'Notebook metadata',
|
|
1201
|
-
translator: translator
|
|
1202
|
-
}).render(props);
|
|
1358
|
+
return notebookMetadataField.render(props);
|
|
1203
1359
|
}
|
|
1204
1360
|
};
|
|
1205
1361
|
formRegistry.addRenderer(
|
|
@@ -1214,28 +1370,50 @@ const customMetadataEditorFields: JupyterFrontEndPlugin<void> = {
|
|
|
1214
1370
|
*/
|
|
1215
1371
|
const activeCellTool: JupyterFrontEndPlugin<void> = {
|
|
1216
1372
|
id: '@jupyterlab/notebook-extension:active-cell-tool',
|
|
1217
|
-
description: 'Adds active cell
|
|
1373
|
+
description: 'Adds active cell fields in the metadata editor tab.',
|
|
1218
1374
|
autoStart: true,
|
|
1219
1375
|
requires: [INotebookTracker, IFormRendererRegistry, IEditorLanguageRegistry],
|
|
1376
|
+
optional: [ITranslator],
|
|
1220
1377
|
activate: (
|
|
1221
1378
|
// Register the custom field.
|
|
1222
1379
|
app: JupyterFrontEnd,
|
|
1223
1380
|
tracker: INotebookTracker,
|
|
1224
1381
|
formRegistry: IFormRendererRegistry,
|
|
1225
|
-
languages: IEditorLanguageRegistry
|
|
1382
|
+
languages: IEditorLanguageRegistry,
|
|
1383
|
+
translator?: ITranslator
|
|
1226
1384
|
) => {
|
|
1385
|
+
// The field renderer is used by rjsf as a React component, so it runs on
|
|
1386
|
+
// every rebuild of the metadata form. The tool is created once and reused:
|
|
1387
|
+
// constructing one per render would rebuild the prompt and the preview from
|
|
1388
|
+
// scratch (showing them empty until the next update) and would leave a
|
|
1389
|
+
// connection to the cell model behind for every abandoned instance.
|
|
1390
|
+
const tool = new ActiveCellTool({
|
|
1391
|
+
tracker,
|
|
1392
|
+
languages
|
|
1393
|
+
});
|
|
1227
1394
|
const component: IFormRenderer = {
|
|
1228
1395
|
fieldRenderer: (props: FieldProps) => {
|
|
1229
|
-
return
|
|
1230
|
-
tracker,
|
|
1231
|
-
languages
|
|
1232
|
-
}).render(props);
|
|
1396
|
+
return tool.render(props);
|
|
1233
1397
|
}
|
|
1234
1398
|
};
|
|
1235
1399
|
formRegistry.addRenderer(
|
|
1236
1400
|
'@jupyterlab/notebook-extension:active-cell-tool.renderer',
|
|
1237
1401
|
component
|
|
1238
1402
|
);
|
|
1403
|
+
|
|
1404
|
+
const cellIdComponent: IFormRenderer = {
|
|
1405
|
+
fieldRenderer: (props: FieldProps) => {
|
|
1406
|
+
return CellIdField({
|
|
1407
|
+
...props,
|
|
1408
|
+
tracker,
|
|
1409
|
+
translator
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1412
|
+
};
|
|
1413
|
+
formRegistry.addRenderer(
|
|
1414
|
+
'@jupyterlab/notebook-extension:active-cell-tool.cell-id',
|
|
1415
|
+
cellIdComponent
|
|
1416
|
+
);
|
|
1239
1417
|
}
|
|
1240
1418
|
};
|
|
1241
1419
|
|
|
@@ -1315,6 +1493,7 @@ const plugins: JupyterFrontEndPlugin<any>[] = [
|
|
|
1315
1493
|
trackerPlugin,
|
|
1316
1494
|
pageHandlerPlugin,
|
|
1317
1495
|
executionIndicator,
|
|
1496
|
+
exportGuidanceDialogPlugin,
|
|
1318
1497
|
exportPlugin,
|
|
1319
1498
|
tools,
|
|
1320
1499
|
cellCounterItem,
|
|
@@ -1436,7 +1615,8 @@ function activatePageHandler(
|
|
|
1436
1615
|
|
|
1437
1616
|
const mimeData = data as nbformat.IMimeBundle;
|
|
1438
1617
|
const metadata = (payload['metadata'] ?? {}) as ReadonlyJSONObject;
|
|
1439
|
-
const
|
|
1618
|
+
const trusted = false;
|
|
1619
|
+
const mimeType = rendermime.preferredMimeType(mimeData, 'ensure');
|
|
1440
1620
|
if (!mimeType) {
|
|
1441
1621
|
return false;
|
|
1442
1622
|
}
|
|
@@ -1456,7 +1636,7 @@ function activatePageHandler(
|
|
|
1456
1636
|
|
|
1457
1637
|
const renderer = rendermime.createRenderer(mimeType);
|
|
1458
1638
|
void renderer.renderModel(
|
|
1459
|
-
new MimeModel({ data: mimeData, metadata, trusted
|
|
1639
|
+
new MimeModel({ data: mimeData, metadata, trusted })
|
|
1460
1640
|
);
|
|
1461
1641
|
content.addWidget(renderer);
|
|
1462
1642
|
|
|
@@ -2132,8 +2312,12 @@ function activateNotebookHandler(
|
|
|
2132
2312
|
value: settings.get('sideBySideOutputRatio').composite as number
|
|
2133
2313
|
})
|
|
2134
2314
|
.then(result => {
|
|
2135
|
-
|
|
2136
|
-
|
|
2315
|
+
if (
|
|
2316
|
+
result.value !== null &&
|
|
2317
|
+
Number.isFinite(result.value) &&
|
|
2318
|
+
result.value >= 0
|
|
2319
|
+
) {
|
|
2320
|
+
setSideBySideOutputRatio(result.value);
|
|
2137
2321
|
void settings.set('sideBySideOutputRatio', result.value);
|
|
2138
2322
|
}
|
|
2139
2323
|
})
|
|
@@ -2224,10 +2408,13 @@ function activateNotebookHandler(
|
|
|
2224
2408
|
widget.title.iconLabel = ft?.iconLabel ?? '';
|
|
2225
2409
|
widget.content.scrollbar = factory.notebookConfig.showMinimap ?? false;
|
|
2226
2410
|
|
|
2227
|
-
// Notify the widget tracker if restore data needs to update.
|
|
2411
|
+
// Notify the widget tracker if restore data needs to update. The context
|
|
2412
|
+
// outlives this panel when other views of the document stay open, so the
|
|
2413
|
+
// connection is made with the panel as receiver: `Widget.dispose()` calls
|
|
2414
|
+
// `Signal.clearData(this)`, which removes it when the panel is closed.
|
|
2228
2415
|
widget.context.pathChanged.connect(() => {
|
|
2229
2416
|
void tracker.save(widget);
|
|
2230
|
-
});
|
|
2417
|
+
}, widget);
|
|
2231
2418
|
// Add the notebook panel to the tracker.
|
|
2232
2419
|
void tracker.add(widget);
|
|
2233
2420
|
});
|
|
@@ -2306,15 +2493,16 @@ function activateNotebookHandler(
|
|
|
2306
2493
|
setSideBySideOutputRatio(factory.notebookConfig.sideBySideOutputRatio);
|
|
2307
2494
|
const sideBySideMarginStyle = `.jp-mod-sideBySide.jp-Notebook .jp-Notebook-cell {
|
|
2308
2495
|
margin-left: ${factory.notebookConfig.sideBySideLeftMarginOverride} !important;
|
|
2309
|
-
margin-right: ${factory.notebookConfig.sideBySideRightMarginOverride} !important
|
|
2496
|
+
margin-right: ${factory.notebookConfig.sideBySideRightMarginOverride} !important;
|
|
2497
|
+
}`;
|
|
2310
2498
|
const sideBySideMarginTag = document.getElementById(SIDE_BY_SIDE_STYLE_ID);
|
|
2311
2499
|
if (sideBySideMarginTag) {
|
|
2312
|
-
sideBySideMarginTag.
|
|
2500
|
+
sideBySideMarginTag.textContent = sideBySideMarginStyle;
|
|
2313
2501
|
} else {
|
|
2314
|
-
document.
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
);
|
|
2502
|
+
const style = document.createElement('style');
|
|
2503
|
+
style.id = SIDE_BY_SIDE_STYLE_ID;
|
|
2504
|
+
style.textContent = sideBySideMarginStyle;
|
|
2505
|
+
document.head.appendChild(style);
|
|
2318
2506
|
}
|
|
2319
2507
|
factory.autoStartDefault = settings.get('autoStartDefaultKernel')
|
|
2320
2508
|
.composite as boolean;
|
|
@@ -2532,6 +2720,7 @@ function activateNotebookCompleterService(
|
|
|
2532
2720
|
keys: ['Enter'],
|
|
2533
2721
|
selector: '.jp-Notebook .jp-mod-completer-active'
|
|
2534
2722
|
});
|
|
2723
|
+
const completerConnected = new WeakSet<NotebookPanel>();
|
|
2535
2724
|
const updateCompleter = async (
|
|
2536
2725
|
_: INotebookTracker | undefined,
|
|
2537
2726
|
notebook: NotebookPanel
|
|
@@ -2543,6 +2732,15 @@ function activateNotebookCompleterService(
|
|
|
2543
2732
|
sanitizer: sanitizer
|
|
2544
2733
|
};
|
|
2545
2734
|
await manager.updateCompleter(completerContext);
|
|
2735
|
+
// `updateCompleter` also runs for every panel on `activeProvidersChanged`
|
|
2736
|
+
// below; connect the listeners only on the first call for a panel so
|
|
2737
|
+
// they do not accumulate per settings change. The disposal check covers
|
|
2738
|
+
// a panel closed while `updateCompleter` was pending: connections made
|
|
2739
|
+
// then would outlive the disposal cleanup that has already run.
|
|
2740
|
+
if (notebook.isDisposed || completerConnected.has(notebook)) {
|
|
2741
|
+
return;
|
|
2742
|
+
}
|
|
2743
|
+
completerConnected.add(notebook);
|
|
2546
2744
|
notebook.content.activeCellChanged.connect((_, cell) => {
|
|
2547
2745
|
// Ensure the editor will exist on the cell before adding the completer
|
|
2548
2746
|
cell?.ready
|
|
@@ -2557,6 +2755,10 @@ function activateNotebookCompleterService(
|
|
|
2557
2755
|
})
|
|
2558
2756
|
.catch(console.error);
|
|
2559
2757
|
});
|
|
2758
|
+
// The session context outlives this panel when other views of the
|
|
2759
|
+
// document stay open, so the connection is made with the panel as
|
|
2760
|
+
// receiver: `Widget.dispose()` calls `Signal.clearData(this)`, which
|
|
2761
|
+
// removes it when the panel is closed.
|
|
2560
2762
|
notebook.sessionContext.sessionChanged.connect(() => {
|
|
2561
2763
|
// Ensure the editor will exist on the cell before adding the completer
|
|
2562
2764
|
notebook.content.activeCell?.ready
|
|
@@ -2569,7 +2771,7 @@ function activateNotebookCompleterService(
|
|
|
2569
2771
|
return manager.updateCompleter(newCompleterContext);
|
|
2570
2772
|
})
|
|
2571
2773
|
.catch(console.error);
|
|
2572
|
-
});
|
|
2774
|
+
}, notebook);
|
|
2573
2775
|
};
|
|
2574
2776
|
notebooks.widgetAdded.connect(updateCompleter);
|
|
2575
2777
|
manager.activeProvidersChanged.connect(() => {
|
|
@@ -2709,18 +2911,28 @@ function addCommands(
|
|
|
2709
2911
|
}
|
|
2710
2912
|
};
|
|
2711
2913
|
|
|
2712
|
-
// Set up signal handler to keep the collapse state consistent
|
|
2914
|
+
// Set up signal handler to keep the collapse state consistent. The
|
|
2915
|
+
// connections are made once per panel: `currentChanged` fires repeatedly
|
|
2916
|
+
// for the same panel, and reconnecting each time would pile up duplicate
|
|
2917
|
+
// handlers for as long as the panel lives.
|
|
2918
|
+
const collapseSynchronized = new WeakSet<NotebookPanel>();
|
|
2713
2919
|
tracker.currentChanged.connect(
|
|
2714
2920
|
(sender: INotebookTracker, panel: NotebookPanel) => {
|
|
2715
|
-
if (!panel?.content?.model?.cells) {
|
|
2921
|
+
if (!panel?.content?.model?.cells || collapseSynchronized.has(panel)) {
|
|
2716
2922
|
return;
|
|
2717
2923
|
}
|
|
2924
|
+
collapseSynchronized.add(panel);
|
|
2925
|
+
// The cell list belongs to the model, which outlives this view when
|
|
2926
|
+
// other views of the document stay open, so the connection is made
|
|
2927
|
+
// with the notebook as receiver: `Widget.dispose()` calls
|
|
2928
|
+
// `Signal.clearData(this)`, which removes it when the view is closed.
|
|
2718
2929
|
panel.content.model.cells.changed.connect(
|
|
2719
2930
|
(list: any, args: IObservableList.IChangedArgs<ICellModel>) => {
|
|
2720
2931
|
// Might be overkill to refresh this every time, but
|
|
2721
2932
|
// it helps to keep the collapse state consistent.
|
|
2722
2933
|
refreshCellCollapsed(panel.content);
|
|
2723
|
-
}
|
|
2934
|
+
},
|
|
2935
|
+
panel.content
|
|
2724
2936
|
);
|
|
2725
2937
|
panel.content.activeCellChanged.connect(
|
|
2726
2938
|
(notebook: Notebook, cell: Cell) => {
|
|
@@ -3077,8 +3289,14 @@ function addCommands(
|
|
|
3077
3289
|
commands.addCommand(CommandIDs.trust, {
|
|
3078
3290
|
label: () => trans.__('Trust Notebook'),
|
|
3079
3291
|
execute: async args => {
|
|
3080
|
-
const
|
|
3081
|
-
|
|
3292
|
+
const trustBoundaryId = args[CommandLinker.TRUST_BOUNDARY_ID_ARG];
|
|
3293
|
+
const current =
|
|
3294
|
+
typeof trustBoundaryId === 'string'
|
|
3295
|
+
? (tracker.find(
|
|
3296
|
+
widget => widget.content.node.id === trustBoundaryId
|
|
3297
|
+
) ?? null)
|
|
3298
|
+
: getCurrent(tracker, shell, args);
|
|
3299
|
+
if (current && !current.isDisposed) {
|
|
3082
3300
|
const { context, content } = current;
|
|
3083
3301
|
const trustResult = await NotebookActions.trust(content);
|
|
3084
3302
|
if (trustResult.trusted) {
|
|
@@ -3092,7 +3310,12 @@ function addCommands(
|
|
|
3092
3310
|
describedBy: {
|
|
3093
3311
|
args: {
|
|
3094
3312
|
type: 'object',
|
|
3095
|
-
properties: {
|
|
3313
|
+
properties: {
|
|
3314
|
+
[CommandLinker.TRUST_BOUNDARY_ID_ARG]: {
|
|
3315
|
+
type: 'string',
|
|
3316
|
+
description: trans.__('The notebook trust boundary ID')
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3096
3319
|
}
|
|
3097
3320
|
}
|
|
3098
3321
|
});
|
package/src/nboutput.ts
CHANGED
|
@@ -65,12 +65,19 @@ function activateNBOutput(
|
|
|
65
65
|
// There is overlap here since unhandled messages are also emitted in the
|
|
66
66
|
// iopubMessage signal. However, unhandled messages warrant a higher log
|
|
67
67
|
// severity, so we'll accept that they are logged twice.
|
|
68
|
+
//
|
|
69
|
+
// The session context outlives this panel when other views of the
|
|
70
|
+
// document stay open, so the connections are made with the panel as
|
|
71
|
+
// receiver: `Widget.dispose()` calls `Signal.clearData(this)`, which
|
|
72
|
+
// removes them when the panel is closed.
|
|
68
73
|
nb.context.sessionContext.iopubMessage.connect(
|
|
69
|
-
(_, msg: KernelMessage.IIOPubMessage) => logOutput(msg, 'info', 'info')
|
|
74
|
+
(_, msg: KernelMessage.IIOPubMessage) => logOutput(msg, 'info', 'info'),
|
|
75
|
+
nb
|
|
70
76
|
);
|
|
71
77
|
nb.context.sessionContext.unhandledMessage.connect(
|
|
72
78
|
(_, msg: KernelMessage.IIOPubMessage) =>
|
|
73
|
-
logOutput(msg, 'warning', 'error')
|
|
79
|
+
logOutput(msg, 'warning', 'error'),
|
|
80
|
+
nb
|
|
74
81
|
);
|
|
75
82
|
}
|
|
76
83
|
nbtracker.forEach(nb => registerNB(nb));
|