xtralab 0.7.0 → 0.9.0

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 (54) hide show
  1. package/README.md +18 -10
  2. package/lib/fileBrowser/commands.d.ts +7 -0
  3. package/lib/fileBrowser/commands.js +74 -1
  4. package/lib/fileBrowser/fileBrowser.js +4 -1
  5. package/lib/fileBrowser/icons.d.ts +5 -9
  6. package/lib/fileBrowser/icons.js +97 -13
  7. package/lib/fileBrowser/index.js +5 -3
  8. package/lib/fileTypeIcons/index.d.ts +6 -0
  9. package/lib/fileTypeIcons/index.js +144 -0
  10. package/lib/git/commands.d.ts +34 -33
  11. package/lib/git/commands.js +132 -10
  12. package/lib/git/diffModel.d.ts +14 -0
  13. package/lib/git/diffModel.js +75 -0
  14. package/lib/git/diffProvider.d.ts +0 -5
  15. package/lib/git/diffProvider.js +6 -284
  16. package/lib/git/diffSurface.d.ts +64 -25
  17. package/lib/git/diffSurface.js +84 -27
  18. package/lib/git/diffTheme.d.ts +9 -0
  19. package/lib/git/diffTheme.js +31 -0
  20. package/lib/git/diffWidget.d.ts +67 -85
  21. package/lib/git/diffWidget.js +242 -318
  22. package/lib/git/index.d.ts +1 -6
  23. package/lib/git/index.js +7 -23
  24. package/lib/git/notebookDiff.d.ts +6 -1
  25. package/lib/git/notebookDiff.js +4 -3
  26. package/lib/index.js +2 -0
  27. package/lib/terminals/model.d.ts +1 -1
  28. package/lib/terminals/model.js +1 -1
  29. package/lib/terminals/widget.d.ts +1 -1
  30. package/lib/terminals/widget.js +1 -1
  31. package/package.json +3 -3
  32. package/schema/plugin.json +6 -0
  33. package/src/fileBrowser/commands.ts +91 -2
  34. package/src/fileBrowser/fileBrowser.tsx +4 -1
  35. package/src/fileBrowser/icons.ts +103 -13
  36. package/src/fileBrowser/index.ts +9 -3
  37. package/src/fileTypeIcons/index.ts +174 -0
  38. package/src/git/commands.ts +177 -43
  39. package/src/git/diffModel.ts +102 -0
  40. package/src/git/diffProvider.tsx +8 -409
  41. package/src/git/diffSurface.tsx +187 -35
  42. package/src/git/diffTheme.ts +40 -0
  43. package/src/git/diffWidget.tsx +338 -393
  44. package/src/git/index.ts +14 -28
  45. package/src/git/notebookDiff.tsx +10 -3
  46. package/src/index.ts +2 -0
  47. package/src/terminals/model.ts +1 -1
  48. package/src/terminals/widget.tsx +1 -1
  49. package/style/base.css +1 -0
  50. package/style/git.css +40 -12
  51. package/style/index.css +1 -0
  52. package/style/index.js +1 -0
  53. package/style/launcher.css +46 -18
  54. package/style/sidebar.css +14 -0
@@ -1,12 +1,7 @@
1
1
  import { JupyterFrontEndPlugin } from '@jupyterlab/application';
2
2
  import { CommandArguments, CommandIDs } from './commands';
3
3
  /**
4
- * Every plugin contributed by xtralab's git integration:
5
- *
6
- * 1. {@link diffCommandPlugin} — the launcher's `xtralab:git:open-diff`.
7
- * 2. `diffProviderPlugin` — registers the `@pierre/diffs` rendering as
8
- * jupyterlab-git's diff providers (replacing its disabled
9
- * notebook/text diff plugins).
4
+ * Plugins contributed by xtralab's git integration.
10
5
  */
11
6
  declare const plugins: JupyterFrontEndPlugin<unknown>[];
12
7
  export { CommandArguments, CommandIDs };
package/lib/git/index.js CHANGED
@@ -1,19 +1,14 @@
1
1
  import { IThemeManager, WidgetTracker } from '@jupyterlab/apputils';
2
2
  import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
