@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 CHANGED
@@ -3,6 +3,20 @@
3
3
  * @module notebook-extension
4
4
  */
5
5
  import type { JupyterFrontEndPlugin } from '@jupyterlab/application';
6
+ import { Token } from '@lumino/coreutils';
7
+ /**
8
+ * An interface describing how export guidance is presented to users.
9
+ */
10
+ export interface INotebookExportGuidance {
11
+ /**
12
+ * Show guidance for enabling notebook exports.
13
+ */
14
+ showExportHelp(): Promise<void>;
15
+ }
16
+ /**
17
+ * A token describing a service for showing notebook export guidance.
18
+ */
19
+ export declare const INotebookExportGuidance: Token<INotebookExportGuidance>;
6
20
  /**
7
21
  * A plugin providing a CommandEdit status item.
8
22
  */
@@ -19,6 +33,10 @@ export declare const executionIndicator: JupyterFrontEndPlugin<void>;
19
33
  * A plugin providing export commands in the main menu and command palette
20
34
  */
21
35
  export declare const exportPlugin: JupyterFrontEndPlugin<void>;
36
+ /**
37
+ * A plugin providing the default notebook export guidance service.
38
+ */
39
+ export declare const exportGuidanceDialogPlugin: JupyterFrontEndPlugin<INotebookExportGuidance>;
22
40
  /**
23
41
  * A plugin that adds a notebook trust status item to the status bar.
24
42
  */
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';
@@ -31,14 +31,14 @@ import { ITableOfContentsRegistry } from '@jupyterlab/toc';
31
31
  import { ITranslator, nullTranslator } from '@jupyterlab/translation';
32
32
  import { addAboveIcon, addBelowIcon, buildIcon, copyIcon, cutIcon, duplicateIcon, fastForwardIcon, IFormRendererRegistry, infoIcon, moveDownIcon, moveUpIcon, notebookIcon, pasteIcon, refreshIcon, runIcon, stopIcon } from '@jupyterlab/ui-components';
33
33
  import { ArrayExt } from '@lumino/algorithm';
34
- import { JSONExt, UUID } from '@lumino/coreutils';
34
+ import { JSONExt, Token, UUID } from '@lumino/coreutils';
35
35
  import { DisposableSet } from '@lumino/disposable';
36
36
  import { MessageLoop } from '@lumino/messaging';
37
37
  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.
@@ -62,6 +62,7 @@ var CommandIDs;
62
62
  CommandIDs.closeAndShutdown = 'notebook:close-and-shutdown';
63
63
  CommandIDs.trust = 'notebook:trust';
64
64
  CommandIDs.exportToFormat = 'notebook:export-to-format';
65
+ CommandIDs.showExportGuidance = 'notebook:show-export-guidance';
65
66
  CommandIDs.run = 'notebook:run-cell';
66
67
  CommandIDs.runAndAdvance = 'notebook:run-cell-and-select-next';
67
68
  CommandIDs.runAndInsert = 'notebook:run-cell-and-insert-below';
