@jupyterlab/notebook-extension 4.7.0-alpha.1 → 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/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,
@@ -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 { ActiveCellTool } from './tool-widgets/activeCellToolWidget';
149
+ import {
150
+ ActiveCellTool,
151
+ CellIdField
152
+ } from './tool-widgets/activeCellToolWidget';
149
153
  import {
150
154
  CellMetadataField,
151
155
  NotebookMetadataField
@@ -878,6 +882,9 @@ export const exportPlugin: JupyterFrontEndPlugin<void> = {
878
882
  .filter(key => !FORMAT_EXCLUDE.includes(key))
879
883
  .map(key => {
880
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
881
888
  const capCaseKey = trans.__(formattedKey);
882
889
  const labelStr = formatLabels[key] ? formatLabels[key] : capCaseKey;
883
890
  return {
@@ -1279,6 +1286,9 @@ const updateRawMimetype: JupyterFrontEndPlugin<void> = {
1279
1286
  ).length > 0;
1280
1287
  if (!mimetypeExists) {
1281
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
1282
1292
  const altOption = trans.__(formattedKey);
1283
1293
  const option = formatLabels[key] ? formatLabels[key] : altOption;
1284
1294
  const mimeTypeValue = response[key].output_mimetype;
@@ -1316,15 +1326,20 @@ const customMetadataEditorFields: JupyterFrontEndPlugin<void> = {
1316
1326
  ) => {
1317
1327
  const editorFactory: CodeEditor.Factory = options =>
1318
1328
  editorServices.factoryService.newInlineEditor(options);
1319
- // 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
+ });
1320
1340
  const cellComponent: IFormRenderer = {
1321
1341
  fieldRenderer: (props: FieldProps) => {
1322
- return new CellMetadataField({
1323
- editorFactory,
1324
- tracker,
1325
- label: 'Cell metadata',
1326
- translator: translator
1327
- }).render(props);
1342
+ return cellMetadataField.render(props);
1328
1343
  }
1329
1344
  };
1330
1345
  formRegistry.addRenderer(
@@ -1332,14 +1347,15 @@ const customMetadataEditorFields: JupyterFrontEndPlugin<void> = {
1332
1347
  cellComponent
1333
1348
  );
1334
1349
 
1350
+ const notebookMetadataField = new NotebookMetadataField({
1351
+ editorFactory,
1352
+ tracker,
1353
+ label: 'Notebook metadata',
1354
+ translator: translator
1355
+ });
1335
1356
  const notebookComponent: IFormRenderer = {
1336
1357
  fieldRenderer: (props: FieldProps) => {
1337
- return new NotebookMetadataField({
1338
- editorFactory,
1339
- tracker,
1340
- label: 'Notebook metadata',
1341
- translator: translator
1342
- }).render(props);
1358
+ return notebookMetadataField.render(props);
1343
1359
  }
1344
1360
  };
1345
1361
  formRegistry.addRenderer(
@@ -1354,28 +1370,50 @@ const customMetadataEditorFields: JupyterFrontEndPlugin<void> = {
1354
1370
  */
1355
1371
  const activeCellTool: JupyterFrontEndPlugin<void> = {
1356
1372
  id: '@jupyterlab/notebook-extension:active-cell-tool',
1357
- description: 'Adds active cell field in the metadata editor tab.',
1373
+ description: 'Adds active cell fields in the metadata editor tab.',
1358
1374
  autoStart: true,
1359
1375
  requires: [INotebookTracker, IFormRendererRegistry, IEditorLanguageRegistry],
1376
+ optional: [ITranslator],
1360
1377
  activate: (
1361
1378
  // Register the custom field.
1362
1379
  app: JupyterFrontEnd,
1363
1380
  tracker: INotebookTracker,
1364
1381
  formRegistry: IFormRendererRegistry,
1365
- languages: IEditorLanguageRegistry
1382
+ languages: IEditorLanguageRegistry,
1383
+ translator?: ITranslator
1366
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
+ });
1367
1394
  const component: IFormRenderer = {
1368
1395
  fieldRenderer: (props: FieldProps) => {
1369
- return new ActiveCellTool({
1370
- tracker,
1371
- languages
1372
- }).render(props);
1396
+ return tool.render(props);
1373
1397
  }
1374
1398
  };
1375
1399
  formRegistry.addRenderer(
1376
1400
  '@jupyterlab/notebook-extension:active-cell-tool.renderer',
1377
1401
  component
1378
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
+ );
1379
1417
  }
1380
1418
  };
1381
1419
 
@@ -1577,7 +1615,8 @@ function activatePageHandler(
1577
1615
 
1578
1616
  const mimeData = data as nbformat.IMimeBundle;
1579
1617
  const metadata = (payload['metadata'] ?? {}) as ReadonlyJSONObject;
1580
- const mimeType = rendermime.preferredMimeType(mimeData, 'any');
1618
+ const trusted = false;
1619
+ const mimeType = rendermime.preferredMimeType(mimeData, 'ensure');
1581
1620
  if (!mimeType) {
1582
1621
  return false;
1583
1622
  }
@@ -1597,7 +1636,7 @@ function activatePageHandler(
1597
1636
 
1598
1637
  const renderer = rendermime.createRenderer(mimeType);
1599
1638
  void renderer.renderModel(
1600
- new MimeModel({ data: mimeData, metadata, trusted: true })
1639
+ new MimeModel({ data: mimeData, metadata, trusted })
1601
1640
  );
1602
1641
  content.addWidget(renderer);
1603
1642
 
@@ -2273,8 +2312,12 @@ function activateNotebookHandler(
2273
2312
  value: settings.get('sideBySideOutputRatio').composite as number
2274
2313
  })
2275
2314
  .then(result => {
2276
- setSideBySideOutputRatio(result.value!);
2277
- if (result.value) {
2315
+ if (
2316
+ result.value !== null &&
2317
+ Number.isFinite(result.value) &&
2318
+ result.value >= 0
2319
+ ) {
2320
+ setSideBySideOutputRatio(result.value);
2278
2321
  void settings.set('sideBySideOutputRatio', result.value);
2279
2322
  }
2280
2323
  })
@@ -2365,10 +2408,13 @@ function activateNotebookHandler(
2365
2408
  widget.title.iconLabel = ft?.iconLabel ?? '';
2366
2409
  widget.content.scrollbar = factory.notebookConfig.showMinimap ?? false;
2367
2410
 
2368
- // 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.
2369
2415
  widget.context.pathChanged.connect(() => {
2370
2416
  void tracker.save(widget);
2371
- });
2417
+ }, widget);
2372
2418
  // Add the notebook panel to the tracker.
2373
2419
  void tracker.add(widget);
2374
2420
  });
@@ -2674,6 +2720,7 @@ function activateNotebookCompleterService(
2674
2720
  keys: ['Enter'],
2675
2721
  selector: '.jp-Notebook .jp-mod-completer-active'
2676
2722
  });
2723
+ const completerConnected = new WeakSet<NotebookPanel>();
2677
2724
  const updateCompleter = async (
2678
2725
  _: INotebookTracker | undefined,
2679
2726
  notebook: NotebookPanel
@@ -2685,6 +2732,15 @@ function activateNotebookCompleterService(
2685
2732
  sanitizer: sanitizer
2686
2733
  };
2687
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);
2688
2744
  notebook.content.activeCellChanged.connect((_, cell) => {
2689
2745
  // Ensure the editor will exist on the cell before adding the completer
2690
2746
  cell?.ready
@@ -2699,6 +2755,10 @@ function activateNotebookCompleterService(
2699
2755
  })
2700
2756
  .catch(console.error);
2701
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.
2702
2762
  notebook.sessionContext.sessionChanged.connect(() => {
2703
2763
  // Ensure the editor will exist on the cell before adding the completer
2704
2764
  notebook.content.activeCell?.ready
@@ -2711,7 +2771,7 @@ function activateNotebookCompleterService(
2711
2771
  return manager.updateCompleter(newCompleterContext);
2712
2772
  })
2713
2773
  .catch(console.error);
2714
- });
2774
+ }, notebook);
2715
2775
  };
2716
2776
  notebooks.widgetAdded.connect(updateCompleter);
2717
2777
  manager.activeProvidersChanged.connect(() => {
@@ -2851,18 +2911,28 @@ function addCommands(
2851
2911
  }
2852
2912
  };
2853
2913
 
2854
- // 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>();
2855
2919
  tracker.currentChanged.connect(
2856
2920
  (sender: INotebookTracker, panel: NotebookPanel) => {
2857
- if (!panel?.content?.model?.cells) {
2921
+ if (!panel?.content?.model?.cells || collapseSynchronized.has(panel)) {
2858
2922
  return;
2859
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.
2860
2929
  panel.content.model.cells.changed.connect(
2861
2930
  (list: any, args: IObservableList.IChangedArgs<ICellModel>) => {
2862
2931
  // Might be overkill to refresh this every time, but
2863
2932
  // it helps to keep the collapse state consistent.
2864
2933
  refreshCellCollapsed(panel.content);
2865
- }
2934
+ },
2935
+ panel.content
2866
2936
  );
2867
2937
  panel.content.activeCellChanged.connect(
2868
2938
  (notebook: Notebook, cell: Cell) => {
@@ -3219,8 +3289,14 @@ function addCommands(
3219
3289
  commands.addCommand(CommandIDs.trust, {
3220
3290
  label: () => trans.__('Trust Notebook'),
3221
3291
  execute: async args => {
3222
- const current = getCurrent(tracker, shell, args);
3223
- if (current) {
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) {
3224
3300
  const { context, content } = current;
3225
3301
  const trustResult = await NotebookActions.trust(content);
3226
3302
  if (trustResult.trusted) {
@@ -3234,7 +3310,12 @@ function addCommands(
3234
3310
  describedBy: {
3235
3311
  args: {
3236
3312
  type: 'object',
3237
- properties: {}
3313
+ properties: {
3314
+ [CommandLinker.TRUST_BOUNDARY_ID_ARG]: {
3315
+ type: 'string',
3316
+ description: trans.__('The notebook trust boundary ID')
3317
+ }
3318
+ }
3238
3319
  }
3239
3320
  }
3240
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));