xtralab 0.14.1 → 0.15.1

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.
Files changed (236) hide show
  1. package/README.md +10 -0
  2. package/lib/about/icons.d.ts +0 -5
  3. package/lib/about/icons.js +0 -5
  4. package/lib/about/index.d.ts +4 -8
  5. package/lib/about/index.js +6 -17
  6. package/lib/agentSessions.d.ts +9 -29
  7. package/lib/agentSessions.js +2 -7
  8. package/lib/askAgent/editorSelection.d.ts +20 -26
  9. package/lib/askAgent/editorSelection.js +11 -26
  10. package/lib/askAgent/icons.d.ts +2 -3
  11. package/lib/askAgent/icons.js +2 -3
  12. package/lib/askAgent/index.d.ts +4 -26
  13. package/lib/askAgent/index.js +54 -158
  14. package/lib/askAgent/popup.d.ts +12 -16
  15. package/lib/askAgent/popup.js +18 -37
  16. package/lib/askAgent/prompt.d.ts +3 -5
  17. package/lib/askAgent/prompt.js +6 -19
  18. package/lib/askAgent/queue.d.ts +11 -21
  19. package/lib/askAgent/queue.js +9 -19
  20. package/lib/askAgent/queuePanel.d.ts +13 -18
  21. package/lib/askAgent/queuePanel.js +15 -37
  22. package/lib/askAgent/targetPicker.d.ts +2 -9
  23. package/lib/askAgent/targetPicker.js +6 -11
  24. package/lib/askAgent/tokens.d.ts +32 -46
  25. package/lib/askAgent/tokens.js +4 -6
  26. package/lib/commandBar/index.d.ts +4 -13
  27. package/lib/commandBar/index.js +21 -39
  28. package/lib/customPanel/index.d.ts +7 -0
  29. package/lib/customPanel/index.js +89 -0
  30. package/lib/editorBreadcrumbs/index.d.ts +3 -6
  31. package/lib/editorBreadcrumbs/index.js +10 -6
  32. package/lib/editorBreadcrumbs/widget.d.ts +17 -4
  33. package/lib/editorBreadcrumbs/widget.js +11 -17
  34. package/lib/editorIndent/index.d.ts +4 -12
  35. package/lib/editorIndent/index.js +8 -23
  36. package/lib/fileBrowser/commands.d.ts +28 -19
  37. package/lib/fileBrowser/commands.js +51 -98
  38. package/lib/fileBrowser/contents.d.ts +14 -0
  39. package/lib/fileBrowser/contents.js +28 -0
  40. package/lib/fileBrowser/dragAndDrop.d.ts +79 -0
  41. package/lib/fileBrowser/dragAndDrop.js +110 -0
  42. package/lib/fileBrowser/fileBrowser.d.ts +21 -9
  43. package/lib/fileBrowser/fileBrowser.js +211 -405
  44. package/lib/fileBrowser/gitStatus.d.ts +4 -9
  45. package/lib/fileBrowser/gitStatus.js +7 -17
  46. package/lib/fileBrowser/gitignore.d.ts +4 -17
  47. package/lib/fileBrowser/gitignore.js +4 -25
  48. package/lib/fileBrowser/icons.d.ts +7 -17
  49. package/lib/fileBrowser/icons.js +25 -80
  50. package/lib/fileBrowser/index.js +41 -10
  51. package/lib/fileBrowser/widget.d.ts +170 -53
  52. package/lib/fileBrowser/widget.js +242 -18
  53. package/lib/fileTypeIcons/index.js +0 -10
  54. package/lib/git/api.d.ts +5 -13
  55. package/lib/git/api.js +10 -36
  56. package/lib/git/askRequest.d.ts +4 -8
  57. package/lib/git/askRequest.js +6 -20
  58. package/lib/git/commands.d.ts +40 -5
  59. package/lib/git/commands.js +16 -10
  60. package/lib/git/diffModel.js +4 -0
  61. package/lib/git/diffSurface.d.ts +41 -55
  62. package/lib/git/diffSurface.js +42 -117
  63. package/lib/git/diffWidget.d.ts +81 -2
  64. package/lib/git/diffWidget.js +62 -12
  65. package/lib/git/imageDiff.d.ts +5 -6
  66. package/lib/git/imageDiff.js +18 -48
  67. package/lib/git/index.d.ts +0 -3
  68. package/lib/git/index.js +3 -8
  69. package/lib/git/notebookDiff.d.ts +110 -33
  70. package/lib/git/notebookDiff.js +52 -195
  71. package/lib/git/tokens.d.ts +73 -31
  72. package/lib/git/tokens.js +2 -3
  73. package/lib/highlight/index.d.ts +4 -10
  74. package/lib/highlight/index.js +10 -41
  75. package/lib/index.d.ts +0 -9
  76. package/lib/index.js +2 -9
  77. package/lib/launcher/agents.d.ts +55 -51
  78. package/lib/launcher/agents.js +25 -54
  79. package/lib/launcher/availability.d.ts +3 -9
  80. package/lib/launcher/availability.js +3 -9
  81. package/lib/launcher/commands.d.ts +7 -32
  82. package/lib/launcher/commands.js +14 -45
  83. package/lib/launcher/dashboard.d.ts +19 -24
  84. package/lib/launcher/dashboard.js +21 -67
  85. package/lib/launcher/editorRegistry.d.ts +10 -22
  86. package/lib/launcher/editorRegistry.js +14 -18
  87. package/lib/launcher/editors.d.ts +39 -45
  88. package/lib/launcher/editors.js +8 -34
  89. package/lib/launcher/icons.d.ts +0 -12
  90. package/lib/launcher/icons.js +14 -37
  91. package/lib/launcher/index.js +12 -55
  92. package/lib/launcher/invocation.d.ts +4 -10
  93. package/lib/launcher/invocation.js +7 -22
  94. package/lib/launcher/registry.d.ts +9 -7
  95. package/lib/launcher/registry.js +9 -7
  96. package/lib/launcher/schemaDefaults.d.ts +2 -9
  97. package/lib/launcher/schemaDefaults.js +37 -13
  98. package/lib/launcher/tokens.d.ts +10 -22
  99. package/lib/launcher/tokens.js +5 -8
  100. package/lib/menuBar/index.d.ts +3 -10
  101. package/lib/menuBar/index.js +11 -28
  102. package/lib/menus/index.d.ts +3 -9
  103. package/lib/menus/index.js +14 -30
  104. package/lib/omnibox/files.d.ts +2 -4
  105. package/lib/omnibox/files.js +5 -11
  106. package/lib/omnibox/index.d.ts +4 -14
  107. package/lib/omnibox/index.js +9 -27
  108. package/lib/omnibox/model.d.ts +38 -10
  109. package/lib/omnibox/model.js +15 -33
  110. package/lib/omnibox/recents.d.ts +15 -17
  111. package/lib/omnibox/recents.js +12 -17
  112. package/lib/omnibox/tokens.d.ts +5 -10
  113. package/lib/omnibox/tokens.js +2 -6
  114. package/lib/omnibox/widget.d.ts +11 -7
  115. package/lib/omnibox/widget.js +8 -16
  116. package/lib/searchReplace/index.d.ts +4 -20
  117. package/lib/searchReplace/index.js +7 -27
  118. package/lib/showOutput/index.d.ts +3 -12
  119. package/lib/showOutput/index.js +6 -22
  120. package/lib/sidebar/index.d.ts +4 -13
  121. package/lib/sidebar/index.js +73 -46
  122. package/lib/terminalNotifications/index.d.ts +4 -10
  123. package/lib/terminalNotifications/index.js +6 -20
  124. package/lib/terminals/agentTerminals.d.ts +12 -5
  125. package/lib/terminals/agentTerminals.js +25 -44
  126. package/lib/terminals/detection.d.ts +4 -8
  127. package/lib/terminals/detection.js +4 -8
  128. package/lib/terminals/index.d.ts +0 -33
  129. package/lib/terminals/index.js +24 -102
  130. package/lib/terminals/model.d.ts +64 -139
  131. package/lib/terminals/model.js +80 -220
  132. package/lib/terminals/tokens.d.ts +11 -22
  133. package/lib/terminals/tokens.js +2 -2
  134. package/lib/terminals/widget.d.ts +72 -38
  135. package/lib/terminals/widget.js +173 -73
  136. package/lib/topBar/icons.d.ts +3 -8
  137. package/lib/topBar/icons.js +3 -8
  138. package/lib/topBar/index.d.ts +4 -11
  139. package/lib/topBar/index.js +10 -38
  140. package/lib/walkthrough/index.d.ts +4 -9
  141. package/lib/walkthrough/index.js +6 -20
  142. package/lib/walkthrough/panel.d.ts +40 -9
  143. package/lib/walkthrough/panel.js +4 -10
  144. package/package.json +2 -2
  145. package/schema/launcher.json +2 -2
  146. package/schema/sidebar.json +47 -7
  147. package/src/about/icons.ts +0 -5
  148. package/src/about/index.tsx +6 -17
  149. package/src/agentSessions.ts +9 -29
  150. package/src/askAgent/editorSelection.ts +22 -29
  151. package/src/askAgent/icons.ts +2 -3
  152. package/src/askAgent/index.ts +54 -158
  153. package/src/askAgent/popup.tsx +23 -45
  154. package/src/askAgent/prompt.ts +6 -19
  155. package/src/askAgent/queue.ts +13 -24
  156. package/src/askAgent/queuePanel.tsx +21 -46
  157. package/src/askAgent/targetPicker.tsx +5 -15
  158. package/src/askAgent/tokens.ts +32 -46
  159. package/src/commandBar/index.ts +24 -39
  160. package/src/customPanel/index.ts +119 -0
  161. package/src/editorBreadcrumbs/index.ts +10 -6
  162. package/src/editorBreadcrumbs/widget.ts +23 -17
  163. package/src/editorIndent/index.ts +17 -23
  164. package/src/fileBrowser/commands.ts +70 -98
  165. package/src/fileBrowser/contents.ts +33 -0
  166. package/src/fileBrowser/dragAndDrop.ts +188 -0
  167. package/src/fileBrowser/fileBrowser.tsx +259 -440
  168. package/src/fileBrowser/gitStatus.ts +7 -17
  169. package/src/fileBrowser/gitignore.ts +4 -25
  170. package/src/fileBrowser/icons.ts +25 -80
  171. package/src/fileBrowser/index.ts +43 -10
  172. package/src/fileBrowser/widget.tsx +344 -64
  173. package/src/fileTypeIcons/index.ts +0 -10
  174. package/src/git/api.ts +10 -36
  175. package/src/git/askRequest.ts +6 -20
  176. package/src/git/commands.ts +70 -10
  177. package/src/git/diffModel.ts +25 -0
  178. package/src/git/diffSurface.tsx +71 -159
  179. package/src/git/diffWidget.tsx +104 -14
  180. package/src/git/imageDiff.tsx +45 -51
  181. package/src/git/index.ts +3 -8
  182. package/src/git/notebookDiff.tsx +211 -238
  183. package/src/git/tokens.ts +73 -31
  184. package/src/highlight/index.ts +10 -41
  185. package/src/index.ts +2 -9
  186. package/src/launcher/agents.ts +74 -92
  187. package/src/launcher/availability.ts +3 -9
  188. package/src/launcher/commands.ts +14 -51
  189. package/src/launcher/dashboard.tsx +49 -88
  190. package/src/launcher/editorRegistry.ts +21 -29
  191. package/src/launcher/editors.ts +39 -58
  192. package/src/launcher/icons.ts +15 -37
  193. package/src/launcher/index.ts +12 -55
  194. package/src/launcher/invocation.ts +7 -22
  195. package/src/launcher/registry.ts +9 -7
  196. package/src/launcher/schemaDefaults.ts +43 -13
  197. package/src/launcher/tokens.ts +10 -22
  198. package/src/menuBar/index.ts +11 -28
  199. package/src/menus/index.ts +14 -30
  200. package/src/omnibox/files.ts +10 -14
  201. package/src/omnibox/index.ts +9 -27
  202. package/src/omnibox/model.ts +50 -38
  203. package/src/omnibox/recents.ts +17 -22
  204. package/src/omnibox/tokens.ts +5 -10
  205. package/src/omnibox/widget.tsx +14 -19
  206. package/src/searchReplace/index.ts +7 -27
  207. package/src/showOutput/index.ts +6 -22
  208. package/src/sidebar/index.ts +103 -52
  209. package/src/terminalNotifications/index.ts +42 -25
  210. package/src/terminals/agentTerminals.ts +25 -49
  211. package/src/terminals/detection.ts +4 -8
  212. package/src/terminals/index.ts +31 -102
  213. package/src/terminals/model.ts +130 -246
  214. package/src/terminals/tokens.ts +11 -22
  215. package/src/terminals/widget.tsx +229 -113
  216. package/src/topBar/icons.ts +3 -8
  217. package/src/topBar/index.ts +21 -44
  218. package/src/walkthrough/index.ts +6 -20
  219. package/src/walkthrough/panel.ts +40 -10
  220. package/style/about.css +2 -7
  221. package/style/askAgent.css +8 -40
  222. package/style/base.css +25 -73
  223. package/style/chrome.css +108 -308
  224. package/style/commandBar.css +4 -20
  225. package/style/customPanel.css +6 -0
  226. package/style/git.css +22 -184
  227. package/style/highlight.css +1 -3
  228. package/style/index.js +1 -0
  229. package/style/launcher.css +6 -70
  230. package/style/omnibox.css +2 -14
  231. package/style/showOutput.css +0 -6
  232. package/style/sidebar.css +29 -9
  233. package/style/tabs.css +5 -13
  234. package/style/terminals.css +18 -123
  235. package/style/topBar.css +2 -22
  236. package/style/walkthrough.css +0 -8
