1import type { ProjectEntry } from "@t3tools/contracts";
2import { isWindowsAbsolutePath, isAbsolutePath } from "@t3tools/shared/path";
3
4export interface FileBreadcrumb {
5 label: string;
6 path: string;
7 kind: "project" | "directory" | "file";
8}
9
10export interface FileBreadcrumbChild extends ProjectEntry {
11 label: string;
12}
13
14/**
15 * Crumbs for a workspace-relative path start at the project. An absolute host
16 * path is outside the workspace, so its crumbs start at the filesystem root.
17 */
18export function fileBreadcrumbs(projectName: string, relativePath: string): FileBreadcrumb[] {
19 const hostPath = isAbsolutePath(relativePath);
20 const separator = isWindowsAbsolutePath(relativePath) ? "\\" : "/";
21 const parts = relativePath.split(/[\\/]/).filter(Boolean);
22 const root = relativePath.startsWith("\\\\") ? "\\\\" : hostPath && separator === "/" ? "/" : "";
23 return [
24 ...(hostPath ? [] : [{ label: projectName, path: "", kind: "project" as const }]),
25 ...parts.map((part, index) => ({
26 label: part,
27 path: root + parts.slice(0, index + 1).join(separator),
28 kind: index === parts.length - 1 ? ("file" as const) : ("directory" as const),
29 })),
30 ];
31}
32
33export function fileBreadcrumbChildren(
34 entries: readonly ProjectEntry[],
35 directoryPath: string,
36): FileBreadcrumbChild[] {
37 let collator: Intl.Collator | undefined;
38 const prefix = directoryPath ? `${directoryPath}/` : "";
39 return entries
40 .flatMap((entry) => {
41 if (!entry.path.startsWith(prefix)) return [];
42 const label = entry.path.slice(prefix.length);
43 if (!label || label.includes("/")) return [];
44 return [{ ...entry, label }];
45 })
46 .toSorted((left, right) => {
47 if (left.kind !== right.kind) return left.kind === "directory" ? -1 : 1;
48 collator ??= new Intl.Collator(undefined, {
49 numeric: true,
50 sensitivity: "base",
51 });
52 return collator.compare(left.label, right.label);
53 });
54}
55
56export function fileBreadcrumbParent(directoryPath: string): string | null {
57 if (!directoryPath) return null;
58 const separatorIndex = directoryPath.lastIndexOf("/");
59 return separatorIndex === -1 ? "" : directoryPath.slice(0, separatorIndex);
60}