22
33import { useBrowseParams } from "@/app/[domain]/browse/hooks/useBrowseParams" ;
44import { useBrowseState } from "@/app/[domain]/browse/hooks/useBrowseState" ;
5- import { getFolderContents , getTree } from "@/app/api/(client)/client" ;
5+ import { getTree } from "@/app/api/(client)/client" ;
66import { KeyboardShortcutHint } from "@/app/components/keyboardShortcutHint" ;
77import { Button } from "@/components/ui/button" ;
88import { 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 { unwrapServiceError } from "@/lib/utils" ;
12+ import { measure , unwrapServiceError } from "@/lib/utils" ;
1313import { useQuery } from "@tanstack/react-query" ;
1414import { SearchIcon } from "lucide-react" ;
1515import { 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