xtralab 0.15.0 → 0.15.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.
Files changed (237) hide show
  1. package/README.md +65 -30
  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.js +15 -0
  29. package/lib/editorBreadcrumbs/index.d.ts +3 -6
  30. package/lib/editorBreadcrumbs/index.js +10 -6
  31. package/lib/editorBreadcrumbs/widget.d.ts +17 -4
  32. package/lib/editorBreadcrumbs/widget.js +11 -17
  33. package/lib/editorIndent/index.d.ts +4 -12
  34. package/lib/editorIndent/index.js +8 -23
  35. package/lib/fileBrowser/commands.d.ts +29 -19
  36. package/lib/fileBrowser/commands.js +83 -97
  37. package/lib/fileBrowser/contents.d.ts +3 -0
  38. package/lib/fileBrowser/dragAndDrop.d.ts +17 -4
  39. package/lib/fileBrowser/dragAndDrop.js +2 -7
  40. package/lib/fileBrowser/fileBrowser.d.ts +19 -9
  41. package/lib/fileBrowser/fileBrowser.js +70 -222
  42. package/lib/fileBrowser/gitStatus.d.ts +16 -10
  43. package/lib/fileBrowser/gitStatus.js +31 -30
  44. package/lib/fileBrowser/gitignore.d.ts +4 -17
  45. package/lib/fileBrowser/gitignore.js +4 -25
  46. package/lib/fileBrowser/icons.d.ts +7 -17
  47. package/lib/fileBrowser/icons.js +25 -80
  48. package/lib/fileBrowser/index.js +2 -4
  49. package/lib/fileBrowser/widget.d.ts +141 -48
  50. package/lib/fileBrowser/widget.js +102 -4
  51. package/lib/fileTypeIcons/index.js +0 -10
  52. package/lib/git/api.d.ts +5 -13
  53. package/lib/git/api.js +10 -36
  54. package/lib/git/askRequest.d.ts +4 -8
  55. package/lib/git/askRequest.js +6 -20
  56. package/lib/git/commands.d.ts +45 -5
  57. package/lib/git/commands.js +20 -13
  58. package/lib/git/diffModel.js +4 -0
  59. package/lib/git/diffPreferences.d.ts +90 -0
  60. package/lib/git/diffPreferences.js +107 -0
  61. package/lib/git/diffProvider.js +10 -2
  62. package/lib/git/diffSurface.d.ts +64 -63
  63. package/lib/git/diffSurface.js +72 -213
  64. package/lib/git/diffTheme.d.ts +7 -0
  65. package/lib/git/diffTheme.js +23 -0
  66. package/lib/git/diffWidget.d.ts +70 -14
  67. package/lib/git/diffWidget.js +100 -94
  68. package/lib/git/imageDiff.d.ts +14 -6
  69. package/lib/git/imageDiff.js +19 -75
  70. package/lib/git/index.d.ts +0 -3
  71. package/lib/git/index.js +44 -10
  72. package/lib/git/notebookDiff.d.ts +110 -33
  73. package/lib/git/notebookDiff.js +55 -197
  74. package/lib/git/tokens.d.ts +73 -31
  75. package/lib/git/tokens.js +2 -3
  76. package/lib/highlight/index.d.ts +4 -10
  77. package/lib/highlight/index.js +10 -41
  78. package/lib/index.d.ts +0 -9
  79. package/lib/index.js +0 -9
  80. package/lib/launcher/agents.d.ts +55 -51
  81. package/lib/launcher/agents.js +15 -57
  82. package/lib/launcher/availability.d.ts +3 -9
  83. package/lib/launcher/availability.js +3 -9
  84. package/lib/launcher/commands.d.ts +7 -32
  85. package/lib/launcher/commands.js +14 -45
  86. package/lib/launcher/dashboard.d.ts +19 -24
  87. package/lib/launcher/dashboard.js +21 -67
  88. package/lib/launcher/editorRegistry.d.ts +10 -22
  89. package/lib/launcher/editorRegistry.js +14 -18
  90. package/lib/launcher/editors.d.ts +39 -45
  91. package/lib/launcher/editors.js +8 -34
  92. package/lib/launcher/icons.d.ts +0 -12
  93. package/lib/launcher/icons.js +5 -39
  94. package/lib/launcher/index.js +12 -55
  95. package/lib/launcher/invocation.d.ts +4 -10
  96. package/lib/launcher/invocation.js +7 -22
  97. package/lib/launcher/registry.d.ts +9 -7
  98. package/lib/launcher/registry.js +9 -7
  99. package/lib/launcher/tokens.d.ts +10 -22
  100. package/lib/launcher/tokens.js +5 -8
  101. package/lib/menuBar/index.d.ts +3 -10
  102. package/lib/menuBar/index.js +11 -28
  103. package/lib/menus/index.d.ts +3 -9
  104. package/lib/menus/index.js +14 -30
  105. package/lib/omnibox/files.d.ts +2 -4
  106. package/lib/omnibox/files.js +5 -11
  107. package/lib/omnibox/index.d.ts +4 -14
  108. package/lib/omnibox/index.js +9 -27
  109. package/lib/omnibox/model.d.ts +38 -10
  110. package/lib/omnibox/model.js +15 -33
  111. package/lib/omnibox/recents.d.ts +15 -17
  112. package/lib/omnibox/recents.js +12 -17
  113. package/lib/omnibox/tokens.d.ts +5 -10
  114. package/lib/omnibox/tokens.js +2 -6
  115. package/lib/omnibox/widget.d.ts +11 -7
  116. package/lib/omnibox/widget.js +8 -16
  117. package/lib/searchReplace/index.d.ts +4 -20
  118. package/lib/searchReplace/index.js +7 -27
  119. package/lib/showOutput/index.d.ts +3 -12
  120. package/lib/showOutput/index.js +6 -22
  121. package/lib/sidebar/index.d.ts +4 -7
  122. package/lib/sidebar/index.js +5 -12
  123. package/lib/terminalNotifications/index.d.ts +4 -10
  124. package/lib/terminalNotifications/index.js +6 -20
  125. package/lib/terminals/agentTerminals.d.ts +12 -5
  126. package/lib/terminals/agentTerminals.js +25 -44
  127. package/lib/terminals/detection.d.ts +4 -8
  128. package/lib/terminals/detection.js +4 -8
  129. package/lib/terminals/index.d.ts +0 -35
  130. package/lib/terminals/index.js +12 -101
  131. package/lib/terminals/model.d.ts +64 -139
  132. package/lib/terminals/model.js +80 -220
  133. package/lib/terminals/tokens.d.ts +11 -22
  134. package/lib/terminals/tokens.js +2 -2
  135. package/lib/terminals/widget.d.ts +43 -13
  136. package/lib/terminals/widget.js +33 -18
  137. package/lib/topBar/icons.d.ts +3 -8
  138. package/lib/topBar/icons.js +3 -8
  139. package/lib/topBar/index.d.ts +4 -11
  140. package/lib/topBar/index.js +10 -38
  141. package/lib/walkthrough/index.d.ts +4 -9
  142. package/lib/walkthrough/index.js +6 -20
  143. package/lib/walkthrough/panel.d.ts +40 -9
  144. package/lib/walkthrough/panel.js +4 -10
  145. package/package.json +5 -3
  146. package/schema/git-diff-preferences.json +42 -0
  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 +15 -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 +119 -96
  165. package/src/fileBrowser/contents.ts +3 -0
  166. package/src/fileBrowser/dragAndDrop.ts +17 -7
  167. package/src/fileBrowser/fileBrowser.tsx +86 -230
  168. package/src/fileBrowser/gitStatus.ts +36 -33
  169. package/src/fileBrowser/gitignore.ts +4 -25
  170. package/src/fileBrowser/icons.ts +25 -80
  171. package/src/fileBrowser/index.ts +2 -4
  172. package/src/fileBrowser/widget.tsx +171 -50
  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 +83 -11
  177. package/src/git/diffModel.ts +25 -0
  178. package/src/git/diffPreferences.ts +192 -0
  179. package/src/git/diffProvider.tsx +11 -2
  180. package/src/git/diffSurface.tsx +165 -286
  181. package/src/git/diffTheme.ts +24 -0
  182. package/src/git/diffWidget.tsx +194 -134
  183. package/src/git/imageDiff.tsx +56 -84
  184. package/src/git/index.ts +54 -10
  185. package/src/git/notebookDiff.tsx +214 -240
  186. package/src/git/tokens.ts +73 -31
  187. package/src/highlight/index.ts +10 -41
  188. package/src/index.ts +0 -9
  189. package/src/launcher/agents.ts +64 -95
  190. package/src/launcher/availability.ts +3 -9
  191. package/src/launcher/commands.ts +14 -51
  192. package/src/launcher/dashboard.tsx +49 -88
  193. package/src/launcher/editorRegistry.ts +21 -29
  194. package/src/launcher/editors.ts +39 -58
  195. package/src/launcher/icons.ts +5 -39
  196. package/src/launcher/index.ts +12 -55
  197. package/src/launcher/invocation.ts +7 -22
  198. package/src/launcher/registry.ts +9 -7
  199. package/src/launcher/tokens.ts +10 -22
  200. package/src/menuBar/index.ts +11 -28
  201. package/src/menus/index.ts +14 -30
  202. package/src/omnibox/files.ts +10 -14
  203. package/src/omnibox/index.ts +9 -27
  204. package/src/omnibox/model.ts +50 -38
  205. package/src/omnibox/recents.ts +17 -22
  206. package/src/omnibox/tokens.ts +5 -10
  207. package/src/omnibox/widget.tsx +14 -19
  208. package/src/searchReplace/index.ts +7 -27
  209. package/src/showOutput/index.ts +6 -22
  210. package/src/sidebar/index.ts +20 -12
  211. package/src/terminalNotifications/index.ts +42 -25
  212. package/src/terminals/agentTerminals.ts +25 -49
  213. package/src/terminals/detection.ts +4 -8
  214. package/src/terminals/index.ts +12 -101
  215. package/src/terminals/model.ts +130 -246
  216. package/src/terminals/tokens.ts +11 -22
  217. package/src/terminals/widget.tsx +48 -31
  218. package/src/topBar/icons.ts +3 -8
  219. package/src/topBar/index.ts +21 -44
  220. package/src/walkthrough/index.ts +6 -20
  221. package/src/walkthrough/panel.ts +40 -10
  222. package/style/about.css +2 -7
  223. package/style/askAgent.css +8 -40
  224. package/style/base.css +9 -45
  225. package/style/chrome.css +108 -308
  226. package/style/commandBar.css +4 -20
  227. package/style/customPanel.css +1 -4
  228. package/style/git.css +26 -186
  229. package/style/highlight.css +1 -3
  230. package/style/launcher.css +6 -70
  231. package/style/omnibox.css +2 -14
  232. package/style/showOutput.css +0 -6
  233. package/style/sidebar.css +29 -9
  234. package/style/tabs.css +5 -13
  235. package/style/terminals.css +6 -53
  236. package/style/topBar.css +2 -22
  237. package/style/walkthrough.css +0 -8
