Skip to content
Source
Sign inSign up

filePath.ts

94 lines3.7 KB

1import type { ProjectEntry } from "@t3tools/contracts";
2import { isWindowsAbsolutePath, isHostPath } 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;
13​
14export function fileBreadcrumbs(projectName: string, relativePath: string): FileBreadcrumb[] {
15 const hostPath = isHostPath(relativePath);
16 const windowsPath = isWindowsAbsolutePath(relativePath);
17 const separator = windowsPath ? "\\" : "/";
18 // `\` is a legal filename character outside Windows paths.
19 const parts = relativePath.split(windowsPath ? /[\\/]/ : "/").filter(Boolean);
20 const root = relativePath.startsWith("\\\\") ? "\\\\" : relativePath.startsWith("/") ? "/" : "";
21 return [
22 ...(hostPath ? [] : [{ label: projectName, path: "", kind: "project" as const }]),
23 ...parts.map((part, index) => {
24 const path = root + parts.slice(0, index + 1).join(separator);
25 return {
26 label: part,
27 // A bare drive (`C:`) means the drive's current directory; its root is `C:\`.
28 // A bare `~` is not a host path; the home folder is `~/`.
29 path: /^[A-Za-z]:$/.test(path) ? `${path}\\` : hostPath && path === "~" ? "~/" : path,
30 kind: index === parts.length - 1 ? ("file" as const) : ("directory" as const),
31 };
32 }),
33 ];
35​
36/** Use the crumb's path spelling so trailing or repeated separators still match. */
37export function breadcrumbPathOf(relativePath: string): string {
38 return fileBreadcrumbs("", relativePath).at(-1)?.path ?? relativePath;
40​
41/** UNC servers are not listable directories; their shares are. */
42export function isListableHostFolder(path: string): boolean {
43 return isHostPath(path) && !/^\\\\[^\\/]+$/.test(path);
45​
46function trailCrumbIndex(trail: string, path: string): number {
47 const crumbPath = breadcrumbPathOf(path);
48 return fileBreadcrumbs("", trail).findIndex((crumb) => crumb.path === crumbPath);
50​
51/** Retain descendant crumbs when navigating up; other navigation resets the trail to path. */
52export function retainedBreadcrumbTrail(trail: string | undefined, path: string): string {
53 return trail !== undefined && trailCrumbIndex(trail, path) >= 0 ? trail : path;
55​
56export function trailChildOf(folder: string, trail: string): string | null {
57 const index = trailCrumbIndex(trail, folder);
58 return index < 0 ? null : (fileBreadcrumbs("", trail)[index + 1]?.label ?? null);
60​
61export function joinHostPath(folder: string, relativePath: string): string {
62 const separator = isWindowsAbsolutePath(folder) ? "\\" : "/";
63 const base = folder.endsWith(separator) ? folder : `${folder}${separator}`;
64 return base + relativePath.split("/").join(separator);
66​
67export function fileBreadcrumbChildren(
68 entries: readonly ProjectEntry[],
69 directoryPath: string,
70): FileBreadcrumbChild[] {
71 let collator: Intl.Collator | undefined;
72 const prefix = directoryPath ? `${directoryPath}/` : "";
73 return entries
74 .flatMap((entry) => {
75 if (!entry.path.startsWith(prefix)) return [];
76 const label = entry.path.slice(prefix.length);
77 if (!label || label.includes("/")) return [];
78 return [{ ...entry, label }];
79 })
80 .toSorted((left, right) => {
81 if (left.kind !== right.kind) return left.kind === "directory" ? -1 : 1;
82 collator ??= new Intl.Collator(undefined, {
83 numeric: true,
84 sensitivity: "base",
85 });
86 return collator.compare(left.label, right.label);
87 });
89​
90export function fileBreadcrumbParent(directoryPath: string): string | null {
91 if (!directoryPath) return null;
92 const separatorIndex = directoryPath.lastIndexOf("/");
93 return separatorIndex === -1 ? "" : directoryPath.slice(0, separatorIndex);