@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/lib/index.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * @module notebook-extension
7
7
  */
8
8
  import { ILabShell, ILayoutRestorer, IRouter } from '@jupyterlab/application';
9
- import { Clipboard, createToolbarFactory, Dialog, ICommandPalette, IKernelStatusModel, InputDialog, ISanitizer, ISessionContextDialogs, IToolbarWidgetRegistry, MainAreaWidget, Sanitizer, SemanticCommand, SessionContextDialogs, showDialog, Toolbar, WidgetTracker } from '@jupyterlab/apputils';
9
+ import { Clipboard, CommandLinker, createToolbarFactory, Dialog, ICommandPalette, IKernelStatusModel, InputDialog, ISanitizer, ISessionContextDialogs, IToolbarWidgetRegistry, MainAreaWidget, Sanitizer, SemanticCommand, SessionContextDialogs, showDialog, Toolbar, WidgetTracker } from '@jupyterlab/apputils';
10
10
  import { MarkdownCell } from '@jupyterlab/cells';
11
11
  import { IEditorServices, IPositionModel } from '@jupyterlab/codeeditor';
12
12
  import { compareVersions, PageConfig } from '@jupyterlab/coreutils';
@@ -38,7 +38,7 @@ import { Panel } from '@lumino/widgets';
38
38
  import { CellBarExtension } from '@jupyterlab/cell-toolbar';
39
39
  import { cellExecutor } from './cellexecutor';
40
40
  import { logNotebookOutput } from './nboutput';
41
- import { ActiveCellTool } from './tool-widgets/activeCellToolWidget';
41
+ import { ActiveCellTool, CellIdField } from './tool-widgets/activeCellToolWidget';
42
42
  import { CellMetadataField, NotebookMetadataField } from './tool-widgets/metadataEditorFields';
43
43
  /**
44
44
  * The command IDs used by the notebook plugin.
@@ -554,6 +554,9 @@ export const exportPlugin = {
554
554
  .filter(key => !FORMAT_EXCLUDE.includes(key))
555
555
  .map(key => {
556
556
  const formattedKey = key[0].toLocaleUpperCase() + key.slice(1);
557
+ // Fallback for export formats the server offers but `formatLabels`
558
+ // does not know about, so there is no literal to extract.
559
+ // eslint-disable-next-line jupyter/no-dynamic-translation
557
560
  const capCaseKey = trans.__(formattedKey);
558
561
  const labelStr = formatLabels[key] ? formatLabels[key] : capCaseKey;
559
562
  return {
@@ -864,6 +867,9 @@ const updateRawMimetype = {
864
867
  const mimetypeExists = ((_a = properties.oneOf) === null || _a === void 0 ? void 0 : _a.filter(value => value.const === key).length) > 0;
865
868
  if (!mimetypeExists) {
866
869
  const formattedKey = key[0].toLocaleUpperCase() + key.slice(1);
870
+ // Fallback for export formats the server offers but `formatLabels`
871
+ // does not know about, so there is no literal to extract.
872
+ // eslint-disable-next-line jupyter/no-dynamic-translation
867
873
  const altOption = trans.__(formattedKey);
868
874
  const option = formatLabels[key] ? formatLabels[key] : altOption;
869
875
  const mimeTypeValue = response[key].output_mimetype;
@@ -891,26 +897,32 @@ const customMetadataEditorFields = {
891
897
  optional: [ITranslator],
892
898
  activate: (app, tracker, editorServices, formRegistry, translator) => {
893
899
  const editorFactory = options => editorServices.factoryService.newInlineEditor(options);
894
- // Register the custom fields.
900
+ // Register the custom fields. As with the active cell tool below, the
901
+ // field renderers are used by rjsf as React components, so they run on
902
+ // every rebuild of the metadata form. Each field is created once and
903
+ // reused: constructing one per render abandons an editor, its host node
904
+ // and its listeners on every keystroke that rebuilds the form.
905
+ const cellMetadataField = new CellMetadataField({
906
+ editorFactory,
907
+ tracker,
908
+ label: 'Cell metadata',
909
+ translator: translator
910
+ });
895
911
  const cellComponent = {
896
912
  fieldRenderer: (props) => {
897
- return new CellMetadataField({
898
- editorFactory,
899
- tracker,
900
- label: 'Cell metadata',
901
- translator: translator
902
- }).render(props);
913
+ return cellMetadataField.render(props);
903
914
  }
904
915
  };
905
916
  formRegistry.addRenderer('@jupyterlab/notebook-extension:metadata-editor.cell-metadata', cellComponent);
917
+ const notebookMetadataField = new NotebookMetadataField({
918
+ editorFactory,
919
+ tracker,
920
+ label: 'Notebook metadata',
921
+ translator: translator
922
+ });
906
923
  const notebookComponent = {
907
924
  fieldRenderer: (props) => {
908
- return new NotebookMetadataField({
909
- editorFactory,
910
- tracker,
911
- label: 'Notebook metadata',
912
- translator: translator
913
- }).render(props);
925
+ return notebookMetadataField.render(props);
914
926
  }
915
927
  };
916
928
  formRegistry.addRenderer('@jupyterlab/notebook-extension:metadata-editor.notebook-metadata', notebookComponent);
@@ -921,21 +933,38 @@ const customMetadataEditorFields = {
921
933
  */
