Skip to content

Commit aa61bd3

Browse files
wip: switch to requesting a sub-tree depending on the opened paths
1 parent 61a7055 commit aa61bd3

6 files changed

Lines changed: 107 additions & 213 deletions

File tree

packages/web/src/features/fileTree/api.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { createLogger } from '@sourcebot/shared';
99
import path from 'path';
1010
import { simpleGit } from 'simple-git';
1111
import { FileTreeItem } from './types';
12-
import { buildFileTree, getPathspecs, isPathValid, normalizePath } from './utils';
12+
import { buildFileTree, isPathValid, normalizePath } from './utils';
1313
import { compareFileTreeItems } from './utils';
1414

1515
const logger = createLogger('file-tree');
@@ -18,9 +18,9 @@ const logger = createLogger('file-tree');
1818
* Returns the tree of files (blobs) and directories (trees) for a given repository,
1919
* at a given revision.
2020
*/
21-
export const getTree = async (params: { repoName: string, revisionName: string, path: string }) => sew(() =>
21+
export const getTree = async (params: { repoName: string, revisionName: string, paths: string[] }) => sew(() =>
2222
withOptionalAuthV2(async ({ org, prisma }) => {
23-
const { repoName, revisionName, path } = params;
23+
const { repoName, revisionName, paths } = params;
2424
const repo = await prisma.repo.findFirst({
2525
where: {
2626
name: repoName,
@@ -35,11 +35,11 @@ export const getTree = async (params: { repoName: string, revisionName: string,
3535
const { path: repoPath } = getRepoPath(repo);
3636

3737
const git = simpleGit().cwd(repoPath);
38-
if (!isPathValid(path)) {
38+
if (!paths.every(path => isPathValid(path))) {
3939
return notFound();
4040
}
4141

42-
const pathSpecs = getPathspecs(path);
42+
const normalizedPaths = paths.map(path => normalizePath(path));
4343

4444
let result: string = '';
4545
try {
@@ -55,7 +55,7 @@ export const getTree = async (params: { repoName: string, revisionName: string,
5555
'-t',
5656
'--',
5757
'.',
58-
...pathSpecs,
58+
...normalizedPaths,
5959
];
6060

6161
result = await git.raw(command);

packages/web/src/features/fileTree/components/fileTreePanel.tsx

Lines changed: 86 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22

33
import { useBrowseParams } from "@/app/[domain]/browse/hooks/useBrowseParams";
44
import { useBrowseState } from "@/app/[domain]/browse/hooks/useBrowseState";
5-
import { getFolderContents, getTree } from "@/app/api/(client)/client";
5+
import { getTree } from "@/app/api/(client)/client";
66
import { KeyboardShortcutHint } from "@/app/components/keyboardShortcutHint";
77
import { Button } from "@/components/ui/button";
88
import { ResizablePanel } from "@/components/ui/resizable";
99
import { Separator } from "@/components/ui/separator";
1010
import { Skeleton } from "@/components/ui/skeleton";
1111
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
12-
import { unwrapServiceError } from "@/lib/utils";
12+
import { measure, unwrapServiceError } from "@/lib/utils";
1313
import { useQuery } from "@tanstack/react-query";
1414
import { SearchIcon } from "lucide-react";
1515
import { useCallback, useEffect, useRef, useState } from "react";
@@ -41,29 +41,78 @@ export const FileTreePanel = ({ order }: FileTreePanelProps) => {
4141
const { repoName, revisionName, path } = useBrowseParams();
4242

4343
const [tree, setTree] = useState<FileTreeNode | null>(null);
44+
const [openPaths, setOpenPaths] = useState<Set<string>>(new Set());
4445

4546
const fileTreePanelRef = useRef<ImperativePanelHandle>(null);
46-
const loadFolderContents = useCallback(async (folderPath: string) => {
47-
return unwrapServiceError(
48-
getFolderContents({
49-
repoName,
50-
revisionName: revisionName ?? 'HEAD',
51-
path: folderPath
52-
})
53-
);
54-
}, [repoName, revisionName]);
5547

5648
const { data, isError } = useQuery({
57-
queryKey: ['tree', repoName, revisionName, path],
58-
queryFn: () => unwrapServiceError(
59-
getTree({
60-
repoName,
61-
revisionName: revisionName ?? 'HEAD',
62-
path
63-
})
64-
),
49+
queryKey: ['tree', repoName, revisionName, ...Array.from(openPaths)],
50+
queryFn: async () => {
51+
const result = await measure(async () => unwrapServiceError(
52+
getTree({
53+
repoName,
54+
revisionName: revisionName ?? 'HEAD',
55+
paths: Array.from(openPaths),
56+
})
57+
), 'getTree');
58+
59+
return result.data;
60+
}
6561
});
6662

63+
useEffect(() => {
64+
if (!data) {
65+
return;
66+
}
67+
setTree(data.tree);
68+
}, [data]);
69+
70+
// Whenever the repo name or revision name changes, we will need to
71+
// reset the open paths since they no longer reference the same repository/revision.
72+
useEffect(() => {
73+
setOpenPaths(new Set());
74+
}, [repoName, revisionName]);
75+
76+
// When the path changes (e.g., the user clicks a reference in the explore panel),
77+
// we want this to be open and visible in the file tree.
78+
useEffect(() => {
79+
const pathParts = path.split('/').filter(Boolean);
80+
81+
setOpenPaths(current => {
82+
const next = new Set<string>(current);
83+
for (let i = 0; i < pathParts.length; i++) {
84+
next.add(pathParts.slice(0, i + 1).join('/'));
85+
}
86+
return next;
87+
});
88+
}, [path]);
89+
90+
// When the user clicks a file tree node, we will want to either
91+
// add or remove it from the open paths depending on if it's already open or not.
92+
const onNodeClicked = useCallback((node: FileTreeNode) => {
93+
if (!openPaths.has(node.path)) {
94+
setOpenPaths(current => {
95+
const next = new Set(current);
96+
next.add(node.path);
97+
return next;
98+
})
99+
} else {
100+
setOpenPaths(current => {
101+
const next = new Set(current);
102+
next.delete(node.path);
103+
return next;
104+
})
105+
}
106+
}, [openPaths]);
107+
108+
// @debug: format the tree for console output.
109+
// useEffect(() => {
110+
// if (!tree) {
111+
// return;
112+
// }
113+
// console.debug(__debugFormatTreeForConsole(tree));
114+
// }, [tree]);
115+
67116
useHotkeys("mod+b", () => {
68117
if (isFileTreePanelCollapsed) {
69118
fileTreePanelRef.current?.expand();
@@ -76,13 +125,6 @@ export const FileTreePanel = ({ order }: FileTreePanelProps) => {
76125
description: "Toggle file tree panel",
77126
});
78127

79-
useEffect(() => {
80-
if (!data) {
81-
return;
82-
}
83-
setTree(data.tree);
84-
}, [data]);
85-
86128
return (
87129
<>
88130
<ResizablePanel
@@ -151,8 +193,9 @@ export const FileTreePanel = ({ order }: FileTreePanelProps) => {
151193
) : (
152194
<PureFileTreePanel
153195
tree={tree}
196+
openPaths={openPaths}
154197
path={path}
155-
onLoadChildren={loadFolderContents}
198+
onNodeClicked={onNodeClicked}
156199
/>
157200
)}
158201
</div>
@@ -343,4 +386,19 @@ const FileTreePanelSkeleton = () => {
343386
</div>
344387
</div>
345388
)
346-
}
389+
}
390+
391+
const __debugFormatTreeForConsole = (node: FileTreeNode): string => {
392+
const lines: string[] = [];
393+
const walk = (current: FileTreeNode, prefix: string, isLast: boolean, isRoot: boolean) => {
394+
const label = current.name || current.path;
395+
const connector = isRoot ? "" : (isLast ? "`-- " : "|-- ");
396+
lines.push(`${prefix}${connector}${label}`);
397+
const nextPrefix = isRoot ? "" : `${prefix}${isLast ? " " : "| "}`;
398+
current.children.forEach((child, index) => {
399+
walk(child, nextPrefix, index === current.children.length - 1, false);
400+
});
401+
};
402+
walk(node, "", true, true);
403+
return lines.join("\n");
404+
};

0 commit comments

Comments
 (0)