@@ -0,0 +1,119 @@
1
+ import {
2
+ JupyterFrontEnd,
3
+ JupyterFrontEndPlugin
4
+ } from '@jupyterlab/application';
5
+ import {
6
+ IMovableSectionDestination,
7
+ IMovableSectionRegistry
8
+ } from '@jupyterlab/apputils';
9
+ import {
10
+ ITranslator,
11
+ nullTranslator,
12
+ type TranslationBundle
13
+ } from '@jupyterlab/translation';
14
+ import { SidePanel, tableRowsIcon } from '@jupyterlab/ui-components';
15
+ import { ISignal, Signal } from '@lumino/signaling';
16
+ import { AccordionPanel, Widget } from '@lumino/widgets';
17
+
18
+ const PLUGIN_ID = 'xtralab:custom-panel';
19
+
20
+ const CUSTOM_PANEL_ID = 'xtralab-custom-panel';
21
+
22
+ /**
23
+ * A sidebar panel holding only the sections moved in from other panels.
24
+ */
25
+ class CustomPanel extends SidePanel implements IMovableSectionDestination {
26
+ constructor(options: { trans: TranslationBundle }) {
27
+ super();
28
+ this.id = CUSTOM_PANEL_ID;
29
+ this.title.icon = tableRowsIcon;
30
+ this.title.caption = options.trans.__('Custom Panel');
31
+ this.addClass('jp-xtralab-CustomPanel');
32
+ }
33
+
34
+ /**
35
+ * The accordion panel hosting the sections.
36
+ */
37
+ get accordionPanel(): AccordionPanel {
38
+ return this.content as AccordionPanel;
39
+ }
40
+
41
+ /**
42
+ * The section widgets currently hosted by the panel.
43
+ */
44
+ get sections(): ReadonlyArray<Widget> {
45
+ return this.content.widgets;
46
+ }
47
+
48
+ /**
49
+ * A signal emitted with the new count when the number of sections changes.
50
+ */
51
+ get sectionCountChanged(): ISignal<this, number> {
52
+ return this._sectionCountChanged;
53
+ }
54
+
55
+ /**
56
+ * Add a section widget to the panel.
57
+ */
58
+ addSection(widget: Widget): void {
59
+ this.addWidget(widget);
60
+ this._sectionCountChanged.emit(this.content.widgets.length);
61
+ }
62
+
63
+ /**
64
+ * Remove a section widget from the panel, if it is hosted here.
65
+ */
66
+ removeSectionWidget(widget: Widget): void {
67
+ if (widget.parent !== this.content) {
68
+ return;
69
+ }
70
+ widget.parent = null;
71
+ this._sectionCountChanged.emit(this.content.widgets.length);
72
+ }
73
+
74
+ private _sectionCountChanged = new Signal<this, number>(this);
75
+ }
76
+
77
+ /**
78
+ * Registers "Custom Panel", which takes a sidebar tab only while it hosts a
79
+ * section.
80
+ */
81
+ const plugin: JupyterFrontEndPlugin<void> = {
82
+ id: PLUGIN_ID,
83
+ description:
84
+ 'A user-assembled sidebar panel hosting sections moved in from other panels; hidden while empty.',
85
+ autoStart: true,
86
+ optional: [IMovableSectionRegistry, ITranslator],
87
+ activate: (
88
+ app: JupyterFrontEnd,
89
+ registry: IMovableSectionRegistry | null,
90
+ translator: ITranslator | null
91
+ ): void => {
92
+ if (!registry) {
93
+ return;
94
+ }
95
+ const trans = (translator ?? nullTranslator).load('jupyterlab');
96
+ const panel = new CustomPanel({ trans });
97
+
98
+ // Only a user move should switch the sidebar, not the startup restoration.
99
+ let restored = false;
100
+ void app.restored.then(() => {
101
+ restored = true;
102
+ });
103
+
104
+ panel.sectionCountChanged.connect((_, count) => {
105
+ if (count > 0 && !panel.parent) {
106
+ app.shell.add(panel, 'left', { rank: 3 });
107
+ if (restored) {
108
+ app.shell.activateById(panel.id);
109
+ }
110
+ } else if (count === 0 && panel.parent) {
111
+ panel.parent = null;
112
+ }
113
+ });
114
+
115
+ registry.registerTarget(PLUGIN_ID, trans.__('Custom Panel'), panel);
116
+ }
117
+ };
118
+
119
+ export default plugin;
@@ -21,6 +21,10 @@ const PLUGIN_ID = 'xtralab:editor-breadcrumbs';
21
21
  const EDITOR_FACTORY = 'Editor';