922
934
  const activeCellTool = {
923
935
  id: '@jupyterlab/notebook-extension:active-cell-tool',
924
- description: 'Adds active cell field in the metadata editor tab.',
936
+ description: 'Adds active cell fields in the metadata editor tab.',
925
937
  autoStart: true,
926
938
  requires: [INotebookTracker, IFormRendererRegistry, IEditorLanguageRegistry],
939
+ optional: [ITranslator],
927
940
  activate: (
928
941
  // Register the custom field.
929
- app, tracker, formRegistry, languages) => {
942
+ app, tracker, formRegistry, languages, translator) => {
943
+ // The field renderer is used by rjsf as a React component, so it runs on
944
+ // every rebuild of the metadata form. The tool is created once and reused:
945
+ // constructing one per render would rebuild the prompt and the preview from
946
+ // scratch (showing them empty until the next update) and would leave a
947
+ // connection to the cell model behind for every abandoned instance.
948
+ const tool = new ActiveCellTool({
949
+ tracker,
950
+ languages
951
+ });
930
952
  const component = {
931
953
  fieldRenderer: (props) => {
932
- return new ActiveCellTool({
933
- tracker,
934
- languages
935
- }).render(props);
954
+ return tool.render(props);
936
955
  }
937
956
  };
938
957
  formRegistry.addRenderer('@jupyterlab/notebook-extension:active-cell-tool.renderer', component);
958
+ const cellIdComponent = {
959
+ fieldRenderer: (props) => {
960
+ return CellIdField({
961
+ ...props,
962
+ tracker,
963
+ translator
964
+ });
965
+ }
966
+ };
967
+ formRegistry.addRenderer('@jupyterlab/notebook-extension:active-cell-tool.cell-id', cellIdComponent);
939
968
  }
940
969
  };
