Skip to content

Commit ed48da4

Browse files
feedback
1 parent b304c93 commit ed48da4

6 files changed

Lines changed: 42 additions & 83 deletions

File tree

packages/web/src/app/api/(client)/client.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@ import {
1818
import {
1919
GetFilesRequest,
2020
GetFilesResponse,
21-
GetFolderContentsRequest,
22-
GetFolderContentsResponse,
2321
GetTreeRequest,
2422
GetTreeResponse,
2523
} from "@/features/fileTree/types";
@@ -104,11 +102,3 @@ export const getFiles = async (body: GetFilesRequest): Promise<GetFilesResponse
104102
}).then(response => response.json());
105103
return result as GetFilesResponse | ServiceError;
106104
}
107-
108-
export const getFolderContents = async (body: GetFolderContentsRequest): Promise<GetFolderContentsResponse | ServiceError> => {
109-
const result = await fetch("/api/folder_contents", {
110-
method: "POST",
111-
body: JSON.stringify(body),
112-
}).then(response => response.json());
113-
return result as GetFolderContentsResponse | ServiceError;
114-
}

packages/web/src/app/api/(server)/folder_contents/route.ts

Lines changed: 0 additions & 23 deletions
This file was deleted.

packages/web/src/app/api/(server)/tree/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { getTree } from "@/features/fileTree/api";
44
import { getTreeRequestSchema } from "@/features/fileTree/types";
55
import { schemaValidationError, serviceErrorResponse } from "@/lib/serviceError";
6-
import { isServiceError, measure } from "@/lib/utils";
6+
import { isServiceError } from "@/lib/utils";
77
import { NextRequest } from "next/server";
88

