@@ -9,6 +9,7 @@ import { ResizablePanel } from "@/components/ui/resizable";
99import { Separator } from "@/components/ui/separator" ;
1010import { Skeleton } from "@/components/ui/skeleton" ;
1111import { Tooltip , TooltipContent , TooltipTrigger } from "@/components/ui/tooltip" ;
12+ import useCaptureEvent from "@/hooks/useCaptureEvent" ;
1213import { measure , unwrapServiceError } from "@/lib/utils" ;
1314import { useQuery } from "@tanstack/react-query" ;
1415import { SearchIcon } from "lucide-react" ;
@@ -19,8 +20,8 @@ import {
1920 GoSidebarCollapse as ExpandIcon
2021} from "react-icons/go" ;
2122import { ImperativePanelHandle } from "react-resizable-panels" ;
22- import { PureFileTreePanel } from "./pureFileTreePanel" ;
2323import { FileTreeNode } from "../types" ;
24+ import { PureFileTreePanel } from "./pureFileTreePanel" ;
2425
2526interface 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- } ;
0 commit comments