@@ -156,6 +157,14 @@ const FACTORY = 'Notebook';
156
157
  * (returned from nbconvert's export list)
157
158
  */
158
159
  const FORMAT_EXCLUDE = ['notebook', 'python', 'custom'];
160
+ /**
161
+ * Documentation page describing notebook export support.
162
+ */
163
+ const NOTEBOOK_EXPORT_DOCS_URL = 'https://jupyterlab.readthedocs.io/en/stable/user/export.html';
164
+ /**
165
+ * A token describing a service for showing notebook export guidance.
166
+ */
167
+ export const INotebookExportGuidance = new Token('@jupyterlab/notebook-extension:INotebookExportGuidance', 'A service for showing notebook export guidance.');
159
168
  /**
160
169
  * Setting Id storing the customized toolbar definition.
161
170
  */
@@ -378,9 +387,8 @@ export const exportPlugin = {
378
387
  description: 'Adds the export notebook commands.',
379
388
  autoStart: true,
380
389
  requires: [ITranslator, INotebookTracker],
381
- optional: [IMainMenu, ICommandPalette],
382
- activate: (app, translator, tracker, mainMenu, palette) => {
383
- var _a;
390
+ optional: [IMainMenu, ICommandPalette, INotebookExportGuidance],
391
+ activate: (app, translator, tracker, mainMenu, palette, exportGuidance) => {
384
392
  const trans = translator.load('jupyterlab');
385
393
  const { commands, shell } = app;
386
394
  const services = app.serviceManager;
@@ -455,63 +463,156 @@ export const exportPlugin = {
455
463
  }
456
464
  }
457
465
  });
466
+ commands.addCommand(CommandIDs.showExportGuidance, {
467
+ label: trans.__('Enable notebook exports'),
468
+ execute: () => {
469
+ if (exportGuidance) {
470
+ return exportGuidance.showExportHelp();
471
+ }
472
+ window.open(NOTEBOOK_EXPORT_DOCS_URL, '_blank', 'noopener,noreferrer');
473
+ return undefined;
474
+ },
475
+ describedBy: {
476
+ args: {
477
+ type: 'object',
478
+ properties: {}
479
+ }
480
+ }
481
+ });
458
482
  // Add a notebook group to the File menu.
459
- let exportTo;
460
- if (mainMenu) {
461
- exportTo = (_a = mainMenu.fileMenu.items.find(item => {
483
+ const fileMenu = mainMenu === null || mainMenu === void 0 ? void 0 : mainMenu.fileMenu;
484
+ const getExportMenu = () => {
485
+ var _a, _b;
486
+ return ((_b = (_a = fileMenu === null || fileMenu === void 0 ? void 0 : fileMenu.items.find(item => {
462
487
  var _a;
463
488
  return item.type === 'submenu' &&
464
489
  ((_a = item.submenu) === null || _a === void 0 ? void 0 : _a.id) === 'jp-mainmenu-file-notebookexport';
465
- })) === null || _a === void 0 ? void 0 : _a.submenu;
466
- }
490
+ })) === null || _a === void 0 ? void 0 : _a.submenu) !== null && _b !== void 0 ? _b : undefined);
491
+ };
467
492
  let formatsInitialized = false;
493
+ let paletteInitialized = false;
494
+ let exportFormats = null;
495
+ const updateExportMenu = () => {
496
+ const exportTo = getExportMenu();
497
+ if (!exportTo || !exportFormats) {
498
+ return;
499
+ }
500
+ exportTo.clearItems();
501
+ if (exportFormats.length === 0) {
502
+ exportTo.addItem({
503
+ command: CommandIDs.showExportGuidance
504
+ });
505
+ return;
506
+ }
507
+ exportFormats.forEach(({ format, label }) => {
508
+ exportTo.addItem({
509
+ command: CommandIDs.exportToFormat,
510
+ args: {
511
+ format,
512
+ label,
513
+ isPalette: false
514
+ }
515
+ });
516
+ });
517
+ if (palette && !paletteInitialized) {
518
+ const category = trans.__('Notebook Operations');
519
+ exportFormats.forEach(({ format, label }) => {
520
+ palette.addItem({
521
+ command: CommandIDs.exportToFormat,
522
+ category,
523
+ args: {
524
+ format,
525
+ label,
526
+ isPalette: true
527
+ }
528
+ });
529
+ });
530
+ paletteInitialized = true;
531
+ }
532
+ };
468
533
  /** Request formats only when a notebook might use them. */
469
534
  const maybeInitializeFormats = async () => {
470
535
  if (formatsInitialized) {
536
+ updateExportMenu();
537
+ return;
538
+ }
539
+ if (tracker.size === 0) {
471
540
  return;
472
541
  }
473
542
  tracker.widgetAdded.disconnect(maybeInitializeFormats);
474
543
  formatsInitialized = true;
475
- const response = await services.nbconvert.getExportFormats(false);
476
- if (!response) {
477
- return;
544
+ let response = null;
545
+ try {
546
+ response = await services.nbconvert.getExportFormats(false);
547
+ }
548
+ catch (_a) {
549
+ // Ignore fetch errors and fallback to export guidance.
478
550
  }
479
551
  const formatLabels = Private.getFormatLabels(translator);
480
- // Convert export list to palette and menu items.
481
- const formatList = Object.keys(response);
482
- formatList.forEach(function (key) {
552
+ // Convert export list to menu and palette items.
553
+ exportFormats = Object.keys(response !== null && response !== void 0 ? response : {})
554
+ .filter(key => !FORMAT_EXCLUDE.includes(key))
555
+ .map(key => {
483
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
484
560
  const capCaseKey = trans.__(formattedKey);
485
561
  const labelStr = formatLabels[key] ? formatLabels[key] : capCaseKey;
486
- let args = {
562
+ return {
487
563
  format: key,
488
- label: labelStr,
489
- isPalette: false
564
+ label: labelStr
490
565
  };
491
- if (FORMAT_EXCLUDE.indexOf(key) === -1) {
492
- if (exportTo) {
493
- exportTo.addItem({
494
- command: CommandIDs.exportToFormat,
495
- args: args
496
- });
497
- }
498
- if (palette) {
499
- args = {
500
- format: key,
501
- label: labelStr,
502
- isPalette: true
503
- };
504
- const category = trans.__('Notebook Operations');
505
- palette.addItem({
506
- command: CommandIDs.exportToFormat,
507
- category,
508
- args
509
- });
510
- }
511
- }
512
566
  });
567
+ updateExportMenu();
513
568
  };
514
569
  tracker.widgetAdded.connect(maybeInitializeFormats);
570
+ if (tracker.size > 0) {
571
+ void maybeInitializeFormats();
572
+ }
573
+ void app.restored.then(() => {
574
+ void maybeInitializeFormats();
575
+ });
576
+ }
577
+ };
578
+ /**
579
+ * A plugin providing the default notebook export guidance service.
580
+ */
581
+ export const exportGuidanceDialogPlugin = {
582
+ id: '@jupyterlab/notebook-extension:export-guidance-dialog',
583
+ description: 'Provides the default notebook export guidance shown when no exporters are available.',
584
+ provides: INotebookExportGuidance,
585
+ autoStart: true,
586
+ requires: [ITranslator],
587
+ activate: (app, translator) => {
588
+ const trans = translator.load('jupyterlab');
589
+ const { commands } = app;
590
+ const openExportDocs = (url, docsLabel) => {
591
+ if (commands.hasCommand('help:open')) {
592
+ return commands.execute('help:open', {
593
+ url,
594
+ text: docsLabel,
595
+ newBrowserTab: true
596
+ });
597
+ }
598
+ window.open(url, '_blank', 'noopener,noreferrer');
599
+ return undefined;
600
+ };
601
+ return {
602
+ showExportHelp: async () => {
603
+ const result = await showDialog({
604
+ title: trans.__('Notebook exports are unavailable'),
605
+ body: trans.__('No notebook export formats are currently available. To enable exports, install nbconvert in the server environment or use another exporter supported by your deployment.'),
606
+ buttons: [
607
+ Dialog.cancelButton({ label: trans.__('Close') }),
608
+ Dialog.okButton({ label: trans.__('Open Documentation') })
609
+ ]
610
+ });
611
+ if (result.button.accept) {
612
+ void openExportDocs(NOTEBOOK_EXPORT_DOCS_URL, trans.__('Notebook Export Documentation'));
613
+ }
614
+ }
615
+ };
515
616
  }
516
617
  };
517
618
  /**
@@ -766,6 +867,9 @@ const updateRawMimetype = {
766
867
  const mimetypeExists = ((_a = properties.oneOf) === null || _a === void 0 ? void 0 : _a.filter(value => value.const === key).length) > 0;
767
868
  if (!mimetypeExists) {
768
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
769
873
  const altOption = trans.__(formattedKey);
770
874
  const option = formatLabels[key] ? formatLabels[key] : altOption;
771
875
  const mimeTypeValue = response[key].output_mimetype;
@@ -793,26 +897,32 @@ const customMetadataEditorFields = {
793
897
  optional: [ITranslator],
794
898
  activate: (app, tracker, editorServices, formRegistry, translator) => {
795
899
  const editorFactory = options => editorServices.factoryService.newInlineEditor(options);
796
- // 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
+ });
797
911
  const cellComponent = {
798
912
  fieldRenderer: (props) => {
799
- return new CellMetadataField({
800
- editorFactory,
801
- tracker,
802
- label: 'Cell metadata',
803
- translator: translator
804
- }).render(props);
913
+ return cellMetadataField.render(props);
805
914
  }
806
915
  };
807
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
+ });
808
923
  const notebookComponent = {
809
924
  fieldRenderer: (props) => {
810
- return new NotebookMetadataField({
811
- editorFactory,
812
- tracker,
813
- label: 'Notebook metadata',
814
- translator: translator
815
- }).render(props);
925
+ return notebookMetadataField.render(props);
816
926
  }
817
927
  };
818
928
  formRegistry.addRenderer('@jupyterlab/notebook-extension:metadata-editor.notebook-metadata', notebookComponent);
@@ -823,21 +933,38 @@ const customMetadataEditorFields = {
823
933
  */
824
934
  const activeCellTool = {
825
935
  id: '@jupyterlab/notebook-extension:active-cell-tool',
826
- description: 'Adds active cell field in the metadata editor tab.',
936
+ description: 'Adds active cell fields in the metadata editor tab.',
827
937
  autoStart: true,
828
938
  requires: [INotebookTracker, IFormRendererRegistry, IEditorLanguageRegistry],
939
+ optional: [ITranslator],
829
940
  activate: (
830
941
  // Register the custom field.
831
- 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
+ });
832
952
  const component = {
833
953
  fieldRenderer: (props) => {
834
- return new ActiveCellTool({
835
- tracker,
836
- languages
837
- }).render(props);
954
+ return tool.render(props);
838
955
  }
839
956
  };
840
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);
841
968
  }
842
969
  };