22
22
  const TOOLBAR_ITEM_NAME = 'xtralab-editor-breadcrumbs';
23
23
 
24
+ /**
25
+ * A widget extension that adds an `EditorBreadcrumbs` item to the toolbar of
26
+ * each new editor widget.
27
+ */
24
28
  class EditorBreadcrumbsExtension implements DocumentRegistry.IWidgetExtension<
25
29
  IDocumentWidget<Widget, DocumentRegistry.IModel>,
26
30
  DocumentRegistry.IModel
@@ -30,6 +34,9 @@ class EditorBreadcrumbsExtension implements DocumentRegistry.IWidgetExtension<
30
34
  this._trans = trans;
31
35
  }
32
36
 
37
+ /**
38
+ * Create the breadcrumbs toolbar item for a new editor widget.
39
+ */
33
40
  createNew(
34
41
  widget: IDocumentWidget<Widget, DocumentRegistry.IModel>,
35
42
  context: DocumentRegistry.IContext<DocumentRegistry.IModel>
@@ -56,12 +63,9 @@ class EditorBreadcrumbsExtension implements DocumentRegistry.IWidgetExtension<
56
63
  }
57
64
 
58
65
  /**
59
- * Adds a VS Code-style path breadcrumb to text editor toolbars. Each
60
- * segment is clickable: clicking dispatches `xtralab:reveal-path` so
61
- * any plugin that listens (today, the file browser) can surface the
62
- * underlying folder or file. The breadcrumbs plugin therefore does not
63
- * import the file browser at all — the JupyterLab command registry is
64
- * the only seam between them.
66
+ * Adds a clickable path breadcrumb to text editor toolbars. Clicks dispatch
67
+ * `xtralab:reveal-path`; the command registry is the only seam with the file
68
+ * browser.
65
69
  */