3
- import { CommandIDs, registerGitCommands } from './commands';
4
- import { PREVIEW_DIFF_WIDGET_ID, pinnedDiffWidgetId } from './diffWidget';
3
+ import { CommandIDs, PREVIEW_DIFF_WIDGET_ID, pinnedDiffWidgetId, registerGitCommands } from './commands';
5
4
  import diffProviderPlugin from './diffProvider';
6
5
  const GIT_DIFF_COMMAND_PLUGIN_ID = 'xtralab:git-diff-command';
7
6
  const GIT_DIFF_TRACKER_NAMESPACE = 'xtralab-git-diff';
8
7
  /**
9
8
  * The launcher's git diff command plugin.
10
9
  *
11
- * The `@jupyterlab/git` frontend stays enabled (only its diff plugins are
12
- * swapped — see `diffProvider.tsx`), so the upstream git panel is what the
13
- * user sees in the sidebar. This plugin is the launcher dashboard's
14
- * independent diff path: the `xtralab:git:open-diff` command the
15
- * dashboard's "Changes" section calls, plus a tracker so reopening a diff
16
- * reveals the existing tab and the preview/pin behavior keeps working.
10
+ * The upstream git panel stays enabled; this plugin only gives the launcher
11
+ * its own `xtralab:git:open-diff` command and preview/pinned-tab tracking.
17
12
  */
