import { LabIcon, fileIcon } from '@jupyterlab/ui-components';
import {
createFileTreeIconResolver,
getBuiltInSpriteSheet
} from '@pierre/trees';
import type { FileTreeIconConfig } from '@pierre/trees';
/**
* The glyph used as the xtralab file browser's sidebar tab icon. Defined as a
* `LabIcon` (rather than the default `folderIcon` from
* `@jupyterlab/ui-components`) so the xtralab browser is visually distinguishable
* from JupyterLab's built-in file browser, which also lives in the left
* sidebar.
*
* Source: `file-tree` from SVG Repo (https://www.svgrepo.com/svg/371275/file-tree).
* The original `fill="#444"` is replaced with `fill="currentColor"` and the
* root element is given the `jp-icon3` class so the icon picks up JupyterLab's
* theme color and the sidebar's inverse-color painting automatically.
*/
export const xtralabFileBrowserIcon = new LabIcon({
name: 'xtralab:file-browser',
svgstr: ``
});
/**
* Sprite sheet injected into the `@pierre/trees` shadow DOM. Contains a single
* `` for the Jupyter notebook icon, drawn from the JupyterLab core
* notebook icon (`@jupyterlab/ui-components/style/icons/filetype/notebook.svg`).
*
* `@pierre/trees` does not ship a Jupyter token, so `.ipynb` files would
* otherwise fall through to the generic file icon. The symbol id below is
* referenced from `byFileExtension` in {@link FILE_BROWSER_ICONS}.
*
* The orange `#EF6C00` fill is set as a presentation attribute on the inner
* `` so it survives the `fill: currentColor` rule the tree applies on the
* icon container — the inherited color does not override a presentation
* attribute set directly on a child.
*/
const JUPYTER_NOTEBOOK_SPRITE_SHEET = ``;
/**
* Icon configuration for the xtralab file browser. Keeps the default `complete`
* built-in icon set (the colored language icons) and layers a Jupyter notebook
* symbol on top so `.ipynb` files render with the JupyterLab notebook glyph.
*
* `set` must be set explicitly: when a config object includes any custom
* overrides without `set`, `@pierre/trees` defaults `set` to `'none'`, which
* disables the built-in icons entirely.
*/
export const FILE_BROWSER_ICONS: FileTreeIconConfig = {
set: 'complete',
colored: true,
spriteSheet: JUPYTER_NOTEBOOK_SPRITE_SHEET,
byFileExtension: {
ipynb: 'jupyter-notebook'
}
};
/**
* Concatenated sprite-sheet text. Combines the built-in sheet for the active
* icon set with our custom Jupyter notebook sheet so {@link extractSymbol}
* can find the body of any symbol the tree itself can render.
*/
const COMBINED_SPRITE_SHEETS = [
getBuiltInSpriteSheet(FILE_BROWSER_ICONS.set ?? 'complete'),
JUPYTER_NOTEBOOK_SPRITE_SHEET
].join('\n');
/**
* Resolver instance reused across calls to {@link getTreeIcon}. The resolver
* is pure relative to the icon config, so building it once at module load
* avoids re-walking the icon rules on every diff that opens.
*/
const treeIconResolver = createFileTreeIconResolver(FILE_BROWSER_ICONS);
/**
* Cache of LabIcons keyed by the resolved sprite symbol id (e.g.
* `file-tree-builtin-python`). All Python files share the same icon, so
* caching by symbol id prevents repeated LabIcon registration warnings and
* keeps the JupyterLab icon registry small.
*/
const TREE_ICON_CACHE = new Map();
/**
* Extract the inner SVG markup and viewBox of the `` with the given
* id from {@link COMBINED_SPRITE_SHEETS}. Returns `null` if no matching
* symbol exists, in which case the caller falls back to JupyterLab's default
* file icon.
*/
function extractSymbol(
symbolId: string
): { viewBox: string; inner: string } | null {
// Match a `` whose `id` attribute equals `symbolId` (allowing other
// attributes in any order). The `[\s\S]*?` body is non-greedy so we stop
// at the first matching `` even when multiple symbols sit
// back-to-back in the sheet.
const escaped = symbolId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const symbolRegex = new RegExp(
`]*\\bid="${escaped}"[^>]*>([\\s\\S]*?)`,
'i'
);
const match = symbolRegex.exec(COMBINED_SPRITE_SHEETS);
if (match === null) {
return null;
}
const viewBoxMatch = /\bviewBox="([^"]*)"/i.exec(match[0]);
return {
viewBox: viewBoxMatch?.[1] ?? '0 0 16 16',
inner: match[1]
};
}
/**
* Fallback colors for the built-in file icons, keyed by the icon token
* `@pierre/trees` resolves for a path (e.g. `python`, `typescript`).
*
* The tree only exposes these as `--trees-file-icon-color-` CSS
* variables scoped to its shadow host, so they resolve to nothing for the
* standalone {@link LabIcon}s {@link getTreeIcon} builds for main-area tabs.
* Mirroring them here lets {@link getBuiltInFileIconColor} bottom out in a
* real color. Each is a `light-dark(…)` pair for dark themes.
*/
const BUILT_IN_FILE_ICON_COLOR_FALLBACKS: Record = {
astro: 'light-dark(#a631be, #d568ea)',
babel: 'light-dark(#d5a910, #ffd452)',
bash: 'light-dark(#199f43, #5ecc71)',
biome: 'light-dark(#1a85d4, #69b1ff)',
bootstrap: 'light-dark(#693acf, #9d6afb)',
browserslist: 'light-dark(#d5a910, #ffd452)',
bun: 'light-dark(#594c5b, #79697b)',
c: 'light-dark(#1a85d4, #69b1ff)',
claude: 'light-dark(#d47628, #ffa359)',
cpp: 'light-dark(#1a85d4, #69b1ff)',
css: 'light-dark(#693acf, #9d6afb)',
database: 'light-dark(#a631be, #d568ea)',
default: 'light-dark(#84848a, #adadb1)',
docker: 'light-dark(#1a85d4, #69b1ff)',
eslint: 'light-dark(#693acf, #9d6afb)',
git: 'light-dark(#ff8c5b, #d5512f)',
go: 'light-dark(#1ca1c7, #68cdf2)',
graphql: 'light-dark(#d32a61, #ff678d)',
html: 'light-dark(#d47628, #ffa359)',
image: 'light-dark(#d32a61, #ff678d)',
javascript: 'light-dark(#d5a910, #ffd452)',
json: 'light-dark(#d47628, #ffa359)',
markdown: 'light-dark(#199f43, #5ecc71)',
mcp: 'light-dark(#17a5af, #64d1db)',
npm: 'light-dark(#d52c36, #ff6762)',
oxc: 'light-dark(#1ca1c7, #68cdf2)',
postcss: 'light-dark(#d52c36, #ff6762)',
prettier: 'light-dark(#17a5af, #64d1db)',
python: 'light-dark(#1a85d4, #69b1ff)',
react: 'light-dark(#1ca1c7, #68cdf2)',
ruby: 'light-dark(#d52c36, #ff6762)',
rust: 'light-dark(#d47628, #ffa359)',
sass: 'light-dark(#d32a61, #ff678d)',
svelte: 'light-dark(#d52c36, #ff6762)',
svg: 'light-dark(#d47628, #ffa359)',
svgo: 'light-dark(#199f43, #5ecc71)',
swift: 'light-dark(#d47628, #ffa359)',
table: 'light-dark(#17a5af, #64d1db)',
tailwind: 'light-dark(#1ca1c7, #68cdf2)',
terraform: 'light-dark(#693acf, #9d6afb)',
text: 'light-dark(#84848a, #adadb1)',
typescript: 'light-dark(#1a85d4, #69b1ff)',
vite: 'light-dark(#a631be, #d568ea)',
vscode: 'light-dark(#1a85d4, #69b1ff)',
vue: 'light-dark(#199f43, #5ecc71)',
wasm: 'light-dark(#693acf, #9d6afb)',
webpack: 'light-dark(#1a85d4, #69b1ff)',
yml: 'light-dark(#d52c36, #ff6762)',
zig: 'light-dark(#d47628, #ffa359)',
zip: 'light-dark(#d47628, #ffa359)'
};
/**
* Color the file tree would paint for the built-in icon `token`, as a
* self-contained CSS value usable outside the tree's shadow DOM: the tree's
* `--trees-file-icon-color-*` properties when in scope, else the literal from
* {@link BUILT_IN_FILE_ICON_COLOR_FALLBACKS}. Returns `undefined` for unknown
* tokens.
*/
function getBuiltInFileIconColor(token: string): string | undefined {
const fallback = BUILT_IN_FILE_ICON_COLOR_FALLBACKS[token];
if (fallback === undefined) {
return undefined;
}
return `var(--trees-file-icon-color-${token}, var(--trees-file-icon-color, ${fallback}))`;
}
/**
* Resolve a tree glyph into a standalone `LabIcon`.
*/
function resolveTreeIcon(filePath: string): {
icon: LabIcon | null;
specific: boolean;
} {
const resolved = treeIconResolver.resolveIcon(
'file-tree-icon-file',
filePath
);
// Anything other than the catch-all glyph is specific to this file.
const specific =
resolved.token !== 'default' && resolved.name !== 'file-tree-icon-file';
const cached = TREE_ICON_CACHE.get(resolved.name);
if (cached !== undefined) {
return { icon: cached, specific };
}
const symbol = extractSymbol(resolved.name);
if (symbol === null) {
return { icon: null, specific };
}
// Built-in language icons paint with `fill="currentColor"` so the colored
// tier of the tree can recolor them by setting `color` on the host
// element. Reproduce that here by setting the same color value as an
// inline `style="color: …"` on the outer `