import type { FileEntry, IndexLoaderData } from 'lib/types' import { paths } from 'lib/paths' import { ActionButton } from './ActionButton' import Tooltip from './Tooltip' import { Dispatch, useEffect, useRef, useState } from 'react' import { useNavigate, useRouteLoaderData } from 'react-router-dom' import { Dialog, Disclosure } from '@headlessui/react' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faChevronRight, faTrashAlt } from '@fortawesome/free-solid-svg-icons' import { useFileContext } from 'hooks/useFileContext' import { useHotkeys } from 'react-hotkeys-hook' import styles from './FileTree.module.css' import { sortProject } from 'lib/tauriFS' import { FILE_EXT } from 'lib/constants' import { CustomIcon } from './CustomIcon' import { codeManager, kclManager } from 'lib/singletons' import { useDocumentHasFocus } from 'hooks/useDocumentHasFocus' import { useLspContext } from './LspProvider' function getIndentationCSS(level: number) { return `calc(1rem * ${level + 1})` } function RenameForm({ fileOrDir, setIsRenaming, level = 0, }: { fileOrDir: FileEntry setIsRenaming: Dispatch> level?: number }) { const { send } = useFileContext() const inputRef = useRef(null) function handleRenameSubmit(e: React.FormEvent) { e.preventDefault() setIsRenaming(false) send({ type: 'Rename file', data: { oldName: fileOrDir.name || '', newName: inputRef.current?.value || fileOrDir.name || '', isDir: fileOrDir.children !== undefined, }, }) } function handleKeyDown(e: React.KeyboardEvent) { if (e.key === 'Escape') { e.stopPropagation() setIsRenaming(false) } } return (
) } function DeleteConfirmationDialog({ fileOrDir, setIsOpen, }: { fileOrDir: FileEntry setIsOpen: Dispatch> }) { const { send } = useFileContext() return ( setIsOpen(false)} className="relative z-50" >
Delete {fileOrDir.children !== undefined ? 'Folder' : 'File'} This will permanently delete "{fileOrDir.name || 'this file'}" {fileOrDir.children !== undefined ? ' and all of its contents. ' : '. '} This action cannot be undone.
{ send({ type: 'Delete file', data: fileOrDir }) setIsOpen(false) }} iconStart={{ icon: faTrashAlt, bgClassName: 'bg-destroy-80', iconClassName: 'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10 dark:text-destroy-20 dark:group-hover:text-destroy-10 dark:hover:text-destroy-10', }} className="hover:border-destroy-40 dark:hover:border-destroy-40" > Delete setIsOpen(false)}> Cancel
) } const FileTreeItem = ({ project, currentFile, fileOrDir, onDoubleClick, level = 0, }: { project?: IndexLoaderData['project'] currentFile?: IndexLoaderData['file'] fileOrDir: FileEntry onDoubleClick?: () => void level?: number }) => { const { send, context } = useFileContext() const { onFileOpen, onFileClose } = useLspContext() const navigate = useNavigate() const [isRenaming, setIsRenaming] = useState(false) const [isConfirmingDelete, setIsConfirmingDelete] = useState(false) const isCurrentFile = fileOrDir.path === currentFile?.path function handleKeyUp(e: React.KeyboardEvent) { if (e.metaKey && e.key === 'Backspace') { // Open confirmation dialog setIsConfirmingDelete(true) } else if (e.key === 'Enter') { // Show the renaming form setIsRenaming(true) } else if (e.code === 'Space') { handleDoubleClick() } } function handleDoubleClick() { if (fileOrDir.children !== undefined) return // Don't open directories if (fileOrDir.name?.endsWith(FILE_EXT) === false && project?.path) { // Import non-kcl files // We want to update both the state and editor here. codeManager.updateCodeStateEditor( `import("${fileOrDir.path.replace(project.path, '.')}")\n` + codeManager.code ) codeManager.writeToFile() kclManager.executeCode(true) } else { // Let the lsp servers know we closed a file. onFileClose(currentFile?.path || null, project?.path || null) onFileOpen(fileOrDir.path, project?.path || null) // Open kcl files navigate(`${paths.FILE}/${encodeURIComponent(fileOrDir.path)}`) } onDoubleClick?.() } return ( <> {fileOrDir.children === undefined ? (
  • {!isRenaming ? ( ) : ( )}
  • ) : ( {({ open }) => (
    {!isRenaming ? ( e.currentTarget.focus()} onClickCapture={(e) => send({ type: 'Set selected directory', data: fileOrDir }) } onFocusCapture={(e) => send({ type: 'Set selected directory', data: fileOrDir }) } onKeyDown={(e) => e.key === 'Enter' && e.preventDefault()} onKeyUp={handleKeyUp} > {fileOrDir.name} ) : (
    )}
      { send({ type: 'Set selected directory', data: fileOrDir }) }} onFocusCapture={(e) => send({ type: 'Set selected directory', data: fileOrDir }) } > {fileOrDir.children?.map((child) => ( ))}
    )}
    )} {isConfirmingDelete && ( )} ) } interface FileTreeProps { className?: string file?: IndexLoaderData['file'] closePanel: ( focusableElement?: | HTMLElement | React.MutableRefObject | undefined ) => void } export const FileTreeMenu = () => { const { send } = useFileContext() async function createFile() { send({ type: 'Create file', data: { name: '', makeDir: false } }) } async function createFolder() { send({ type: 'Create file', data: { name: '', makeDir: true } }) } useHotkeys('meta + n', createFile) useHotkeys('meta + shift + n', createFolder) return ( <> Create file Create folder ) } export const FileTree = ({ className = '', closePanel }: FileTreeProps) => { return (

    Files

    ) } export const FileTreeInner = ({ onDoubleClick, }: { onDoubleClick?: () => void }) => { const loaderData = useRouteLoaderData(paths.FILE) as IndexLoaderData const { send, context } = useFileContext() const documentHasFocus = useDocumentHasFocus() // Refresh the file tree when the document gets focus useEffect(() => { send({ type: 'Refresh' }) }, [documentHasFocus]) return (
      { send({ type: 'Set selected directory', data: context.project }) }} > {sortProject(context.project.children || []).map((fileOrDir) => ( ))}
    ) }