18
13
  const diffCommandPlugin = {
19
14
  id: GIT_DIFF_COMMAND_PLUGIN_ID,
@@ -21,9 +16,7 @@ const diffCommandPlugin = {
21
16
  autoStart: true,
22
17
  optional: [IThemeManager, IRenderMimeRegistry],
23
18
  activate: (app, themeManager, rendermime) => {
24
- // Track open diff widgets so the openDiff handler can reveal an
25
- // already-open diff for the same file instead of creating a duplicate
26
- // (and so a promoted preview can find its pinned twin).
19
+ // Track open diffs so repeated opens reveal the existing tab.
27
20
  const tracker = new WidgetTracker({
28
21
  namespace: GIT_DIFF_TRACKER_NAMESPACE
29
22
  });
@@ -37,13 +30,9 @@ const diffCommandPlugin = {
37
30
  themeManager,
38
31
  contentsManager: app.serviceManager.contents,
39
32
  rendermime,
40
- // The launcher polls git status on its own cadence and the
41
- // jupyterlab-git panel auto-refreshes on the contents `fileChanged`
42
- // signal a hunk-discard save emits, so no extra refresh is needed.
43
- onChanged: () => undefined,
44
33
  trackDiff: widget => tracker.add(widget),
45
34
  onPinned: current => {
46
- const existing = findDiff(current.content.change, true);
35
+ const existing = findDiff(current.change, true);
47
36
  if (existing !== undefined &&
48
37
  existing !== current &&
49
38
  !existing.isDisposed) {
@@ -51,7 +40,7 @@ const diffCommandPlugin = {
51
40
  current.close();
52
41
  return;
53
42
  }
54
- current.id = pinnedDiffWidgetId(current.content.change);
43
+ current.id = pinnedDiffWidgetId(current.change);
55
44
  current.title.className = '';
56
45
  app.shell.activateById(current.id);
57
46
  },
@@ -60,12 +49,7 @@ const diffCommandPlugin = {
60
49
  }
61
50
  };
62
51
  /**
63
- * Every plugin contributed by xtralab's git integration:
64
- *
65
- * 1. {@link diffCommandPlugin} — the launcher's `xtralab:git:open-diff`.
66
- * 2. `diffProviderPlugin` — registers the `@pierre/diffs` rendering as
67
- * jupyterlab-git's diff providers (replacing its disabled
68
- * notebook/text diff plugins).
52
+ * Plugins contributed by xtralab's git integration.
69
53
  */
70
54
  const plugins = [
71
55
  diffCommandPlugin,
@@ -97,7 +97,7 @@ export declare function parseNotebook(text: string): INotebook | null;
97
97
  * nbformat allows multiline string fields (`source`, stream `text`,
98
98
  * `text/plain` outputs, …) to be either a single string or an array of
99
99
  * strings. Normalize to one string so equality checks and diffs see the
100
- * same shape regardless of how the writer chose to serialise it.
100
+ * same shape regardless of how the writer chose to serialize it.
101
101
  */
102
102
  export declare function joinMultiline(value: string | string[] | undefined): string;
103
103
  export declare function cellSource(cell: INotebookCell): string;
@@ -160,6 +160,11 @@ export declare function buildNotebookDiff(options: IBuildNotebookDiffOptions): I
160
160
  interface INotebookDiffViewProps {
161
161
  diff: INotebookDiffResult;
162
162
  dark: boolean;
163
+ /**
164
+ * Whether a Pierre JupyterLab theme is active; selects Pierre's rich
165
+ * highlighting vs the CSS-variable theme (see {@link resolveDiffTheme}).
166
+ */
167
+ pierreTheme: boolean;
163
168
  rendermime: IRenderMimeRegistry | null;
164
169
  }
165
170
  /**
@@ -5,6 +5,7 @@ import { MessageLoop } from '@lumino/messaging';
5
5
  import { Widget } from '@lumino/widgets';
6
6
  import { FileDiff } from '@pierre/diffs/react';
7
7
  import { parseDiffFromFile } from '@pierre/diffs';
8
+ import { resolveDiffTheme } from './diffTheme';
8
9
  /**
9
10
  * The CSS class added to the root of the notebook diff. Selectors that
10
11
  * style cell sections, headers, and the per-cell diff blocks hang off this
@@ -50,7 +51,7 @@ function emptyNotebook() {
50
51
  * nbformat allows multiline string fields (`source`, stream `text`,
51
52
  * `text/plain` outputs, …) to be either a single string or an array of
52
53
  * strings. Normalize to one string so equality checks and diffs see the
53
- * same shape regardless of how the writer chose to serialise it.
54
+ * same shape regardless of how the writer chose to serialize it.
54
55
  */
55
56
  export function joinMultiline(value) {
56
57
  if (value === undefined) {
@@ -596,8 +597,8 @@ function MarkdownPreviewSection(props) {
596
597
  * the orientation clear without a separate column header.
597
598
  */
598
599
  export function NotebookDiffView(props) {
599
- const { diff, dark, rendermime } = props;
600
- const theme = dark ? 'pierre-dark' : 'pierre-light';
600
+ const { diff, dark, pierreTheme, rendermime } = props;
601
+ const theme = resolveDiffTheme(dark, pierreTheme);
601
602
  const summary = React.useMemo(() => summarize(diff.cells), [diff.cells]);
602
603
  return (React.createElement("div", { className: NOTEBOOK_DIFF_CSS_CLASS },
603
604
  React.createElement(NotebookDiffHeader, { summary: summary }),
package/lib/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import agentSessionsPlugin from './agentSessions';
2
2
  import fileBrowserPlugin from './fileBrowser';
3
+ import fileTypeIconsPlugin from './fileTypeIcons';
3
4
  import editorBreadcrumbsPlugin from './editorBreadcrumbs';
4
5
  import editorIndentPlugin from './editorIndent';
5
6
  import gitPlugins from './git';
@@ -22,6 +23,7 @@ const plugins = [
22
23
  editorBreadcrumbsPlugin,
23
24
  editorIndentPlugin,
24
25
  fileBrowserPlugin,
26
+ fileTypeIconsPlugin,
25
27
  ...gitPlugins,
26
28
  ...launcherPlugins,
27
29
  menusPlugin,
@@ -82,7 +82,7 @@ export declare class SessionRegistry implements IDisposable {
82
82
  agentCommandFor(name: string): string | null;
83
83
  /**
84
84
  * Return the open widget for a session, if any. The panel uses this to
85
- * switch behaviour between "activate existing tab" and "open a new tab
85
+ * switch behavior between "activate existing tab" and "open a new tab
86
86
  * connected to the live session".
87
87
  */
88
88
  widgetFor(name: string): TerminalWidget | null;
@@ -176,7 +176,7 @@ export class SessionRegistry {
176
176
  }
177
177
  /**
178
178
  * Return the open widget for a session, if any. The panel uses this to
179
- * switch behaviour between "activate existing tab" and "open a new tab
179
+ * switch behavior between "activate existing tab" and "open a new tab
180
180
  * connected to the live session".
181
181
  */
182
182
  widgetFor(name) {
@@ -18,7 +18,7 @@ export declare const RUNNING_TERMINALS_ID = "xtralab-running-terminals";
18
18
  * Backed by {@link SessionRegistry}: a `UseSignal` re-renders the list
19
19
  * whenever the registry emits `stateChanged`, so it tracks
20
20
  * `runningChanged` and the agent-title cache without any local state.
21
- * Click behaviour (activate vs. reopen, shutdown, new terminal) is
21
+ * Click behavior (activate vs. reopen, shutdown, new terminal) is
22
22
  * delegated to plugin-supplied callbacks so the widget never imports
23
23
  * `app`.
24
24
  */
@@ -17,7 +17,7 @@ export const RUNNING_TERMINALS_ID = 'xtralab-running-terminals';
17
17
  * Backed by {@link SessionRegistry}: a `UseSignal` re-renders the list
18
18
  * whenever the registry emits `stateChanged`, so it tracks
19
19
  * `runningChanged` and the agent-title cache without any local state.
20
- * Click behaviour (activate vs. reopen, shutdown, new terminal) is
20
+ * Click behavior (activate vs. reopen, shutdown, new terminal) is
21
21
  * delegated to plugin-supplied callbacks so the widget never imports
22
22
  * `app`.
23
23
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xtralab",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "An opinionated JupyterLab meta-package that bundles a curated set of extensions, ships a path-first file browser, renders jupyterlab-git's text, notebook and image diffs with its own viewers, and applies a quieter default workspace configuration.",
5
5
  "keywords": [
6
6
  "jupyter",
@@ -87,8 +87,8 @@
87
87
  "@lumino/polling": "^2.1.5",
88
88
  "@lumino/signaling": "^2.0.0",
89
89
  "@lumino/widgets": "^2.0.0",
90
- "@pierre/diffs": "^1.1.20",
91
- "@pierre/trees": "1.0.0-beta.3",
90
+ "@pierre/diffs": "^1.2.7",
91
+ "@pierre/trees": "1.0.0-beta.4",
92
92
  "detect-indent": "^7.0.2",
93
93
  "ignore": "^5.3.2",
94
94
  "react": "^18.2.0",
@@ -58,6 +58,12 @@
58
58
  "command": "xtralab:download",
59
59
  "selector": ".jp-xtralab-FileBrowser [data-type=\"item\"][data-item-type=\"file\"]",
60
60
  "rank": 10
61
+ },
62
+ {
63
+ "command": "xtralab:reveal-in-file-tree",
64
+ "args": { "fromTab": true },
65
+ "selector": "[data-type=\"document-title\"]",
66
+ "rank": 25
61
67
  }
62
68
  ]
63
69
  },
@@ -2,11 +2,14 @@ import { JupyterFrontEnd } from '@jupyterlab/application';
2
2
  import {
3
3
  Clipboard,
4
4
  Dialog,
5
+ ICommandPalette,
5
6
  InputDialog,
6
7
  showDialog,
7
8
  showErrorMessage
8
9
  } from '@jupyterlab/apputils';
9
10
  import { PageConfig, PathExt } from '@jupyterlab/coreutils';
11
+ import { IDocumentManager } from '@jupyterlab/docmanager';
12
+ import { ITranslator, nullTranslator } from '@jupyterlab/translation';
10
13
  import {
11
14
  addIcon,
12
15
  closeIcon,
@@ -22,7 +25,7 @@ import {
22
25
  refreshIcon
23
26
  } from '@jupyterlab/ui-components';
24
27
  import { ReadonlyPartialJSONObject } from '@lumino/coreutils';
25
- import { ContextMenu } from '@lumino/widgets';
28
+ import { ContextMenu, Widget } from '@lumino/widgets';
26
29
 
27
30
  import { toCanonicalPath, toServerPath } from './contents';
28
31
  import { FILE_BROWSER_ID, IXtralabFileBrowser } from './widget';
@@ -47,6 +50,7 @@ export namespace CommandIDs {
47
50
  export const createNewDirectory = 'xtralab:create-new-directory';
48
51
  export const newLauncher = 'xtralab:new-launcher';
49
52
  export const revealPath = 'xtralab:reveal-path';
53
+ export const revealInFileTree = 'xtralab:reveal-in-file-tree';
50
54
  }
51
55
 
52
56
  /**
@@ -60,6 +64,9 @@ export const OPEN_WITH_SUBMENU_ID = 'jp-contextmenu-xtralab-open-with';
60
64
  export interface IRegisterCommandsOptions {
61
65
  app: JupyterFrontEnd;
62
66
  browser: IXtralabFileBrowser;
67
+ docManager: IDocumentManager;
68
+ palette: ICommandPalette | null;
69
+ translator: ITranslator | null;
63
70
  }
64
71
 
65
72
  /**
@@ -152,6 +159,53 @@ function getWorkingDirectory(browser: IXtralabFileBrowser): string {
152
159
  return PathExt.dirname(serverPath);
153
160
  }
154
161
 
162
+ /**
163
+ * Resolve the main-area widget whose tab is the target of the current
164
+ * context-menu event, or `null` when that target is not a document tab. The
165
+ * shell stamps each tab's `data-id` with its widget id, so the right-clicked
166
+ * tab resolves even when it is not the current one.
167
+ *
168
+ * Valid only during a context-menu invocation: `contextMenuHitTest` reads the
169
+ * last context-menu event, which is not cleared when the menu closes, so
170
+ * {@link documentPathToReveal} gates this to avoid resolving a stale tab.
171
+ */
172
+ function contextMenuTabWidget(app: JupyterFrontEnd): Widget | null {
173
+ const node = app.contextMenuHitTest(
174
+ n => n.dataset !== undefined && n.dataset.type === 'document-title'
175
+ );
176
+ const id = node?.dataset.id;
177
+ if (id === undefined) {
178
+ return null;
179
+ }
180
+ for (const widget of app.shell.widgets('main')) {
181
+ if (widget.id === id) {
182
+ return widget;
183
+ }
184
+ }
185
+ return null;
186
+ }
187
+
188
+ /**
189
+ * Resolve the document to reveal as a canonical `@pierre/trees` path: the
190
+ * right-clicked tab when `fromContextMenu` is true (the file-tab menu),
191
+ * otherwise the active main-area widget (the palette). A file's context
192
+ * `path` is already canonical. Returns `undefined` when nothing resolves.
193
+ */
194
+ function documentPathToReveal(
195
+ app: JupyterFrontEnd,
196
+ docManager: IDocumentManager,
197
+ fromContextMenu: boolean
198
+ ): string | undefined {
199
+ const widget =
200
+ (fromContextMenu ? contextMenuTabWidget(app) : null) ??
201
+ app.shell.currentWidget;
202
+ if (widget === null) {
203
+ return undefined;
204
+ }
205
+ const path = docManager.contextForWidget(widget)?.path ?? '';
206
+ return path.length > 0 ? path : undefined;
207
+ }
208
+
155
209
  /**
156
210
  * Refresh the "Open With" submenu's items from the document factories that
157
211
  * can open the file under the right-click. Mirrors the
@@ -210,8 +264,9 @@ function makeOpenWithUpdater(
210
264
  * the plugin.
211
265
  */
212
266
  export function registerCommands(opts: IRegisterCommandsOptions): () => void {
213
- const { app, browser } = opts;
267
+ const { app, browser, docManager, palette, translator } = opts;
214
268
  const { commands } = app;
269
+ const trans = (translator ?? nullTranslator).load('jupyterlab');
215
270
 
216
271
  commands.addCommand(CommandIDs.open, {
217
272
  label: args =>
@@ -439,6 +494,23 @@ export function registerCommands(opts: IRegisterCommandsOptions): () => void {
439
494
  }
440
495
  });
441
496
 
497
+ // Reveal the resolved document in the tree browser via the reveal-path
498
+ // command, which surfaces the sidebar and scrolls to the target.
499
+ commands.addCommand(CommandIDs.revealInFileTree, {
500
+ label: trans.__('Show in File Tree'),
501
+ caption: trans.__('Show this file in the xtralab file browser'),
502
+ isEnabled: args =>
503
+ documentPathToReveal(app, docManager, args.fromTab === true) !==
504
+ undefined,
505
+ execute: args => {
506
+ const path = documentPathToReveal(app, docManager, args.fromTab === true);
507
+ if (path === undefined) {
508
+ return undefined;
509
+ }
510
+ return commands.execute(CommandIDs.revealPath, { path });
511
+ }
512
+ });
513
+
442
514
  commands.addCommand(CommandIDs.createNewDirectory, {
443
515
  label: 'New Folder',
444
516
  caption: 'Create a new folder',
@@ -479,6 +551,21 @@ export function registerCommands(opts: IRegisterCommandsOptions): () => void {
479
551
  }
480
552
  });
481
553
 
554
+ // This command's enabled state tracks the active widget, not the tree
555
+ // selection, so re-evaluate it on current-widget changes. `currentChanged`
556
+ // is optional on the shell interface.
557
+ const onCurrentChanged = (): void => {
558
+ commands.notifyCommandChanged(CommandIDs.revealInFileTree);
559
+ };
560
+ app.shell.currentChanged?.connect(onCurrentChanged);
561
+
562
+ // Also surface it in the palette (acts on the active editor); the file-tab
563
+ // context-menu entry is declared in `schema/plugin.json`.
564
+ const paletteItem = palette?.addItem({
565
+ command: CommandIDs.revealInFileTree,
566
+ category: trans.__('File Browser')
567
+ });
568
+
482
569
  // The static items are declared in `schema/plugin.json` under
483
570
  // `jupyter.lab.menus.context` so users can override or disable them. The
484
571
  // schema also declares an empty submenu placeholder with id
@@ -489,6 +576,8 @@ export function registerCommands(opts: IRegisterCommandsOptions): () => void {
489
576
 
490
577
  return () => {
491
578
  app.contextMenu.opened.disconnect(updateOpenWithMenu);
579
+ app.shell.currentChanged?.disconnect(onCurrentChanged);
580
+ paletteItem?.dispose();
492
581
  };
493
582
  }
494
583
 
@@ -652,7 +652,10 @@ export function FileBrowserComponent(
652
652
  previous?.deselect();
653
653
  }
654
654
  target.select();
655
- model.focusPath(canonicalPath);
655
+ // `select` highlights the row; `scrollToPath` focuses it and scrolls it
656
+ // to the middle of the viewport, even when the row is virtualized out of
657
+ // the rendered window.
658
+ model.scrollToPath(canonicalPath, { focus: true, offset: 'center' });
656
659
  };
657
660
 
658
661
  /**
@@ -1,7 +1,6 @@
1
1
  import { LabIcon, fileIcon } from '@jupyterlab/ui-components';
2
2
  import {
3
3
  createFileTreeIconResolver,
4
- getBuiltInFileIconColor,
5
4
  getBuiltInSpriteSheet
6
5
  } from '@pierre/trees';
7
6
  import type { FileTreeIconConfig } from '@pierre/trees';
@@ -121,28 +120,104 @@ function extractSymbol(
121
120
  }
122
121
 
123
122
  /**
124
- * Return a JupyterLab `LabIcon` matching the icon the xtralab file tree would
125
- * render for the given file path. Used by other plugins (e.g. the git diff
126
- * viewer) that want their main-area widget tabs to display the same per-file
127
- * glyph the tree shows in the sidebar.
123
+ * Fallback colors for the built-in file icons, keyed by the icon token
124
+ * `@pierre/trees` resolves for a path (e.g. `python`, `typescript`).
128
125
  *
129
- * Resolves the symbol via the same `@pierre/trees` resolver the tree itself
130
- * uses, then inlines the symbol's body into a self-contained SVG so the icon
131
- * works outside the tree's shadow DOM. Falls back to JupyterLab's default
132
- * `fileIcon` when the sprite cannot be located in either sheet.
126
+ * The tree only exposes these as `--trees-file-icon-color-<token>` CSS
127
+ * variables scoped to its shadow host, so they resolve to nothing for the
128
+ * standalone {@link LabIcon}s {@link getTreeIcon} builds for main-area tabs.
129
+ * Mirroring them here lets {@link getBuiltInFileIconColor} bottom out in a
130
+ * real color. Each is a `light-dark(…)` pair for dark themes.
133
131
  */
134
- export function getTreeIcon(filePath: string): LabIcon {
132
+ const BUILT_IN_FILE_ICON_COLOR_FALLBACKS: Record<string, string> = {
133
+ astro: 'light-dark(#a631be, #d568ea)',
134
+ babel: 'light-dark(#d5a910, #ffd452)',
135
+ bash: 'light-dark(#199f43, #5ecc71)',
136
+ biome: 'light-dark(#1a85d4, #69b1ff)',
137
+ bootstrap: 'light-dark(#693acf, #9d6afb)',
138
+ browserslist: 'light-dark(#d5a910, #ffd452)',
139
+ bun: 'light-dark(#594c5b, #79697b)',
140
+ c: 'light-dark(#1a85d4, #69b1ff)',
141
+ claude: 'light-dark(#d47628, #ffa359)',
142
+ cpp: 'light-dark(#1a85d4, #69b1ff)',
143
+ css: 'light-dark(#693acf, #9d6afb)',
144
+ database: 'light-dark(#a631be, #d568ea)',
145
+ default: 'light-dark(#84848a, #adadb1)',
146
+ docker: 'light-dark(#1a85d4, #69b1ff)',
147
+ eslint: 'light-dark(#693acf, #9d6afb)',
148
+ git: 'light-dark(#ff8c5b, #d5512f)',
149
+ go: 'light-dark(#1ca1c7, #68cdf2)',
150
+ graphql: 'light-dark(#d32a61, #ff678d)',
151
+ html: 'light-dark(#d47628, #ffa359)',
152
+ image: 'light-dark(#d32a61, #ff678d)',
153
+ javascript: 'light-dark(#d5a910, #ffd452)',
154
+ json: 'light-dark(#d47628, #ffa359)',
155
+ markdown: 'light-dark(#199f43, #5ecc71)',
156
+ mcp: 'light-dark(#17a5af, #64d1db)',
157
+ npm: 'light-dark(#d52c36, #ff6762)',
158
+ oxc: 'light-dark(#1ca1c7, #68cdf2)',
159
+ postcss: 'light-dark(#d52c36, #ff6762)',
160
+ prettier: 'light-dark(#17a5af, #64d1db)',
161
+ python: 'light-dark(#1a85d4, #69b1ff)',
162
+ react: 'light-dark(#1ca1c7, #68cdf2)',
163
+ ruby: 'light-dark(#d52c36, #ff6762)',
164
+ rust: 'light-dark(#d47628, #ffa359)',
165
+ sass: 'light-dark(#d32a61, #ff678d)',
166
+ svelte: 'light-dark(#d52c36, #ff6762)',
167
+ svg: 'light-dark(#d47628, #ffa359)',
168
+ svgo: 'light-dark(#199f43, #5ecc71)',
169
+ swift: 'light-dark(#d47628, #ffa359)',
170
+ table: 'light-dark(#17a5af, #64d1db)',
171
+ tailwind: 'light-dark(#1ca1c7, #68cdf2)',
172
+ terraform: 'light-dark(#693acf, #9d6afb)',
173
+ text: 'light-dark(#84848a, #adadb1)',
174
+ typescript: 'light-dark(#1a85d4, #69b1ff)',
175
+ vite: 'light-dark(#a631be, #d568ea)',
176
+ vscode: 'light-dark(#1a85d4, #69b1ff)',
177
+ vue: 'light-dark(#199f43, #5ecc71)',
178
+ wasm: 'light-dark(#693acf, #9d6afb)',
179
+ webpack: 'light-dark(#1a85d4, #69b1ff)',
180
+ yml: 'light-dark(#d52c36, #ff6762)',
181
+ zig: 'light-dark(#d47628, #ffa359)',
182
+ zip: 'light-dark(#d47628, #ffa359)'
183
+ };
184
+
185
+ /**
186
+ * Color the file tree would paint for the built-in icon `token`, as a
187
+ * self-contained CSS value usable outside the tree's shadow DOM: the tree's
188
+ * `--trees-file-icon-color-*` properties when in scope, else the literal from
189
+ * {@link BUILT_IN_FILE_ICON_COLOR_FALLBACKS}. Returns `undefined` for unknown
190
+ * tokens.
191
+ */
192
+ function getBuiltInFileIconColor(token: string): string | undefined {
193
+ const fallback = BUILT_IN_FILE_ICON_COLOR_FALLBACKS[token];
194
+ if (fallback === undefined) {
195
+ return undefined;
196
+ }
197
+ return `var(--trees-file-icon-color-${token}, var(--trees-file-icon-color, ${fallback}))`;
198
+ }
199
+
200
+ /**
201
+ * Resolve a tree glyph into a standalone `LabIcon`.
202
+ */
203
+ function resolveTreeIcon(filePath: string): {
204
+ icon: LabIcon | null;
205
+ specific: boolean;
206
+ } {
135
207
  const resolved = treeIconResolver.resolveIcon(
136
208
  'file-tree-icon-file',
137
209
  filePath
138
210
  );
211
+ // Anything other than the catch-all glyph is specific to this file.
212
+ const specific =
213
+ resolved.token !== 'default' && resolved.name !== 'file-tree-icon-file';
139
214
  const cached = TREE_ICON_CACHE.get(resolved.name);
140
215
  if (cached !== undefined) {
141
- return cached;
216
+ return { icon: cached, specific };
142
217
  }
143
218
  const symbol = extractSymbol(resolved.name);
144
219
  if (symbol === null) {
145
- return fileIcon;
220
+ return { icon: null, specific };
146
221
  }
147
222
  // Built-in language icons paint with `fill="currentColor"` so the colored
148
223
  // tier of the tree can recolor them by setting `color` on the host
@@ -168,5 +243,20 @@ export function getTreeIcon(filePath: string): LabIcon {
168
243
  svgstr
169
244
  });
170
245
  TREE_ICON_CACHE.set(resolved.name, icon);
171
- return icon;
246
+ return { icon, specific };
247
+ }
248
+
249
+ /**
250
+ * Return the icon the xtralab file tree would render for `filePath`.
251
+ */
252
+ export function getTreeIcon(filePath: string): LabIcon {
253
+ return resolveTreeIcon(filePath).icon ?? fileIcon;
254
+ }
255
+
256
+ /**
257
+ * Like {@link getTreeIcon}, but returns `null` for the generic file glyph.
258
+ */
259
+ export function getSpecificTreeIcon(filePath: string): LabIcon | null {
260
+ const { icon, specific } = resolveTreeIcon(filePath);
261
+ return specific ? icon : null;
172
262
  }
@@ -4,10 +4,14 @@ import {
4
4
  JupyterFrontEndPlugin
5
5
  } from '@jupyterlab/application';
6
6
 
7
+ import { ICommandPalette } from '@jupyterlab/apputils';
8
+
7
9
  import { IDocumentManager } from '@jupyterlab/docmanager';
8
10
 
9
11
  import { ISettingRegistry } from '@jupyterlab/settingregistry';
10
12
 
13
+ import { ITranslator } from '@jupyterlab/translation';
14
+
11
15
  import { populateToolbar, registerCommands } from './commands';
12
16
  import { XtralabFileBrowser } from './widget';
13
17
 
@@ -23,12 +27,14 @@ const plugin: JupyterFrontEndPlugin<void> = {
23
27
  'A path-first file browser for JupyterLab built on @pierre/trees.',
24
28
  autoStart: true,
25
29
  requires: [IDocumentManager],
26
- optional: [ILayoutRestorer, ISettingRegistry],
30
+ optional: [ILayoutRestorer, ISettingRegistry, ICommandPalette, ITranslator],
27
31
  activate: (
28
32
  app: JupyterFrontEnd,
29
33
  docManager: IDocumentManager,
30
34
  restorer: ILayoutRestorer | null,
31
- settingRegistry: ISettingRegistry | null
35
+ settingRegistry: ISettingRegistry | null,
36
+ palette: ICommandPalette | null,
37
+ translator: ITranslator | null
32
38
  ): void => {
33
39
  const browser = new XtralabFileBrowser({
34
40
  contentsManager: app.serviceManager.contents,
@@ -46,7 +52,7 @@ const plugin: JupyterFrontEndPlugin<void> = {
46
52
  restorer.add(browser, browser.id);
47
53
  }
48
54
 
49
- registerCommands({ app, browser });
55
+ registerCommands({ app, browser, docManager, palette, translator });
50
56
  populateToolbar({ app, browser });
51
57
 
52
58
  if (settingRegistry) {