66
70
  const plugin: JupyterFrontEndPlugin<void> = {
67
71
  id: PLUGIN_ID,
@@ -15,15 +15,25 @@ const CURRENT_ITEM_CLASS = 'jp-mod-current';
15
15
  const SEPARATOR_CLASS = 'jp-BreadCrumbs-separator';
16
16
 
17
17
  /**
18
- * Command dispatched when the user clicks a breadcrumb segment.
19
- * Registered by the file browser plugin; the breadcrumbs only know
20
- * the name so they stay decoupled from the file browser internals.
18
+ * Registered by the file browser plugin; only the name is shared here.
21
19
  */
22
20
  const REVEAL_COMMAND = 'xtralab:reveal-path';
23
21
 
22
+ /**
23
+ * The options used to create an `EditorBreadcrumbs` widget.
24
+ */
24
25
  interface IEditorBreadcrumbsOptions {
26
+ /**
27
+ * The document context whose path is rendered.
28
+ */
25
29
  context: DocumentRegistry.IContext<DocumentRegistry.IModel>;
30
+ /**
31
+ * The command registry used to dispatch the reveal command.
32
+ */
26
33
  commands: CommandRegistry;
34
+ /**
35
+ * The application translation bundle.
36
+ */
27
37
  trans: TranslationBundle;
28
38
  }
29
39
 
@@ -38,9 +48,8 @@ export class EditorBreadcrumbs extends Widget {
38
48
  this._commands = options.commands;
39
49
  this._trans = options.trans;
40
50
  this.addClass(EDITOR_BREADCRUMBS_CLASS);
41
- // The breadcrumbs stretch to fill the toolbar, so ReactiveToolbar's
42
- // overflow math must count them as a spacer (2px) — measured at their
43
- // stretched clientWidth they would evict every item into the popup.
51
+ // ReactiveToolbar must count the stretched breadcrumbs as a spacer (2px);
52
+ // measured at clientWidth they would evict every item into the popup.
44
53
  this.addClass(TOOLBAR_SPACER_CLASS);
45
54
  this.node.setAttribute('aria-label', this._trans.__('Editor file path'));
46
55
 
@@ -55,6 +64,9 @@ export class EditorBreadcrumbs extends Widget {
55
64
  this._render();
56
65
  }
57
66
 
67
+ /**
68
+ * Dispose of the resources held by the widget.
69
+ */
58
70
  dispose(): void {
59
71
  if (this.isDisposed) {
60
72
  return;
@@ -81,9 +93,7 @@ export class EditorBreadcrumbs extends Widget {
81
93
  parts.forEach((part, index) => {
82
94
  const isLast = index === parts.length - 1;
83
95
  const subPath = parts.slice(0, index + 1).join('/');
84
- // Canonical `@pierre/trees` paths: directories carry a trailing
85
- // slash, files do not. Every part except the last names a folder
86
- // along the way to the open file.
96
+ // Canonical @pierre/trees paths: directories carry a trailing slash.
87
97
  const canonical = isLast ? subPath : `${subPath}/`;
88
98
 
89
99
  const item = document.createElement('span');
@@ -117,19 +127,15 @@ export class EditorBreadcrumbs extends Widget {
117
127
  item.dataset.path = '/';
118
128
  item.tabIndex = 0;
119
129
  item.setAttribute('role', 'button');
120
- // Empty canonical path: the reveal command treats this as the
121
- // workspace-root gesture — surface the file browser, clear its
122
- // selection, and scroll back to the top. The tree has no row for
123
- // the workspace root itself.
130
+ // Empty path is the workspace-root gesture; the tree has no row for
131
+ // the root itself.
124
132
  this._attachReveal(item, '');
125
133
  return item;
126
134
  }
127
135
 
128
136
  /**
129
- * Bind click and keyboard activation on a crumb element to the
130
- * shared reveal command. Dispatch is best-effort: a failure to
131
- * resolve or execute the command is logged but never thrown into
132
- * the editor's toolbar.
137
+ * Bind click and keyboard activation on a crumb to the reveal command;
138
+ * dispatch failures are logged, never thrown into the toolbar.
133
139
  */
134
140
  private _attachReveal(element: HTMLElement, canonicalPath: string): void {
135
141
  const fire = (event: Event): void => {
@@ -14,10 +14,8 @@ import detectIndent from 'detect-indent';
14
14
  const PLUGIN_ID = 'xtralab:editor-indent';
15
15
 
16
16
  /**
17
- * Mime types whose newly-created (or too-short-to-detect) files should default
18
- * to a 2-space indent. Matches Prettier's defaults for web languages; mime
19
- * types not listed here inherit whatever the user has set in JupyterLab.
20
- * Covers .ts/.tsx/.jsx/.js variants registered by `@jupyterlab/codemirror`.
17
+ * 2-space fallback for new or undetectable files, matching Prettier's
18
+ * web-language defaults; unlisted mime types keep the user's setting.
21
19
  */
22
20
  const FALLBACK_INDENT_BY_MIME: Record<string, number> = {
23
21
  'application/ecmascript': 2,
@@ -38,15 +36,20 @@ const FALLBACK_INDENT_BY_MIME: Record<string, number> = {
38
36
  'text/yaml': 2
39
37
  };
40
38
 
39
+ /**
40
+ * The indentation resolved for a document.
41
+ */
41
42
  interface IResolvedIndent {
43
+ /**
44
+ * The string inserted per indent level (spaces or a tab), for `indentUnit`.
45
+ */
42
46
  unit: string;
47
+ /**
48
+ * The indent width in columns, for `EditorState.tabSize`.
49
+ */
43
50
  width: number;
44
51
  }
45
52
 
46
- /**
47
- * Run `detect-indent` against the file's content and convert its result into
48
- * something we can hand to CodeMirror.
49
- */
50
53
  function detectFromContent(text: string): IResolvedIndent | null {
51
54
  if (text.length === 0) {
52
55
  return null;
@@ -76,9 +79,8 @@ function resolveIndent(text: string, mimeType: string): IResolvedIndent | null {
76
79
  }
77
80
 
78
81
  function buildExtension(resolved: IResolvedIndent): Extension {
79
- // `Prec.highest` keeps these values first in their respective facets so
80
- // they beat both `defaultConfig` and per-editor `editorConfig.indentUnit`,
81
- // both of which JupyterLab registers at default precedence.
82
+ // `Prec.highest` beats both `defaultConfig` and per-editor
83
+ // `editorConfig.indentUnit`, which JupyterLab registers at default precedence.
82
84
  return Prec.highest([
83
85
  indentUnit.of(resolved.unit),
84
86
  EditorState.tabSize.of(resolved.width)
@@ -86,18 +88,10 @@ function buildExtension(resolved: IResolvedIndent): Extension {
86
88
  }
87
89
 
88
90
  /**
89
- * Sniff each opened document's leading whitespace and configure CodeMirror's
90
- * `indentUnit` / `tabSize` to match. JupyterLab only exposes a single global
91
- * indent setting, which means TypeScript files written with 4-space indent and
92
- * Python files written with 2-space indent both look wrong in the same editor;
93
- * detecting from the file's own content sidesteps that without forcing every
94
- * project to share the same convention.
95
- *
96
- * Detection runs at editor-construction time and again on the first content
97
- * change — that second pass exists because JupyterLab's collaborative document
98
- * loader can settle the file body into the shared model after the editor view
99
- * already exists, in which case the factory would otherwise have only seen an
100
- * empty string.
91
+ * Sniff each document's indentation and configure CodeMirror's `indentUnit` /
92
+ * `tabSize` to match, since JupyterLab only has one global indent setting.
93
+ * Detection reruns on the first content change: the collaborative loader can
94
+ * settle the file body after the editor view exists, so the factory may see ''.
101
95
  */
102
96
  const plugin: JupyterFrontEndPlugin<void> = {
103
97
  id: PLUGIN_ID,
@@ -32,11 +32,9 @@ import { toCanonicalPath, toServerPath } from './contents';
32
32
  import { FILE_BROWSER_ID, IXtralabFileBrowser } from './widget';
33
33
 
34
34
  /**
35
- * Command identifiers exposed by the xtralab browser. We deliberately namespace
36
- * these under `xtralab:` rather than reusing the `filebrowser:` ids
37
- * because the core `filebrowser:*` commands look up a `FileBrowser` instance
38
- * via `IFileBrowserFactory.tracker` — our widget is not a `FileBrowser`, so
39
- * those handlers would never see it.
35
+ * Command ids for the xtralab browser. Namespaced `xtralab:` rather than
36
+ * reusing the `filebrowser:` ids: the core handlers resolve their target via
37
+ * `IFileBrowserFactory.tracker`, which never sees this widget.
40
38
  */
41
39
  export namespace CommandIDs {
42
40
  export const open = 'xtralab:open';
@@ -56,27 +54,40 @@ export namespace CommandIDs {
56
54
  }
57
55
 
58
56
  /**
59
- * Submenu id used to host the dynamically populated "Open With" entries.
60
- * Distinct from the core file browser's `jp-contextmenu-open-with` so the
61
- * core's populator does not try to fill ours with items derived from the
62
- * default file browser's selection.
57
+ * "Open With" submenu id. Distinct from the core `jp-contextmenu-open-with`
58
+ * so the core's populator does not fill ours from its own selection.
63
59
  */
64
60
  const OPEN_WITH_SUBMENU_ID = 'jp-contextmenu-xtralab-open-with';
65
61
 
62
+ /**
63
+ * Options for registering the xtralab file browser commands.
64
+ */
66
65
  interface IRegisterCommandsOptions {
66
+ /**
67
+ * The JupyterLab application the commands are registered on.
68
+ */
67
69
  app: JupyterFrontEnd;
70
+ /**
71
+ * The file browser widget the commands act on.
72
+ */
68
73
  browser: IXtralabFileBrowser;
74
+ /**
75
+ * The document manager used to resolve the current widget's file path.
76
+ */
69
77
  docManager: IDocumentManager;
78
+ /**
79
+ * The command palette the commands are added to, or `null` when unavailable.
80
+ */
70
81
  palette: ICommandPalette | null;
82
+ /**
83
+ * The translator for user-facing strings; `null` falls back to English.
84
+ */
71
85
  translator: ITranslator | null;
72
86
  }
73
87
 
74
88
  /**
75
- * Resolve the path of the item the user right-clicked. We prefer the
76
- * contextmenu event's target — that is how `app.contextMenu` resolves which
77
- * items match a selector — and fall back to the first item in the tree's
78
- * selection if the command was invoked without an active contextmenu event
79
- * (e.g. from the command palette).
89
+ * Path of the right-clicked item, from the contextmenu event's target;
90
+ * falls back to the tree selection when invoked without one (the palette).
80
91
  */
81
92
  function getTargetPath(
82
93
  app: JupyterFrontEnd,
@@ -92,11 +103,6 @@ function getTargetPath(
92
103
  return browser.selectedPaths[0];
93
104
  }
94
105
 
95
- /**
96
- * Resolve the kind of the item that was right-clicked, based on the data
97
- * attributes emitted by `@pierre/trees` rows. Returns `undefined` if no
98
- * contextmenu event is active or the target is not a tree item.
99
- */
100
106
  function getTargetKind(app: JupyterFrontEnd): 'file' | 'folder' | undefined {
101
107
  const node = app.contextMenuHitTest(
102
108
  n => n.dataset !== undefined && n.dataset.type === 'item'
@@ -106,14 +112,9 @@ function getTargetKind(app: JupyterFrontEnd): 'file' | 'folder' | undefined {
106
112
  }
107
113
 
108
114
  /**
109
- * Resolve the canonical paths the "Open" command should act on. When the
110
- * user right-clicks a row that is part of the current selection, every
111
- * selected file is opened — matching the default file browser, where
112
- * "Open" on any file in a multi-selection opens them all. If the
113
- * right-clicked row is *not* in the selection, only that row is opened.
114
- *
115
- * Folders are always filtered out: there is no "current directory" in
116
- * this tree, so opening a folder via "Open" is a no-op.
115
+ * Canonical paths "Open" acts on: the whole selection when the right-clicked
116
+ * row is part of it (matching the default browser), else just that row.
117
+ * Folders are filtered out — this tree has no "current directory".
117
118
  */
118
119
  function getOpenPaths(
119
120
  app: JupyterFrontEnd,
@@ -134,9 +135,6 @@ function getOpenPaths(
134
135
  return candidates.filter(path => !path.endsWith('/'));
135
136
  }
136
137
 
137
- /**
138
- * True iff there is an actionable target for a command on the right-click.
139
- */
140
138
  function hasTarget(
141
139
  app: JupyterFrontEnd,
142
140
  browser: IXtralabFileBrowser
@@ -145,11 +143,9 @@ function hasTarget(
145
143
  }
146
144
 
147
145
  /**
148
- * Resolve the directory used as the working directory for actions that
149
- * create a new file or folder. Folder selections become their own cwd;
150
- * file selections fall back to their parent. Returns the empty string
151
- * when the tree has no selection — that matches the contents API's
152
- * convention for "the root directory".
146
+ * Working directory for create actions: a selected folder is its own cwd, a
147
+ * file falls back to its parent; empty string (the contents-API root) when
148
+ * nothing is selected.
153
149
  */
154
150
  function getWorkingDirectory(browser: IXtralabFileBrowser): string {
155
151
  const first = browser.selectedPaths[0];
@@ -164,14 +160,10 @@ function getWorkingDirectory(browser: IXtralabFileBrowser): string {
164
160
  }
165
161
 
166
162
  /**
167
- * Resolve the main-area widget whose tab is the target of the current
168
- * context-menu event, or `null` when that target is not a document tab. The
169
- * shell stamps each tab's `data-id` with its widget id, so the right-clicked
170
- * tab resolves even when it is not the current one.
171
- *
172
- * Valid only during a context-menu invocation: `contextMenuHitTest` reads the
173
- * last context-menu event, which is not cleared when the menu closes, so
174
- * {@link documentPathToReveal} gates this to avoid resolving a stale tab.
163
+ * Main-area widget whose tab is the target of the current context-menu
164
+ * event, or `null`. `contextMenuHitTest` reads the last context-menu event,
165
+ * which is not cleared when the menu closes, so callers must gate against
166
+ * resolving a stale tab.
175
167
  */
176
168
  function contextMenuTabWidget(app: JupyterFrontEnd): Widget | null {
177
169
  const node = app.contextMenuHitTest(
@@ -190,10 +182,9 @@ function contextMenuTabWidget(app: JupyterFrontEnd): Widget | null {
190
182
  }
191
183
 
192
184
  /**
193
- * Resolve the document to reveal as a canonical `@pierre/trees` path: the
194
- * right-clicked tab when `fromContextMenu` is true (the file-tab menu),
195
- * otherwise the active main-area widget (the palette). A file's context
196
- * `path` is already canonical. Returns `undefined` when nothing resolves.
185
+ * Canonical path of the document to reveal: the right-clicked tab when
186
+ * `fromContextMenu` is true (the file-tab menu), else the active main-area
187
+ * widget (the palette).
197
188
  */
198
189
  function documentPathToReveal(
199
190
  app: JupyterFrontEnd,
@@ -211,14 +202,10 @@ function documentPathToReveal(
211
202
  }
212
203
 
213
204
  /**
214
- * Refresh the "Open With" submenu's items from the document factories that
215
- * can open the file under the right-click. Mirrors the
216
- * `@jupyterlab/filebrowser-extension:open-with` pattern: the schema declares
217
- * an empty submenu by id, and we look it up on the open context menu and
218
- * fill it before the user can hover.
219
- *
205
+ * Build the populator that fills the "Open With" submenu on every
206
+ * context-menu open, mirroring `filebrowser-extension:open-with`.
220
207
  * `preferredWidgetFactories(path)` is path-only, so the populator stays
221
- * synchronous and the submenu is ready before the user can hover over it.
208
+ * synchronous and the submenu is ready before the user can hover.
222
209
  */
223
210
  function makeOpenWithUpdater(
224
211
  app: JupyterFrontEnd,
@@ -259,13 +246,9 @@ function makeOpenWithUpdater(
259
246
  }
260
247
 
261
248
  /**
262
- * Register every xtralab command on the application command registry, attach
263
- * the items to the application context menu, and wire up the dynamic
264
- * "Open With" submenu populator.
265
- *
266
- * @returns A function that detaches the menu items and signal listener.
267
- * The commands themselves are owned by the registry for the lifetime of
268
- * the plugin.
249
+ * Register the xtralab commands, context-menu wiring, and the dynamic
250
+ * "Open With" populator. Returns a detach function; the commands themselves
251
+ * stay owned by the registry.
269
252
  */
270
253
  export function registerCommands(opts: IRegisterCommandsOptions): () => void {
271
254
  const { app, browser, docManager, palette, translator } = opts;
@@ -496,27 +479,33 @@ export function registerCommands(opts: IRegisterCommandsOptions): () => void {
496
479
  }
497
480
  });
498
481
 
499
- // Refresh the toggle's state however the filter is shown or hidden —
500
- // the toolbar button, or the auto-show that kicks in when typing with
501
- // the tree focused opens a search session.
482
+ // The filter can also auto-show when typing with the tree focused, so
483
+ // track visibility changes rather than the command's own executions.
502
484
  const onFilterVisibleChanged = (): void => {
503
485
  commands.notifyCommandChanged(CommandIDs.toggleFileFilter);
504
486
  };
505
487
  browser.fileFilterVisibleChanged.connect(onFilterVisibleChanged);
506
488
 
507
- // Public reveal seam. Other plugins (editor breadcrumbs, future
508
- // "reveal in tree" actions) call this command rather than depending
509
- // on the file browser widget directly. Activating the sidebar is part
510
- // of the contract: the tree is hidden behind a tab and a reveal that
511
- // does not surface the panel would silently no-op from the user's
512
- // point of view. An empty path is the "workspace root" gesture —
513
- // there is no tree row for the root itself, so the browser is asked
514
- // to scroll back to the top and clear its selection instead.
489
+ // Public reveal seam for other plugins; an empty path is the workspace-root
490
+ // gesture (the root has no tree row of its own). The tree is movable, so
491
+ // surface whichever sidebar widget holds it.
492
+ const activateTreeHost = (): void => {
493
+ for (const area of ['left', 'right']) {
494
+ for (const widget of app.shell.widgets(area)) {
495
+ if (widget.node.contains(browser.sectionNode)) {
496
+ app.shell.activateById(widget.id);
497
+ return;
498
+ }
499
+ }
500
+ }
501
+ app.shell.activateById(FILE_BROWSER_ID);
502
+ };
503
+
515
504
  commands.addCommand(CommandIDs.revealPath, {
516
505
  label: trans.__('Reveal in File Browser'),
517
506
  execute: (args: ReadonlyPartialJSONObject) => {
518
507
  const path = (args.path as string | undefined) ?? '';
519
- app.shell.activateById(FILE_BROWSER_ID);
508
+ activateTreeHost();
520
509
  if (path.length === 0) {
521
510
  browser.scrollToRoot();
522
511
  } else {
@@ -525,8 +514,6 @@ export function registerCommands(opts: IRegisterCommandsOptions): () => void {
525
514
  }
526
515
  });
527
516
 
528
- // Reveal the resolved document in the tree browser via the reveal-path
529
- // command, which surfaces the sidebar and scrolls to the target.
530
517
  commands.addCommand(CommandIDs.revealInFileTree, {
531
518
  label: trans.__('Show in File Tree'),
532
519
  caption: trans.__('Show this file in the xtralab file browser'),
@@ -566,10 +553,8 @@ export function registerCommands(opts: IRegisterCommandsOptions): () => void {
566
553
  commands.addCommand(CommandIDs.newLauncher, {
567
554
  label: trans.__('New Launcher'),
568
555
  caption: trans.__('Open a new launcher'),
569
- // Skip the `menuItem` bindprops the other commands use: this command
570
- // lives on the toolbar, where the launcher-extension's blue button
571
- // styling expects the raw `jp-icon3` paths from `addIcon` so it can
572
- // recolor them against the brand background.
556
+ // Raw `addIcon`, no `menuItem` bindprops: the launcher-extension's blue
557
+ // toolbar-button styling recolors the raw `jp-icon3` paths.
573
558
  icon: addIcon,
574
559
  execute: () => {
575
560
  const cwd = getWorkingDirectory(browser);
@@ -577,34 +562,24 @@ export function registerCommands(opts: IRegisterCommandsOptions): () => void {
577
562
  }
578
563
  });
579
564
 
580
- // Re-evaluate enabled/visible state when the user changes the tree
581
- // selection, in case a command's predicate depends on the selection.
582
565
  browser.selectionChanged.connect(() => {
583
566
  for (const id of Object.values(CommandIDs)) {
584
567
  commands.notifyCommandChanged(id);
585
568
  }
586
569
  });
587
570
 
588
- // This command's enabled state tracks the active widget, not the tree
589
- // selection, so re-evaluate it on current-widget changes. `currentChanged`
590
- // is optional on the shell interface.
591
571
  const onCurrentChanged = (): void => {
592
572
  commands.notifyCommandChanged(CommandIDs.revealInFileTree);
593
573
  };
594
574
  app.shell.currentChanged?.connect(onCurrentChanged);
595
575
 
596
- // Also surface it in the palette (acts on the active editor); the file-tab
597
- // context-menu entry is declared in `schema/plugin.json`.
598
576
  const paletteItem = palette?.addItem({
599
577
  command: CommandIDs.revealInFileTree,
600
578
  category: trans.__('File Browser')
601
579
  });
602
580
 
603
- // The static items are declared in `schema/plugin.json` under
604
- // `jupyter.lab.menus.context` so users can override or disable them. The
605
- // schema also declares an empty submenu placeholder with id
606
- // `OPEN_WITH_SUBMENU_ID`; we fill it on every open from the document
607
- // factories that can open the right-clicked file.
581
+ // Static context-menu items live in `schema/plugin.json` so users can
582
+ // override them; the schema declares the empty "Open With" placeholder.
608
583
  const updateOpenWithMenu = makeOpenWithUpdater(app, browser);
609
584
  app.contextMenu.opened.connect(updateOpenWithMenu);
610
585
 
@@ -617,9 +592,8 @@ export function registerCommands(opts: IRegisterCommandsOptions): () => void {
617
592
  }
618
593
 
619
594
  /**
620
- * The names we register with the toolbar. Kept distinct from
621
- * {@link CommandIDs} because the toolbar API uses opaque names rather
622
- * than commands.
595
+ * Toolbar item names, distinct from {@link CommandIDs}: the toolbar API
596
+ * uses opaque names rather than commands.
623
597
  */
624
598
  export namespace ToolbarNames {
625
599
  export const newLauncher = 'new-launcher';
@@ -630,10 +604,8 @@ export namespace ToolbarNames {
630
604
  }
631
605
 
632
606
  /**
633
- * Populate the file browser's toolbar with the buttons that mirror the
634
- * default JupyterLab file browser: a "+" launcher button, a new-folder
635
- * button, a refresh button, a collapse-all button, and the file filter
636
- * toggle.
607
+ * Populate the toolbar with the buttons that mirror the default JupyterLab
608
+ * file browser.
637
609
  */
638
610
  export function populateToolbar(opts: {
639
611
  app: JupyterFrontEnd;