import type { IndexLoaderData } from 'lib/types' import { PATHS } from 'lib/paths' import { ActionButton } from './ActionButton' import Tooltip from './Tooltip' import { Dispatch, useCallback, useEffect, useRef, useState } from 'react' import { useNavigate, useRouteLoaderData } from 'react-router-dom' import { Disclosure } from '@headlessui/react' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faChevronRight } from '@fortawesome/free-solid-svg-icons' import { useFileContext } from 'hooks/useFileContext' import styles from './FileTree.module.css' import { sortProject } from 'lib/desktopFS' 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' import useHotkeyWrapper from 'lib/hotkeyWrapper' import { useModelingContext } from 'hooks/useModelingContext' import { DeleteConfirmationDialog } from './ProjectCard/DeleteProjectDialog' import { ContextMenu, ContextMenuItem } from './ContextMenu' import usePlatform from 'hooks/usePlatform' import { FileEntry } from 'lib/project' function getIndentationCSS(level: number) { return `calc(1rem * ${level + 1})` } function RenameForm({ fileOrDir, onSubmit, level = 0, }: { fileOrDir: FileEntry onSubmit: () => void level?: number }) { const { send } = useFileContext() const inputRef = useRef(null) function handleRenameSubmit(e: React.FormEvent) { e.preventDefault() send({ type: 'Rename file', data: { oldName: fileOrDir.name || '', newName: inputRef.current?.value || fileOrDir.name || '', isDir: fileOrDir.children !== null, }, }) } function handleKeyDown(e: React.KeyboardEvent) { if (e.key === 'Escape') { e.stopPropagation() onSubmit() } } return (
) } function DeleteFileTreeItemDialog({ fileOrDir, setIsOpen, }: { fileOrDir: FileEntry setIsOpen: Dispatch> }) { const { send } = useFileContext() return ( setIsOpen(false)} onConfirm={() => { send({ type: 'Delete file', data: fileOrDir }) setIsOpen(false) }} >

This will permanently delete "{fileOrDir.name || 'this file'}" {fileOrDir.children !== null ? ' and all of its contents. ' : '. '}

Are you sure you want to delete "{fileOrDir.name || 'this file'} "? This action cannot be undone.

) } const FileTreeItem = ({ project, currentFile, fileOrDir, onNavigateToFile, level = 0, }: { project?: IndexLoaderData['project'] currentFile?: IndexLoaderData['file'] fileOrDir: FileEntry onNavigateToFile?: () => void level?: number }) => { const { send: fileSend, context: fileContext } = useFileContext() const { onFileOpen, onFileClose } = useLspContext() const navigate = useNavigate() const [isConfirmingDelete, setIsConfirmingDelete] = useState(false) const isCurrentFile = fileOrDir.path === currentFile?.path const itemRef = useRef(null) const isRenaming = fileContext.itemsBeingRenamed.includes(fileOrDir.path) const removeCurrentItemFromRenaming = useCallback( () => fileSend({ type: 'assign', data: { itemsBeingRenamed: fileContext.itemsBeingRenamed.filter( (path) => path !== fileOrDir.path ), }, }), [fileContext.itemsBeingRenamed, fileOrDir.path, fileSend] ) const addCurrentItemToRenaming = useCallback(() => { fileSend({ type: 'assign', data: { itemsBeingRenamed: [...fileContext.itemsBeingRenamed, fileOrDir.path], }, }) }, [fileContext.itemsBeingRenamed, fileOrDir.path, fileSend]) 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 addCurrentItemToRenaming() } else if (e.code === 'Space') { handleClick() } } function handleClick() { if (fileOrDir.children !== null) 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() // Prevent seeing the model built one piece at a time when changing files kclManager.isFirstRender = true kclManager.executeCode(true).then(() => { kclManager.isFirstRender = false }) } 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)}`) } onNavigateToFile?.() } return (
{fileOrDir.children === null ? (
  • {!isRenaming ? ( ) : ( )}
  • ) : ( {({ open }) => (
    {!isRenaming ? ( e.currentTarget.focus()} onClickCapture={(e) => fileSend({ type: 'Set selected directory', data: fileOrDir, }) } onFocusCapture={(e) => fileSend({ type: 'Set selected directory', data: fileOrDir, }) } onKeyDown={(e) => e.key === 'Enter' && e.preventDefault()} onKeyUp={handleKeyUp} > {fileOrDir.name} ) : (
    )}
      { fileSend({ type: 'Set selected directory', data: fileOrDir, }) }} onFocusCapture={(e) => fileSend({ type: 'Set selected directory', data: fileOrDir, }) } > {fileOrDir.children?.map((child) => ( ))}
    )}
    )} {isConfirmingDelete && ( )} setIsConfirmingDelete(true)} />
    ) } interface FileTreeContextMenuProps { itemRef: React.RefObject onRename: () => void onDelete: () => void } function FileTreeContextMenu({ itemRef, onRename, onDelete, }: FileTreeContextMenuProps) { const platform = usePlatform() const metaKey = platform === 'macos' ? '⌘' : 'Ctrl' return ( Rename , Delete , ]} /> ) } interface FileTreeProps { className?: string file?: IndexLoaderData['file'] onNavigateToFile: ( 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 }, }) } useHotkeyWrapper(['mod + n'], createFile) useHotkeyWrapper(['mod + shift + n'], createFolder) return ( <> Create file Create folder ) } export const FileTree = ({ className = '', onNavigateToFile: closePanel, }: FileTreeProps) => { return (

    Files

    ) } export const FileTreeInner = ({ onNavigateToFile, }: { onNavigateToFile?: () => void }) => { const loaderData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData const { send: fileSend, context: fileContext } = useFileContext() const { send: modelingSend } = useModelingContext() const documentHasFocus = useDocumentHasFocus() // Refresh the file tree when the document gets focus useEffect(() => { fileSend({ type: 'Refresh' }) }, [documentHasFocus]) return (
      { fileSend({ type: 'Set selected directory', data: fileContext.project, }) }} > {sortProject(fileContext.project?.children || []).map((fileOrDir) => ( { // Reset modeling state when navigating to a new file modelingSend({ type: 'Cancel' }) onNavigateToFile?.() }} key={fileOrDir.path} /> ))}
    ) }