941
970
  /**
@@ -1097,7 +1126,8 @@ function activatePageHandler(app, rendermime, settingRegistry, translator_) {
1097
1126
  }
1098
1127
  const mimeData = data;
1099
1128
  const metadata = ((_a = payload['metadata']) !== null && _a !== void 0 ? _a : {});
1100
- const mimeType = rendermime.preferredMimeType(mimeData, 'any');
1129
+ const trusted = false;
1130
+ const mimeType = rendermime.preferredMimeType(mimeData, 'ensure');
1101
1131
  if (!mimeType) {
1102
1132
  return false;
1103
1133
  }
@@ -1113,7 +1143,7 @@ function activatePageHandler(app, rendermime, settingRegistry, translator_) {
1113
1143
  const content = helpWidget.content;
1114
1144
  content.widgets.forEach(widget => widget.dispose());
1115
1145
  const renderer = rendermime.createRenderer(mimeType);
1116
- void renderer.renderModel(new MimeModel({ data: mimeData, metadata, trusted: true }));
1146
+ void renderer.renderModel(new MimeModel({ data: mimeData, metadata, trusted }));
1117
1147
  content.addWidget(renderer);
1118
1148
  if (!helpWidget.isAttached) {
1119
1149
  app.shell.add(helpWidget, 'down');
@@ -1612,8 +1642,10 @@ function activateNotebookHandler(app, factory, extensions, executor, palette, de
1612
1642
  value: settings.get('sideBySideOutputRatio').composite
1613
1643
  })
1614
1644
  .then(result => {
1615
- setSideBySideOutputRatio(result.value);
1616
- if (result.value) {
1645
+ if (result.value !== null &&
1646
+ Number.isFinite(result.value) &&
1647
+ result.value >= 0) {
1648
+ setSideBySideOutputRatio(result.value);
1617
1649
  void settings.set('sideBySideOutputRatio', result.value);
1618
1650
  }
1619
1651
  })
@@ -1676,10 +1708,13 @@ function activateNotebookHandler(app, factory, extensions, executor, palette, de
1676
1708
  widget.title.iconClass = (_a = ft === null || ft === void 0 ? void 0 : ft.iconClass) !== null && _a !== void 0 ? _a : '';
1677
1709
  widget.title.iconLabel = (_b = ft === null || ft === void 0 ? void 0 : ft.iconLabel) !== null && _b !== void 0 ? _b : '';
1678
1710
  widget.content.scrollbar = (_c = factory.notebookConfig.showMinimap) !== null && _c !== void 0 ? _c : false;
1679
- // Notify the widget tracker if restore data needs to update.
1711
+ // Notify the widget tracker if restore data needs to update. The context
1712
+ // outlives this panel when other views of the document stay open, so the
1713
+ // connection is made with the panel as receiver: `Widget.dispose()` calls
1714
+ // `Signal.clearData(this)`, which removes it when the panel is closed.
1680
1715
  widget.context.pathChanged.connect(() => {
1681
1716
  void tracker.save(widget);
1682
- });
1717
+ }, widget);
1683
1718
  // Add the notebook panel to the tracker.
1684
1719
  void tracker.add(widget);
1685
1720
  });
@@ -1932,6 +1967,7 @@ function activateNotebookCompleterService(app, notebooks, manager, translator, a
1932
1967
  keys: ['Enter'],
1933
1968
  selector: '.jp-Notebook .jp-mod-completer-active'
1934
1969
  });
1970
+ const completerConnected = new WeakSet();
1935
1971
  const updateCompleter = async (_, notebook) => {
1936
1972
  var _a, _b;
1937
1973
  const completerContext = {
@@ -1941,6 +1977,15 @@ function activateNotebookCompleterService(app, notebooks, manager, translator, a
1941
1977
  sanitizer: sanitizer
1942
1978
  };
1943
1979
  await manager.updateCompleter(completerContext);
1980
+ // `updateCompleter` also runs for every panel on `activeProvidersChanged`
1981
+ // below; connect the listeners only on the first call for a panel so
1982
+ // they do not accumulate per settings change. The disposal check covers
1983
+ // a panel closed while `updateCompleter` was pending: connections made
1984
+ // then would outlive the disposal cleanup that has already run.
1985
+ if (notebook.isDisposed || completerConnected.has(notebook)) {
1986
+ return;
1987
+ }
1988
+ completerConnected.add(notebook);
1944
1989
  notebook.content.activeCellChanged.connect((_, cell) => {
1945
1990
  // Ensure the editor will exist on the cell before adding the completer
1946
1991
  cell === null || cell === void 0 ? void 0 : cell.ready.then(() => {
@@ -1953,6 +1998,10 @@ function activateNotebookCompleterService(app, notebooks, manager, translator, a
1953
1998
  return manager.updateCompleter(newCompleterContext);
1954
1999
  }).catch(console.error);
1955
2000
  });
2001
+ // The session context outlives this panel when other views of the
2002
+ // document stay open, so the connection is made with the panel as
2003
+ // receiver: `Widget.dispose()` calls `Signal.clearData(this)`, which
2004
+ // removes it when the panel is closed.
1956
2005
  notebook.sessionContext.sessionChanged.connect(() => {
1957
2006
  var _a;
1958
2007
  // Ensure the editor will exist on the cell before adding the completer
@@ -1965,7 +2014,7 @@ function activateNotebookCompleterService(app, notebooks, manager, translator, a
1965
2014
  };
1966
2015
  return manager.updateCompleter(newCompleterContext);
1967
2016
  }).catch(console.error);
1968
- });
2017
+ }, notebook);
1969
2018
  };
1970
2019
  notebooks.widgetAdded.connect(updateCompleter);
1971
2020
  manager.activeProvidersChanged.connect(() => {
@@ -2073,17 +2122,26 @@ function addCommands(app, tracker, translator, sessionDialogs, settings, isEnabl
2073
2122
  NotebookActions.paste(notebook, mode, { stripOutputs });
2074
2123
  }
2075
2124
  };
2076
- // Set up signal handler to keep the collapse state consistent
2125
+ // Set up signal handler to keep the collapse state consistent. The
2126
+ // connections are made once per panel: `currentChanged` fires repeatedly
2127
+ // for the same panel, and reconnecting each time would pile up duplicate
2128
+ // handlers for as long as the panel lives.
2129
+ const collapseSynchronized = new WeakSet();
2077
2130
  tracker.currentChanged.connect((sender, panel) => {
2078
2131
  var _a, _b;
2079
- if (!((_b = (_a = panel === null || panel === void 0 ? void 0 : panel.content) === null || _a === void 0 ? void 0 : _a.model) === null || _b === void 0 ? void 0 : _b.cells)) {
2132
+ if (!((_b = (_a = panel === null || panel === void 0 ? void 0 : panel.content) === null || _a === void 0 ? void 0 : _a.model) === null || _b === void 0 ? void 0 : _b.cells) || collapseSynchronized.has(panel)) {
2080
2133
  return;
2081
2134
  }
2135
+ collapseSynchronized.add(panel);
2136
+ // The cell list belongs to the model, which outlives this view when
2137
+ // other views of the document stay open, so the connection is made
2138
+ // with the notebook as receiver: `Widget.dispose()` calls
2139
+ // `Signal.clearData(this)`, which removes it when the view is closed.
2082
2140
  panel.content.model.cells.changed.connect((list, args) => {
2083
2141
  // Might be overkill to refresh this every time, but
2084
2142
  // it helps to keep the collapse state consistent.
2085
2143
  refreshCellCollapsed(panel.content);
2086
- });
2144
+ }, panel.content);
2087
2145
  panel.content.activeCellChanged.connect((notebook, cell) => {
2088
2146
  NotebookActions.expandParent(cell, notebook);
2089
2147
  });
@@ -2363,8 +2421,12 @@ function addCommands(app, tracker, translator, sessionDialogs, settings, isEnabl
2363
2421
  commands.addCommand(CommandIDs.trust, {
2364
2422
  label: () => trans.__('Trust Notebook'),
2365
2423
  execute: async (args) => {
2366
- const current = getCurrent(tracker, shell, args);
2367
- if (current) {
2424
+ var _a;
2425
+ const trustBoundaryId = args[CommandLinker.TRUST_BOUNDARY_ID_ARG];
2426
+ const current = typeof trustBoundaryId === 'string'
2427
+ ? ((_a = tracker.find(widget => widget.content.node.id === trustBoundaryId)) !== null && _a !== void 0 ? _a : null)
2428
+ : getCurrent(tracker, shell, args);
2429
+ if (current && !current.isDisposed) {
2368
2430
  const { context, content } = current;
2369
2431
  const trustResult = await NotebookActions.trust(content);
2370
2432
  if (trustResult.trusted) {
@@ -2378,7 +2440,12 @@ function addCommands(app, tracker, translator, sessionDialogs, settings, isEnabl
2378
2440
  describedBy: {
2379
2441
  args: {
2380
2442
  type: 'object',
2381
- properties: {}
2443
+ properties: {
2444
+ [CommandLinker.TRUST_BOUNDARY_ID_ARG]: {
2445
+ type: 'string',
2446
+ description: trans.__('The notebook trust boundary ID')
2447
+ }
2448
+ }
2382
2449
  }
2383
2450
  }
2384
2451
  });