@@ -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,
@@ -21,6 +21,7 @@ import {
21
21
  fileIcon,
22
22
  filterIcon,
23
23
  IDisposableMenuItem,
24
+ LabIcon,
24
25
  newFolderIcon,
25
26
  RankedMenu,
26
27
  refreshIcon
@@ -28,19 +29,24 @@ import {
28
29
  import { ReadonlyPartialJSONObject } from '@lumino/coreutils';
29
30
  import { ContextMenu, Widget } from '@lumino/widgets';
30
31
 
32
+ import {
33
+ CommandArguments as GitCommandArguments,
34
+ CommandIDs as GitCommandIDs
35
+ } from '../git/commands';
36
+ import type { IFileChange } from '../git/tokens';
31
37
  import { toCanonicalPath, toServerPath } from './contents';
38
+ import { GIT_REPO_PATH } from './gitStatus';
32
39
  import { FILE_BROWSER_ID, IXtralabFileBrowser } from './widget';
33
40
 
34
41
  /**
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.
42
+ * Command ids for the xtralab browser. Namespaced `xtralab:` rather than
43
+ * reusing the `filebrowser:` ids: the core handlers resolve their target via
44
+ * `IFileBrowserFactory.tracker`, which never sees this widget.
40
45
  */
41
46
  export namespace CommandIDs {
42
47
  export const open = 'xtralab:open';
43
48
  export const openBrowserTab = 'xtralab:open-browser-tab';
49
+ export const openGitDiff = 'xtralab:open-git-diff';
44
50
  export const rename = 'xtralab:rename';
45
51
  export const del = 'xtralab:delete';
46
52
  export const duplicate = 'xtralab:duplicate';
@@ -56,27 +62,40 @@ export namespace CommandIDs {
56
62
  }
57
63
 
58
64
  /**
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.
65
+ * "Open With" submenu id. Distinct from the core `jp-contextmenu-open-with`
66
+ * so the core's populator does not fill ours from its own selection.
63
67
  */
64
68
  const OPEN_WITH_SUBMENU_ID = 'jp-contextmenu-xtralab-open-with';
65
69
 
70
+ /**
71
+ * Options for registering the xtralab file browser commands.
72
+ */
66
73
  interface IRegisterCommandsOptions {
74
+ /**
75
+ * The JupyterLab application the commands are registered on.
76
+ */
67
77
  app: JupyterFrontEnd;
78
+ /**
79
+ * The file browser widget the commands act on.
80
+ */
68
81
  browser: IXtralabFileBrowser;
82
+ /**
83
+ * The document manager used to resolve the current widget's file path.
84
+ */
69
85
  docManager: IDocumentManager;
86
+ /**
87
+ * The command palette the commands are added to, or `null` when unavailable.
88
+ */
70
89
  palette: ICommandPalette | null;
90
+ /**
91
+ * The translator for user-facing strings; `null` falls back to English.
92
+ */
71
93
  translator: ITranslator | null;
72
94
  }
73
95
 
74
96
  /**
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).
97
+ * Path of the right-clicked item, from the contextmenu event's target;
98
+ * falls back to the tree selection when invoked without one (the palette).
80
99
  */
81
100
  function getTargetPath(
82
101
  app: JupyterFrontEnd,
@@ -92,11 +111,6 @@ function getTargetPath(
92
111
  return browser.selectedPaths[0];
93
112
  }
94
113
 
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
114
  function getTargetKind(app: JupyterFrontEnd): 'file' | 'folder' | undefined {
101
115
  const node = app.contextMenuHitTest(
102
116
  n => n.dataset !== undefined && n.dataset.type === 'item'
@@ -106,14 +120,9 @@ function getTargetKind(app: JupyterFrontEnd): 'file' | 'folder' | undefined {
106
120
  }
107
121
 
108
122
  /**
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.
123
+ * Canonical paths "Open" acts on: the whole selection when the right-clicked
124
+ * row is part of it (matching the default browser), else just that row.
125
+ * Folders are filtered out — this tree has no "current directory".
117
126
  */
118
127
  function getOpenPaths(
119
128
  app: JupyterFrontEnd,
@@ -135,8 +144,22 @@ function getOpenPaths(
135
144
  }
136
145
 
137
146
  /**
138
- * True iff there is an actionable target for a command on the right-click.
147
+ * Git changes of the target file, at most one staged and one unstaged.
148
+ * Status paths are repo-relative; like the tree's git badges, this assumes
149
+ * the repo root is the server root.
139
150
  */
151
+ function getTargetChanges(
152
+ app: JupyterFrontEnd,
153
+ browser: IXtralabFileBrowser
154
+ ): IFileChange[] {
155
+ const targetPath = getTargetPath(app, browser);
156
+ if (targetPath === undefined) {
157
+ return [];
158
+ }
159
+ const serverPath = toServerPath(targetPath);
160
+ return browser.gitChanges.filter(change => change.path === serverPath);
161
+ }
162
+
140
163
  function hasTarget(
141
164
  app: JupyterFrontEnd,
142
165
  browser: IXtralabFileBrowser
@@ -145,11 +168,9 @@ function hasTarget(
145
168
  }
146
169
 
147
170
  /**
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".
171
+ * Working directory for create actions: a selected folder is its own cwd, a
172
+ * file falls back to its parent; empty string (the contents-API root) when
173
+ * nothing is selected.
153
174
  */
154
175
  function getWorkingDirectory(browser: IXtralabFileBrowser): string {
155
176
  const first = browser.selectedPaths[0];
@@ -164,14 +185,10 @@ function getWorkingDirectory(browser: IXtralabFileBrowser): string {
164
185
  }
165
186
 
166
187
  /**
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.
188
+ * Main-area widget whose tab is the target of the current context-menu
189
+ * event, or `null`. `contextMenuHitTest` reads the last context-menu event,
190
+ * which is not cleared when the menu closes, so callers must gate against
191
+ * resolving a stale tab.
175
192
  */
176
193
  function contextMenuTabWidget(app: JupyterFrontEnd): Widget | null {
177
194
  const node = app.contextMenuHitTest(
@@ -190,10 +207,9 @@ function contextMenuTabWidget(app: JupyterFrontEnd): Widget | null {
190
207
  }
191
208
 
192
209
  /**
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.
210
+ * Canonical path of the document to reveal: the right-clicked tab when
211
+ * `fromContextMenu` is true (the file-tab menu), else the active main-area
212
+ * widget (the palette).
197
213
  */
198
214
  function documentPathToReveal(
199
215
  app: JupyterFrontEnd,
@@ -211,14 +227,10 @@ function documentPathToReveal(
211
227
  }
212
228
 
213
229
  /**
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
- *
230
+ * Build the populator that fills the "Open With" submenu on every
231
+ * context-menu open, mirroring `filebrowser-extension:open-with`.
220
232
  * `preferredWidgetFactories(path)` is path-only, so the populator stays
221
- * synchronous and the submenu is ready before the user can hover over it.
233
+ * synchronous and the submenu is ready before the user can hover.
222
234
  */
223
235
  function makeOpenWithUpdater(
224
236
  app: JupyterFrontEnd,
@@ -255,17 +267,26 @@ function makeOpenWithUpdater(
255
267
  command: CommandIDs.open
256
268
  })
257
269
  );
270
+
271
+ const changes = getTargetChanges(app, browser);
272
+ if (changes.length > 0) {
273
+ items.push(submenu.addItem({ type: 'separator' }));
274
+ for (const change of changes) {
275
+ items.push(
276
+ submenu.addItem({
277
+ args: { group: change.group },
278
+ command: CommandIDs.openGitDiff
279
+ })
280
+ );
281
+ }
282
+ }
258
283
  };
259
284
  }
260
285
 
261
286
  /**
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.
287
+ * Register the xtralab commands, context-menu wiring, and the dynamic
288
+ * "Open With" populator. Returns a detach function; the commands themselves
289
+ * stay owned by the registry.
269
290
  */
270
291
  export function registerCommands(opts: IRegisterCommandsOptions): () => void {
271
292
  const { app, browser, docManager, palette, translator } = opts;
@@ -320,6 +341,32 @@ export function registerCommands(opts: IRegisterCommandsOptions): () => void {
320
341
  }
321
342
  });
322
343
 
344
+ commands.addCommand(CommandIDs.openGitDiff, {
345
+ label: args =>
346
+ args.group === 'staged'
347
+ ? trans.__('Git Diff (Staged)')
348
+ : trans.__('Git Diff (Working)'),
349
+ icon: LabIcon.resolve({ icon: 'git:diff' }).bindprops({
350
+ stylesheet: 'menuItem'
351
+ }),
352
+ execute: async (args: ReadonlyPartialJSONObject) => {
353
+ const change = getTargetChanges(app, browser).find(
354
+ candidate => candidate.group === args.group
355
+ );
356
+ if (change === undefined) {
357
+ return;
358
+ }
359
+ const diffArgs: GitCommandArguments.IOpenDiff = {
360
+ repoPath: GIT_REPO_PATH,
361
+ change
362
+ };
363
+ return commands.execute(
364
+ GitCommandIDs.openDiff,
365
+ diffArgs as unknown as ReadonlyPartialJSONObject
366
+ );
367
+ }
368
+ });
369
+
323
370
  commands.addCommand(CommandIDs.rename, {
324
371
  label: trans.__('Rename…'),
325
372
  icon: editIcon.bindprops({ stylesheet: 'menuItem' }),
@@ -496,23 +543,16 @@ export function registerCommands(opts: IRegisterCommandsOptions): () => void {
496
543
  }
497
544
  });
498
545
 
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.
546
+ // The filter can also auto-show when typing with the tree focused, so
547
+ // track visibility changes rather than the command's own executions.
502
548
  const onFilterVisibleChanged = (): void => {
503
549
  commands.notifyCommandChanged(CommandIDs.toggleFileFilter);
504
550
  };
505
551
  browser.fileFilterVisibleChanged.connect(onFilterVisibleChanged);
506
552
 
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.
515
- // The tree is movable, so surface whichever sidebar widget holds it.
553
+ // Public reveal seam for other plugins; an empty path is the workspace-root
554
+ // gesture (the root has no tree row of its own). The tree is movable, so
555
+ // surface whichever sidebar widget holds it.
516
556
  const activateTreeHost = (): void => {
517
557
  for (const area of ['left', 'right']) {
518
558
  for (const widget of app.shell.widgets(area)) {
@@ -538,8 +578,6 @@ export function registerCommands(opts: IRegisterCommandsOptions): () => void {
538
578
  }
539
579
  });
540
580
 
541
- // Reveal the resolved document in the tree browser via the reveal-path
542
- // command, which surfaces the sidebar and scrolls to the target.
543
581
  commands.addCommand(CommandIDs.revealInFileTree, {
544
582
  label: trans.__('Show in File Tree'),
545
583
  caption: trans.__('Show this file in the xtralab file browser'),
@@ -579,10 +617,8 @@ export function registerCommands(opts: IRegisterCommandsOptions): () => void {
579
617
  commands.addCommand(CommandIDs.newLauncher, {
580
618
  label: trans.__('New Launcher'),
581
619
  caption: trans.__('Open a new launcher'),
582
- // Skip the `menuItem` bindprops the other commands use: this command
583
- // lives on the toolbar, where the launcher-extension's blue button
584
- // styling expects the raw `jp-icon3` paths from `addIcon` so it can
585
- // recolor them against the brand background.
620
+ // Raw `addIcon`, no `menuItem` bindprops: the launcher-extension's blue
621
+ // toolbar-button styling recolors the raw `jp-icon3` paths.
586
622
  icon: addIcon,
587
623
  execute: () => {
588
624
  const cwd = getWorkingDirectory(browser);
@@ -590,34 +626,24 @@ export function registerCommands(opts: IRegisterCommandsOptions): () => void {
590
626
  }
591
627
  });
592
628
 
593
- // Re-evaluate enabled/visible state when the user changes the tree
594
- // selection, in case a command's predicate depends on the selection.
595
629
  browser.selectionChanged.connect(() => {
596
630
  for (const id of Object.values(CommandIDs)) {
597
631
  commands.notifyCommandChanged(id);
598
632
  }
599
633
  });
600
634
 
601
- // This command's enabled state tracks the active widget, not the tree
602
- // selection, so re-evaluate it on current-widget changes. `currentChanged`
603
- // is optional on the shell interface.
604
635
  const onCurrentChanged = (): void => {
605
636
  commands.notifyCommandChanged(CommandIDs.revealInFileTree);
606
637
  };
607
638
  app.shell.currentChanged?.connect(onCurrentChanged);
608
639
 
609
- // Also surface it in the palette (acts on the active editor); the file-tab
610
- // context-menu entry is declared in `schema/plugin.json`.
611
640
  const paletteItem = palette?.addItem({
612
641
  command: CommandIDs.revealInFileTree,
613
642
  category: trans.__('File Browser')
614
643
  });
615
644
 
616
- // The static items are declared in `schema/plugin.json` under
617
- // `jupyter.lab.menus.context` so users can override or disable them. The
618
- // schema also declares an empty submenu placeholder with id
619
- // `OPEN_WITH_SUBMENU_ID`; we fill it on every open from the document
620
- // factories that can open the right-clicked file.
645
+ // Static context-menu items live in `schema/plugin.json` so users can
646
+ // override them; the schema declares the empty "Open With" placeholder.
621
647
  const updateOpenWithMenu = makeOpenWithUpdater(app, browser);
622
648
  app.contextMenu.opened.connect(updateOpenWithMenu);
623
649
 
@@ -630,9 +656,8 @@ export function registerCommands(opts: IRegisterCommandsOptions): () => void {
630
656
  }
631
657
 
632
658
  /**
633
- * The names we register with the toolbar. Kept distinct from
634
- * {@link CommandIDs} because the toolbar API uses opaque names rather
635
- * than commands.
659
+ * Toolbar item names, distinct from {@link CommandIDs}: the toolbar API
660
+ * uses opaque names rather than commands.
636
661
  */
637
662
  export namespace ToolbarNames {
638
663
  export const newLauncher = 'new-launcher';
@@ -643,10 +668,8 @@ export namespace ToolbarNames {
643
668
  }
644
669
 
645
670
  /**
646
- * Populate the file browser's toolbar with the buttons that mirror the
647
- * default JupyterLab file browser: a "+" launcher button, a new-folder
648
- * button, a refresh button, a collapse-all button, and the file filter
649
- * toggle.
671
+ * Populate the toolbar with the buttons that mirror the default JupyterLab
672
+ * file browser.
650
673
  */
651
674
  export function populateToolbar(opts: {
652
675
  app: JupyterFrontEnd;
@@ -60,6 +60,9 @@ export function toServerPath(canonicalPath: string): string {
60
60
  : canonicalPath;
61
61
  }
62
62
 
63
+ /**
64
+ * The immediate children of a directory, as canonical `@pierre/trees` paths.
65
+ */
63
66
  interface IListedDirectory {
64
67
  /**
65
68
  * Canonical paths for every immediate child of the requested directory.
@@ -14,7 +14,13 @@ import { ROOT_LOAD_KEY, canonicalBasename, parentOf } from './contents';
14
14
  * One tree-model move a drop performs, as canonical paths.
15
15
  */
16
16
  export interface IDropMove {
17
+ /**
18
+ * The canonical path of the dragged item.
19
+ */
17
20
  from: string;
21
+ /**
22
+ * The canonical destination path: its basename under the target directory.
23
+ */
18
24
  to: string;
19
25
  }
20
26
 
@@ -55,9 +61,6 @@ export function computeDropMoves(context: FileTreeDropContext): IDropMove[] {
55
61
  .map(path => ({ from: path, to: `${dir}${canonicalBasename(path)}` }));
56
62
  }
57
63
 
58
- /**
59
- * The drop target the tree reports for a move to the workspace root.
60
- */
61
64
  const ROOT_DROP_TARGET: FileTreeDropTarget = {
62
65
  directoryPath: null,
63
66
  flattenedSegmentPath: null,
@@ -92,8 +95,17 @@ export function createTreeDragAndDropConfig(
92
95
  };
93
96
  }
94
97
 
98
+ /**
99
+ * Options for {@link useRootDropZone}.
100
+ */
95
101
  interface IRootDropZoneOptions {
102
+ /**
103
+ * The tree model, queried for the selection at drag start.
104
+ */
96
105
  model: FileTree;
106
+ /**
107
+ * The drop handler ref, filled by the contents-sync effect.
108
+ */
97
109
  handlerRef: React.RefObject<ITreeDropHandler | null>;
98
110
  /**
99
111
  * The light-DOM wrapper around the tree host; composed drag events
@@ -104,10 +116,8 @@ interface IRootDropZoneOptions {
104
116
 
105
117
  /**
106
118
  * Accept drops on the empty space below the last row as moves to the
107
- * workspace root — the tree resolves a drop target only while the
108
- * cursor is over a row and silently discards such drops. dragstart
109
- * records the dragged paths; a drop whose composed path contains no
110
- * row hands them over as a root move.
119
+ * workspace root — the tree resolves a drop target only while the cursor
120
+ * is over a row and silently discards such drops.
111
121
  */
112
122
  export function useRootDropZone(options: IRootDropZoneOptions): void {
113
123
  const { model, handlerRef, wrapperRef } = options;