import { type IndexLoaderData } from 'lib/types' import { paths } from 'lib/paths' import { ActionButton } from './ActionButton' import Tooltip from './Tooltip' import { FileEntry } from '@tauri-apps/api/fs' import { Dispatch, useEffect, useRef, useState } from 'react' import { useNavigate } 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 { FILE_EXT, sortProject } from 'lib/tauriFS' import { CustomIcon } from './CustomIcon' import { kclManager } from 'lang/KclSingleton' import { useDocumentHasFocus } from 'hooks/useDocumentHasFocus' import { useLspContext } from './LspProvider' function getIndentationCSS(level: number) { return `calc(1rem * ${level + 1})` } // an OS-agnostic way to get the basename of the path. export function basename(path: string): string { // Regular expression to match the last portion of the path, taking into account both POSIX and Windows delimiters const re = /[^\\/]+$/ const match = path.match(re) return match ? match[0] : '' } 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) }} icon={{ 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, closePanel, level = 0, }: { project?: IndexLoaderData['project'] currentFile?: IndexLoaderData['file'] fileOrDir: FileEntry closePanel: ( focusableElement?: | HTMLElement | React.MutableRefObject | undefined ) => void level?: number }) => { const { send, context } = useFileContext() const { lspClients } = 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 kclManager.setCodeAndExecute( `import("${fileOrDir.path.replace(project.path, '.')}")\n` + kclManager.code ) } else { // Let the lsp servers know we closed a file. const currentFilePath = basename(currentFile?.path || 'main.kcl') lspClients.forEach((lspClient) => { lspClient.textDocumentDidClose({ textDocument: { uri: `file:///${currentFilePath}`, }, }) }) const newFilePath = basename(fileOrDir.path) // Then let the clients know we opened a file. lspClients.forEach((lspClient) => { lspClient.textDocumentDidOpen({ textDocument: { uri: `file:///${newFilePath}`, languageId: 'kcl', version: 1, text: '', }, }) }) // Open kcl files navigate(`${paths.FILE}/${encodeURIComponent(fileOrDir.path)}`) } closePanel() } 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 FileTree = ({ className = '', file, closePanel, }: FileTreeProps) => { const { send, context } = useFileContext() const docuemntHasFocus = useDocumentHasFocus() useHotkeys('meta + n', createFile) useHotkeys('meta + shift + n', createFolder) // Refresh the file tree when the document gets focus useEffect(() => { send({ type: 'Refresh' }) }, [docuemntHasFocus]) async function createFile() { send({ type: 'Create file', data: { name: '', makeDir: false } }) } async function createFolder() { send({ type: 'Create file', data: { name: '', makeDir: true } }) } return (

    Files

    Create File Create Folder
      { send({ type: 'Set selected directory', data: context.project }) }} > {sortProject(context.project.children || []).map((fileOrDir) => ( ))}
    ) }