Open History to see the last commit
1import * as Schema from "effect/Schema";
2
3import { getLocalStorageItem, setLocalStorageItem } from "~/hooks/useLocalStorage";
4
5export interface FileTreeExpansionModel {
6 getItem(path: string): unknown;
7}
8
9type DirectoryHandle = {
10 isDirectory(): boolean;
11 isExpanded(): boolean;
12 expand(): void;
13 collapse(): void;
14};
15
16function asDirectoryHandle(item: unknown): DirectoryHandle | null {
17 if (
18 typeof item !== "object" ||
19 item === null ||
20 !("isDirectory" in item) ||
21 typeof item.isDirectory !== "function" ||
22 !item.isDirectory() ||
23 !("isExpanded" in item) ||
24 typeof item.isExpanded !== "function" ||
25 !("expand" in item) ||
26 typeof item.expand !== "function" ||
27 !("collapse" in item) ||
28 typeof item.collapse !== "function"
29 ) {
30 return null;
31 }
32 return item as DirectoryHandle;
33}
34
35export function areAllDirectoriesExpanded(
36 model: FileTreeExpansionModel,
37 directoryPaths: readonly string[],
38): boolean {
39 return (
40 directoryPaths.length > 0 &&
41 directoryPaths.every((path) => {
42 const item = asDirectoryHandle(model.getItem(path));
43 return item !== null && item.isExpanded();
44 })
45 );
46}
47
48export function setAllDirectoriesExpanded(
49 model: FileTreeExpansionModel,
50 directoryPaths: readonly string[],
51 expanded: boolean,
52): void {
53 for (const path of directoryPaths) {
54 const item = asDirectoryHandle(model.getItem(path));
55 if (item === null || item.isExpanded() === expanded) continue;
56 if (expanded) item.expand();
57 else item.collapse();
58 }
59}
60
61/** Calls made while `run` is running rerun it once it returns, instead of nesting. */
62export function coalesceReentrantCalls(run: () => void): () => void {
63 let running = false;
64 let calledAgain = false;
65 return () => {
66 if (running) {
67 calledAgain = true;
68 return;
69 }
70 running = true;
71 try {
72 do {
73 calledAgain = false;
74 run();
75 } while (calledAgain);
76 } finally {
77 running = false;
78 }
79 };
80}
81
82const EXPANDED_DIRECTORIES_STORAGE_KEY = "t3code.fileTreeExpandedDirectories";
83// Roots are kept most recent first and capped so worktrees that come and go do not grow storage.
84const MAX_STORED_ROOTS = 20;
85const StoredExpandedDirectories = Schema.Array(
86 Schema.Struct({ root: Schema.String, paths: Schema.Array(Schema.String) }),
87);
88
89function readStoredExpandedDirectories() {
90 try {
91 return getLocalStorageItem(EXPANDED_DIRECTORIES_STORAGE_KEY, StoredExpandedDirectories) ?? [];
92 } catch (error) {
93 console.error(error);
94 return [];
95 }
96}
97
98export function readExpandedDirectories(root: string): readonly string[] {
99 return readStoredExpandedDirectories().find((entry) => entry.root === root)?.paths ?? [];
100}
101
102export function writeExpandedDirectories(root: string, paths: readonly string[]): void {
103 const others = readStoredExpandedDirectories().filter((entry) => entry.root !== root);
104 const next = paths.length > 0 ? [{ root, paths }, ...others] : others;
105 try {
106 setLocalStorageItem(
107 EXPANDED_DIRECTORIES_STORAGE_KEY,
108 next.slice(0, MAX_STORED_ROOTS),
109 StoredExpandedDirectories,
110 );
111 } catch (error) {
112 console.error(error);
113 }
114}