1- import React , { type JSX , useCallback , useEffect , useRef , useState } from 'react'
1+ import React , { type JSX , useCallback , useEffect , useMemo , useRef , useState } from 'react'
22import Search from '~/components/search/search'
33import LoadingSpinner from '~/components/loading-spinner'
44import FolderIcon from '../../../icons/solar/Folder.svg?react'
55import FolderOpenIcon from '../../../icons/solar/Folder Open.svg?react'
6+ import ListDown from '../../../icons/solar/List Down.svg?react'
67import '/styles/editor-files.css'
78import AltArrowRightIcon from '../../../icons/solar/Alt Arrow Right.svg?react'
89import AltArrowDownIcon from '../../../icons/solar/Alt Arrow Down.svg?react'
@@ -11,6 +12,7 @@ import CodeFileIcon from '../../../icons/solar/Code File.svg?react'
1112import TrashBinIcon from '../../../icons/solar/Trash Bin.svg?react'
1213import Pen from '../../../icons/solar/Pen.svg?react'
1314import { useShortcut } from '~/hooks/use-shortcut'
15+ import { getAncestorIds , isVisibleInTree , selectAndReveal , toTreeItemId } from './tree-utilities'
1416import type { ContextMenuState } from './use-file-tree-context-menu'
1517
1618import {
@@ -51,16 +53,38 @@ export default function EditorFileStructure() {
5153 const getTab = useEditorTabStore ( ( state ) => state . getTab )
5254 const removeTab = useEditorTabStore ( ( state ) => state . removeTab )
5355 const removeTabAndSelectFallback = useEditorTabStore ( ( state ) => state . removeTabAndSelectFallback )
56+ const activeTabFilePath = useEditorTabStore ( ( state ) => state . activeTabFilePath )
5457
5558 const [ dataProvider , setDataProvider ] = useState < EditorFilesDataProvider | null > ( null )
5659 const [ selectedItemId , setSelectedItemId ] = useState < TreeItemIndex | null > ( null )
60+ const [ rootPath , setRootPath ] = useState < string | null > ( null )
5761
5862 const expandedItemsRef = useRef ( editorExpandedItems )
5963
6064 useEffect ( ( ) => {
6165 expandedItemsRef . current = editorExpandedItems
6266 } , [ editorExpandedItems ] )
6367
68+ useEffect ( ( ) => {
69+ if ( ! dataProvider ) {
70+ setRootPath ( null )
71+ return
72+ }
73+ void dataProvider . getTreeItem ( 'root' ) . then ( ( root ) => {
74+ if ( root ) setRootPath ( ( root . data as FileNode ) . path )
75+ } )
76+ } , [ dataProvider ] )
77+
78+ const activeTabItemId = useMemo (
79+ ( ) => ( rootPath && activeTabFilePath ? toTreeItemId ( activeTabFilePath , rootPath ) : null ) ,
80+ [ rootPath , activeTabFilePath ] ,
81+ )
82+
83+ const isActiveItemVisible = useMemo (
84+ ( ) => isVisibleInTree ( activeTabItemId , editorExpandedItems ) ,
85+ [ activeTabItemId , editorExpandedItems ] ,
86+ )
87+
6488 const onAfterRename = useCallback (
6589 ( oldPath : string , newName : string ) => {
6690 const tab = getTab ( oldPath )
@@ -126,12 +150,31 @@ export default function EditorFileStructure() {
126150 [ buildContextForItem ] ,
127151 )
128152
153+ const revealActiveFile = useCallback ( async ( ) => {
154+ if ( ! dataProvider || ! activeTabFilePath || ! rootPath || ! tree . current ) return
155+
156+ const itemId = toTreeItemId ( activeTabFilePath , rootPath )
157+
158+ for ( const ancestorId of getAncestorIds ( itemId ) ) {
159+ await dataProvider . loadDirectory ( ancestorId )
160+ tree . current . expandItem ( ancestorId )
161+ }
162+
163+ selectAndReveal ( tree . current , itemId )
164+ } , [ dataProvider , activeTabFilePath , rootPath ] )
165+
129166 useShortcut ( {
130167 'explorer.new-file' : ( ) => triggerExplorerAction ( editorContextMenu . handleNewFile , false ) ,
131168 'explorer.new-folder' : ( ) => triggerExplorerAction ( editorContextMenu . handleNewFolder , false ) ,
132- 'explorer.rename' : ( ) => triggerExplorerAction ( editorContextMenu . handleRename , true ) ,
133- 'explorer.delete' : ( ) => triggerExplorerAction ( editorContextMenu . handleDelete , true ) ,
134- 'explorer.delete-mac' : ( ) => triggerExplorerAction ( editorContextMenu . handleDelete , true ) ,
169+ 'explorer.rename' : ( ) => {
170+ if ( ! selectedItemId ) return false
171+ triggerExplorerAction ( editorContextMenu . handleRename , true )
172+ } ,
173+ 'explorer.delete' : ( ) => {
174+ if ( ! selectedItemId ) return false
175+ triggerExplorerAction ( editorContextMenu . handleDelete , true )
176+ } ,
177+ 'explorer.reveal' : ( ) => void revealActiveFile ( ) ,
135178 } )
136179
137180 useEffect ( ( ) => {
@@ -421,6 +464,14 @@ export default function EditorFileStructure() {
421464 < div className = "border-border flex items-center justify-between border-b px-2 py-1" >
422465 < span className = "text-foreground/50 text-xs font-semibold tracking-wider uppercase" > Explorer</ span >
423466 < div className = "flex items-center gap-0.5" >
467+ < button
468+ className = { `${ toolbarBtnClass } ${ ! activeTabFilePath || isActiveItemVisible ? 'cursor-not-allowed opacity-40' : '' } ` }
469+ title = "Open File Tree to Active Tab"
470+ disabled = { ! activeTabFilePath || isActiveItemVisible }
471+ onClick = { ( ) => void revealActiveFile ( ) }
472+ >
473+ < ListDown className = "fill-foreground h-5 w-5" />
474+ </ button >
424475 < button
425476 className = { toolbarBtnClass }
426477 title = "New File"
@@ -458,6 +509,9 @@ export default function EditorFileStructure() {
458509 } }
459510 onCollapseItem = { ( item ) => {
460511 removeEditorExpandedItem ( String ( item . index ) )
512+ setSelectedItemId ( ( previous ) =>
513+ previous && String ( previous ) . startsWith ( `${ String ( item . index ) } /` ) ? null : previous ,
514+ )
461515 } }
462516 getItemTitle = { getItemTitle }
463517 dataProvider = { dataProvider }
0 commit comments