843
970
  /**
@@ -902,6 +1029,7 @@ const plugins = [
902
1029
  trackerPlugin,
903
1030
  pageHandlerPlugin,
904
1031
  executionIndicator,
1032
+ exportGuidanceDialogPlugin,
905
1033
  exportPlugin,
906
1034
  tools,
907
1035
  cellCounterItem,
@@ -998,7 +1126,8 @@ function activatePageHandler(app, rendermime, settingRegistry, translator_) {
998
1126
  }
999
1127
  const mimeData = data;
1000
1128
  const metadata = ((_a = payload['metadata']) !== null && _a !== void 0 ? _a : {});
1001
- const mimeType = rendermime.preferredMimeType(mimeData, 'any');
1129
+ const trusted = false;
1130
+ const mimeType = rendermime.preferredMimeType(mimeData, 'ensure');
1002
1131
  if (!mimeType) {
1003
1132
  return false;
1004
1133
  }
@@ -1014,7 +1143,7 @@ function activatePageHandler(app, rendermime, settingRegistry, translator_) {
1014
1143
  const content = helpWidget.content;
1015
1144
  content.widgets.forEach(widget => widget.dispose());
1016
1145
  const renderer = rendermime.createRenderer(mimeType);
1017
- void renderer.renderModel(new MimeModel({ data: mimeData, metadata, trusted: true }));
1146
+ void renderer.renderModel(new MimeModel({ data: mimeData, metadata, trusted }));
1018
1147
  content.addWidget(renderer);
1019
1148
  if (!helpWidget.isAttached) {
1020
1149
  app.shell.add(helpWidget, 'down');
@@ -1513,8 +1642,10 @@ function activateNotebookHandler(app, factory, extensions, executor, palette, de
1513
1642
  value: settings.get('sideBySideOutputRatio').composite
1514
1643
  })
1515
1644
  .then(result => {
1516
- setSideBySideOutputRatio(result.value);
1517
- if (result.value) {
1645
+ if (result.value !== null &&
1646
+ Number.isFinite(result.value) &&
1647
+ result.value >= 0) {
1648
+ setSideBySideOutputRatio(result.value);
1518
1649
  void settings.set('sideBySideOutputRatio', result.value);
1519
1650
  }
1520
1651
  })
@@ -1577,10 +1708,13 @@ function activateNotebookHandler(app, factory, extensions, executor, palette, de
1577
1708
  widget.title.iconClass = (_a = ft === null || ft === void 0 ? void 0 : ft.iconClass) !== null && _a !== void 0 ? _a : '';
1578
1709
  widget.title.iconLabel = (_b = ft === null || ft === void 0 ? void 0 : ft.iconLabel) !== null && _b !== void 0 ? _b : '';
1579
1710
  widget.content.scrollbar = (_c = factory.notebookConfig.showMinimap) !== null && _c !== void 0 ? _c : false;
1580
- // 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.
1581
1715
  widget.context.pathChanged.connect(() => {
1582
1716
  void tracker.save(widget);
1583
- });
1717
+ }, widget);
1584
1718
  // Add the notebook panel to the tracker.
1585
1719
  void tracker.add(widget);
1586
1720
  });
@@ -1643,13 +1777,17 @@ function activateNotebookHandler(app, factory, extensions, executor, palette, de
1643
1777
  setSideBySideOutputRatio(factory.notebookConfig.sideBySideOutputRatio);
1644
1778
  const sideBySideMarginStyle = `.jp-mod-sideBySide.jp-Notebook .jp-Notebook-cell {
1645
1779
  margin-left: ${factory.notebookConfig.sideBySideLeftMarginOverride} !important;
1646
- margin-right: ${factory.notebookConfig.sideBySideRightMarginOverride} !important;`;
1780
+ margin-right: ${factory.notebookConfig.sideBySideRightMarginOverride} !important;
1781
+ }`;
1647
1782
  const sideBySideMarginTag = document.getElementById(SIDE_BY_SIDE_STYLE_ID);
1648
1783
  if (sideBySideMarginTag) {
1649
- sideBySideMarginTag.innerText = sideBySideMarginStyle;
1784
+ sideBySideMarginTag.textContent = sideBySideMarginStyle;
1650
1785
  }
1651
1786
  else {
1652
- document.head.insertAdjacentHTML('beforeend', `<style id="${SIDE_BY_SIDE_STYLE_ID}">${sideBySideMarginStyle}}</style>`);
1787
+ const style = document.createElement('style');
1788
+ style.id = SIDE_BY_SIDE_STYLE_ID;
1789
+ style.textContent = sideBySideMarginStyle;
1790
+ document.head.appendChild(style);
1653
1791
  }
1654
1792
  factory.autoStartDefault = settings.get('autoStartDefaultKernel')
1655
1793
  .composite;
@@ -1829,6 +1967,7 @@ function activateNotebookCompleterService(app, notebooks, manager, translator, a
1829
1967
  keys: ['Enter'],
1830
1968
  selector: '.jp-Notebook .jp-mod-completer-active'
1831
1969
  });
1970
+ const completerConnected = new WeakSet();
1832
1971
  const updateCompleter = async (_, notebook) => {
1833
1972
  var _a, _b;
1834
1973
  const completerContext = {
@@ -1838,6 +1977,15 @@ function activateNotebookCompleterService(app, notebooks, manager, translator, a
1838
1977
  sanitizer: sanitizer
1839
1978
  };
1840
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);
1841
1989
  notebook.content.activeCellChanged.connect((_, cell) => {
1842
1990
  // Ensure the editor will exist on the cell before adding the completer
1843
1991
  cell === null || cell === void 0 ? void 0 : cell.ready.then(() => {
@@ -1850,6 +1998,10 @@ function activateNotebookCompleterService(app, notebooks, manager, translator, a
1850
1998
  return manager.updateCompleter(newCompleterContext);
1851
1999
  }).catch(console.error);
1852
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.
1853
2005
  notebook.sessionContext.sessionChanged.connect(() => {
1854
2006
  var _a;
1855
2007
  // Ensure the editor will exist on the cell before adding the completer
@@ -1862,7 +2014,7 @@ function activateNotebookCompleterService(app, notebooks, manager, translator, a
1862
2014
  };
1863
2015
  return manager.updateCompleter(newCompleterContext);
1864
2016
  }).catch(console.error);
1865
- });
2017
+ }, notebook);
1866
2018
  };
1867
2019
  notebooks.widgetAdded.connect(updateCompleter);
1868
2020
  manager.activeProvidersChanged.connect(() => {
@@ -1970,17 +2122,26 @@ function addCommands(app, tracker, translator, sessionDialogs, settings, isEnabl
1970
2122
  NotebookActions.paste(notebook, mode, { stripOutputs });
1971
2123
  }
1972
2124
  };
1973
- // 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();
1974
2130
  tracker.currentChanged.connect((sender, panel) => {
1975
2131
  var _a, _b;
1976
- 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)) {
1977
2133
  return;
1978
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.
1979
2140
  panel.content.model.cells.changed.connect((list, args) => {
1980
2141
  // Might be overkill to refresh this every time, but
1981
2142
  // it helps to keep the collapse state consistent.
1982
2143
  refreshCellCollapsed(panel.content);
1983
- });
2144
+ }, panel.content);
1984
2145
  panel.content.activeCellChanged.connect((notebook, cell) => {
1985
2146
  NotebookActions.expandParent(cell, notebook);
1986
2147
  });
@@ -2260,8 +2421,12 @@ function addCommands(app, tracker, translator, sessionDialogs, settings, isEnabl
2260
2421
  commands.addCommand(CommandIDs.trust, {
2261
2422
  label: () => trans.__('Trust Notebook'),
2262
2423
  execute: async (args) => {
2263
- const current = getCurrent(tracker, shell, args);
2264
- 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) {
2265
2430
  const { context, content } = current;
2266
2431
  const trustResult = await NotebookActions.trust(content);
2267
2432
  if (trustResult.trusted) {
@@ -2275,7 +2440,12 @@ function addCommands(app, tracker, translator, sessionDialogs, settings, isEnabl
2275
2440
  describedBy: {
2276
2441
  args: {
2277
2442
  type: 'object',
2278
- properties: {}
2443
+ properties: {
2444
+ [CommandLinker.TRUST_BOUNDARY_ID_ARG]: {
2445
+ type: 'string',
2446
+ description: trans.__('The notebook trust boundary ID')
2447
+ }
2448
+ }
2279
2449
  }
2280
2450
  }
2281
2451
  });