99
export const POST = async (request: NextRequest) => {
@@ -13,7 +13,7 @@ export const POST = async (request: NextRequest) => {
1313
return serviceErrorResponse(schemaValidationError(parsed.error));
1414
}
1515

16-
const { data: response } = await measure(() => getTree(parsed.data), 'getTree');
16+
const response = await getTree(parsed.data);
1717
if (isServiceError(response)) {
1818
return serviceErrorResponse(response);
1919
}

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

Lines changed: 26 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ 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 useCaptureEvent from "@/hooks/useCaptureEvent";
1213
import { measure, unwrapServiceError } from "@/lib/utils";
1314
import { useQuery } from "@tanstack/react-query";
1415
import { SearchIcon } from "lucide-react";
@@ -19,8 +20,8 @@ import {
1920
GoSidebarCollapse as ExpandIcon
2021
} from "react-icons/go";
2122
import { ImperativePanelHandle } from "react-resizable-panels";
22-
import { PureFileTreePanel } from "./pureFileTreePanel";
2323
import { FileTreeNode } from "../types";
24+
import { PureFileTreePanel } from "./pureFileTreePanel";
2425

2526
interface FileTreePanelProps {
2627
order: number;
@@ -38,14 +39,13 @@ export const FileTreePanel = ({ order }: FileTreePanelProps) => {
3839
updateBrowseState,
3940
} = useBrowseState();
4041

41-
const { repoName, revisionName, path } = useBrowseParams();
42-
43-
const [tree, setTree] = useState<FileTreeNode | null>(null);
42+
const { repoName, revisionName, path, pathType } = useBrowseParams();
4443
const [openPaths, setOpenPaths] = useState<Set<string>>(new Set());
44+
const captureEvent = useCaptureEvent();
4545

4646
const fileTreePanelRef = useRef<ImperativePanelHandle>(null);
4747

48-
const { data, isError } = useQuery({
48+
const { data, isError, isPending } = useQuery({
4949
queryKey: ['tree', repoName, revisionName, ...Array.from(openPaths)],
5050
queryFn: async () => {
5151
const result = await measure(async () => unwrapServiceError(
@@ -56,17 +56,19 @@ export const FileTreePanel = ({ order }: FileTreePanelProps) => {
5656
})
5757
), 'getTree');
5858

59+
captureEvent('wa_file_tree_loaded', {
60+
durationMs: result.durationMs,
61+
});
62+
5963
return result.data;
60-
}
64+
},
65+
// The tree changes only when the query key changes (repo/revision/openPaths),
66+
// so we can treat it as perpetually fresh and avoid background refetches.
67+
staleTime: Infinity,
68+
// Reuse the last tree during refetches (openPaths changes) to avoid UI flicker.
69+
placeholderData: (previousData) => previousData,
6170
});
6271

63-
useEffect(() => {
64-
if (!data) {
65-
return;
66-
}
67-
setTree(data.tree);
68-
}, [data]);
69-
7072
// Whenever the repo name or revision name changes, we will need to
7173
// reset the open paths since they no longer reference the same repository/revision.
7274
useEffect(() => {
@@ -76,7 +78,12 @@ export const FileTreePanel = ({ order }: FileTreePanelProps) => {
7678
// When the path changes (e.g., the user clicks a reference in the explore panel),
7779
// we want this to be open and visible in the file tree.
7880
useEffect(() => {
79-
const pathParts = path.split('/').filter(Boolean);
81+
let pathParts = path.split('/').filter(Boolean);
82+
83+
// If the path is a blob, we want to open the parent directory.
84+
if (pathType === 'blob') {
85+
pathParts = pathParts.slice(0, -1);
86+
}
8087

8188
setOpenPaths(current => {
8289
const next = new Set<string>(current);
@@ -85,11 +92,11 @@ export const FileTreePanel = ({ order }: FileTreePanelProps) => {
8592
}
8693
return next;
8794
});
88-
}, [path]);
95+
}, [path, pathType]);
8996

9097
// When the user clicks a file tree node, we will want to either
9198
// add or remove it from the open paths depending on if it's already open or not.
92-
const onNodeClicked = useCallback((node: FileTreeNode) => {
99+
const onTreeNodeClicked = useCallback((node: FileTreeNode) => {
93100
if (!openPaths.has(node.path)) {
94101
setOpenPaths(current => {
95102
const next = new Set(current);
@@ -105,14 +112,6 @@ export const FileTreePanel = ({ order }: FileTreePanelProps) => {
105112
}
106113
}, [openPaths]);
107114

108-
// @debug: format the tree for console output.
109-
// useEffect(() => {
110-
// if (!tree) {
111-
// return;
112-
// }
113-
// console.debug(__debugFormatTreeForConsole(tree));
114-
// }, [tree]);
115-
116115
useHotkeys("mod+b", () => {
117116
if (isFileTreePanelCollapsed) {
118117
fileTreePanelRef.current?.expand();
@@ -183,7 +182,7 @@ export const FileTreePanel = ({ order }: FileTreePanelProps) => {
183182
</Tooltip>
184183
</div>
185184
<Separator orientation="horizontal" className="w-full mb-2" />
186-
{!tree ? (
185+
{isPending ? (
187186
<FileTreePanelSkeleton />
188187
) :
189188
isError ? (
@@ -192,10 +191,10 @@ export const FileTreePanel = ({ order }: FileTreePanelProps) => {
192191
</div>
193192
) : (
194193
<PureFileTreePanel
195-
tree={tree}
194+
tree={data.tree}
196195
openPaths={openPaths}
197196
path={path}
198-
onNodeClicked={onNodeClicked}
197+
onTreeNodeClicked={onTreeNodeClicked}
199198
/>
200199
)}
201200
</div>
@@ -387,18 +386,3 @@ const FileTreePanelSkeleton = () => {
387386
</div>
388387
)
389388
}
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-
};

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

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ interface PureFileTreePanelProps {
2222
tree: FileTreeNode;
2323
openPaths: Set<string>;
2424
path: string;
25-
onNodeClicked: (node: FileTreeNode) => void;
25+
onTreeNodeClicked: (node: FileTreeNode) => void;
2626
}
2727

28-
export const PureFileTreePanel = ({ tree, openPaths, path, onNodeClicked }: PureFileTreePanelProps) => {
28+
export const PureFileTreePanel = ({ tree, openPaths, path, onTreeNodeClicked }: PureFileTreePanelProps) => {
2929
const scrollAreaRef = useRef<HTMLDivElement>(null);
3030
const { repoName, revisionName } = useBrowseParams();
3131
const domain = useDomain();
@@ -54,8 +54,8 @@ export const PureFileTreePanel = ({ tree, openPaths, path, onNodeClicked }: Pure
5454
// (i.e., not ctrl/cmd click).
5555
onClick={(e) => {
5656
const isMetaOrCtrlKey = e.metaKey || e.ctrlKey;
57-
if (!isMetaOrCtrlKey) {
58-
onNodeClicked(node);
57+
if (node.type === 'tree' && !isMetaOrCtrlKey) {
58+
onTreeNodeClicked(node);
5959
}
6060
}}
6161
// @note: onNavigate _won't_ be called when the user ctrl/cmd clicks on a tree node.
@@ -69,14 +69,18 @@ export const PureFileTreePanel = ({ tree, openPaths, path, onNodeClicked }: Pure
6969
parentRef={scrollAreaRef}
7070
/>
7171
{node.type === 'tree' && node.children.length > 0 && openPaths.has(node.path) && renderTree(node, depth + 1)}
72-
{/* @note: a empty tree indicates that the contents are beaing loaded. Render a loading skeleton to indicate that. */}
72+
{/*
73+
@note: a empty tree indicates that the contents are beaing loaded. Render a loading skeleton to indicate that.
74+
This relies on the fact that you cannot have empty tress in git.
75+
@see: https://archive.kernel.org/oldwiki/git.wiki.kernel.org/index.php/GitFaq.html#Can_I_add_empty_directories.3F
76+
*/}
7377
{node.type === 'tree' && node.children.length === 0 && openPaths.has(node.path) && renderLoadingSkeleton(depth)}
7478
</React.Fragment>
7579
);
7680
})}
7781
</>
7882
);
79-
}, [domain, onNodeClicked, path, repoName, revisionName, openPaths]);
83+
}, [domain, onTreeNodeClicked, path, repoName, revisionName, openPaths]);
8084

8185
const renderedTree = useMemo(() => renderTree(tree), [tree, renderTree]);
8286

packages/web/src/lib/posthogEvents.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,10 @@ export type PosthogEventMap = {
181181
isGlobalSearchEnabled: boolean,
182182
},
183183
//////////////////////////////////////////////////////////////////
184+
wa_file_tree_loaded: {
185+
durationMs: number,
186+
},
187+
//////////////////////////////////////////////////////////////////
184188
api_code_search_request: {
185189
source: string;
186190
type: 'streamed' | 'blocking';

0 commit comments

Comments
 (0)