diff --git a/src/components/Explorer/utils.ts b/src/components/Explorer/utils.ts index 9ca9781b2..836cf3237 100644 --- a/src/components/Explorer/utils.ts +++ b/src/components/Explorer/utils.ts @@ -101,7 +101,7 @@ const flattenProjectHelper = ( name: f.name, }), setSize, - positionInSet + positionInSet, } // keep track of the file once within the recursive list that will be built up list.push(content) @@ -120,7 +120,7 @@ const flattenProjectHelper = ( constructPath({ parentPath: parentPath, name: f.name }), level + 1, f.children.length, - i+1 + i + 1 ) } } @@ -145,7 +145,7 @@ export const flattenProject = ( projectName, // first parent 0, projectChildren.length, - index+1 + index + 1 ) } return flattenTreeInOrder diff --git a/src/components/FileMachineProvider.tsx b/src/components/FileMachineProvider.tsx index 63a3e6252..584e5c101 100644 --- a/src/components/FileMachineProvider.tsx +++ b/src/components/FileMachineProvider.tsx @@ -1,51 +1,21 @@ -import { useMachine } from '@xstate/react' -import React, { createContext, useEffect, useMemo } from 'react' -import { toast } from 'react-hot-toast' +import React, { useEffect, useMemo } from 'react' import { useLocation, useNavigate, useRouteLoaderData } from 'react-router-dom' -import type { - Actor, - AnyStateMachine, - ContextFrom, - Prop, - StateFrom, -} from 'xstate' -import { fromPromise } from 'xstate' import { useAbsoluteFilePath } from '@src/hooks/useAbsoluteFilePath' import { useMenuListener } from '@src/hooks/useMenu' -import { newKclFile } from '@src/lang/project' import { createNamedViewsCommand } from '@src/lib/commandBarConfigs/namedViewsConfig' import { createRouteCommands } from '@src/lib/commandBarConfigs/routeCommandConfig' -import { - DEFAULT_DEFAULT_LENGTH_UNIT, - DEFAULT_FILE_NAME, - DEFAULT_PROJECT_KCL_FILE, - FILE_EXT, -} from '@src/lib/constants' -import { getProjectInfo } from '@src/lib/desktop' -import { getNextDirName, getNextFileName } from '@src/lib/desktopFS' +import { DEFAULT_DEFAULT_LENGTH_UNIT } from '@src/lib/constants' import { isDesktop } from '@src/lib/isDesktop' import { kclCommands } from '@src/lib/kclCommands' import { BROWSER_PATH, PATHS } from '@src/lib/paths' import { markOnce } from '@src/lib/performance' import { codeManager, kclManager } from '@src/lib/singletons' -import { err } from '@src/lib/trap' import { type IndexLoaderData } from '@src/lib/types' import { useSettings, useToken } from '@src/lib/singletons' import { commandBarActor } from '@src/lib/singletons' -import { fileMachine } from '@src/machines/fileMachine' import { modelingMenuCallbackMostActions } from '@src/menu/register' -type MachineContext = { - state: StateFrom - context: ContextFrom - send: Prop, 'send'> -} - -export const FileContext = createContext( - {} as MachineContext -) - export const FileMachineProvider = ({ children, }: { @@ -93,300 +63,6 @@ export const FileMachineProvider = ({ markOnce('code/didLoadFile') }, []) - const [state, send] = useMachine( - fileMachine.provide({ - actions: { - renameToastSuccess: ({ event }) => { - if (event.type !== 'xstate.done.actor.rename-file') return - toast.success(event.output.message) - }, - createToastSuccess: ({ event }) => { - if (event.type !== 'xstate.done.actor.create-and-open-file') return - toast.success(event.output.message) - }, - toastSuccess: ({ event }) => { - if ( - event.type !== 'xstate.done.actor.rename-file' && - event.type !== 'xstate.done.actor.delete-file' - ) - return - toast.success(event.output.message) - }, - toastError: ({ event }) => { - if (event.type !== 'xstate.done.actor.rename-file') return - toast.error(event.output.message) - }, - navigateToFile: ({ context, event }) => { - if (event.type !== 'xstate.done.actor.create-and-open-file') return - if (event.output && 'name' in event.output) { - // TODO: Technically this is not the same as the FileTree Onclick even if they are in the same page - // What is "Open file?" - commandBarActor.send({ type: 'Close' }) - navigate( - `..${PATHS.FILE}/${encodeURIComponent( - // TODO: Should this be context.selectedDirectory.path? - // eslint-disable-next-line @typescript-eslint/restrict-plus-operands - context.selectedDirectory + - window.electron.path.sep + - // eslint-disable-next-line @typescript-eslint/restrict-plus-operands - event.output.name - )}` - ) - } else if ( - event.output && - 'path' in event.output && - event.output.path.endsWith(FILE_EXT) - ) { - // Don't navigate to newly created directories - navigate(`..${PATHS.FILE}/${encodeURIComponent(event.output.path)}`) - } - }, - openFileInNewWindow: ({ event }) => { - if (event.type !== 'Open file in new window') { - return - } - - commandBarActor.send({ type: 'Close' }) - window.electron.openInNewWindow(event.data.name) - }, - }, - actors: { - readFiles: fromPromise(async ({ input }) => { - const newFiles = - (isDesktop() ? (await getProjectInfo(input.path)).children : []) ?? - [] - return { - ...input, - children: newFiles, - } - }), - createAndOpenFile: fromPromise(async ({ input }) => { - let createdName = input.name.trim() || DEFAULT_FILE_NAME - let createdPath: string - - if ( - (input.targetPathToClone && - (await window.electron.statIsDirectory( - input.targetPathToClone - ))) || - input.makeDir - ) { - let { name, path } = getNextDirName({ - entryName: input.targetPathToClone - ? window.electron.path.basename(input.targetPathToClone) - : createdName, - baseDir: input.targetPathToClone - ? window.electron.path.dirname(input.targetPathToClone) - : input.selectedDirectory.path, - }) - createdName = name - createdPath = path - await window.electron.mkdir(createdPath) - } else { - const isTargetPathToCloneASubPath = - input.targetPathToClone && - input.selectedDirectory.path.indexOf(input.targetPathToClone) > -1 - if (isTargetPathToCloneASubPath) { - const { name, path } = getNextFileName({ - entryName: input.targetPathToClone - ? window.electron.path.basename(input.targetPathToClone) - : createdName, - baseDir: input.targetPathToClone - ? window.electron.path.dirname(input.targetPathToClone) - : input.selectedDirectory.path, - }) - createdName = name - createdPath = path - } else { - const { name, path } = getNextFileName({ - entryName: input.targetPathToClone - ? window.electron.path.basename(input.targetPathToClone) - : createdName, - baseDir: input.selectedDirectory.path, - }) - createdName = name - createdPath = path - } - if (input.targetPathToClone) { - await window.electron.copyFile( - input.targetPathToClone, - createdPath - ) - } else { - const codeToWrite = newKclFile( - input.content, - settings.modeling.defaultUnit.current - ) - if (err(codeToWrite)) return Promise.reject(codeToWrite) - await window.electron.writeFile(createdPath, codeToWrite) - } - } - - return { - message: `Successfully created "${createdName}"`, - path: createdPath, - shouldSetToRename: input.shouldSetToRename, - } - }), - createFile: fromPromise(async ({ input }) => { - let createdName = input.name.trim() || DEFAULT_FILE_NAME - let createdPath: string - - if (input.makeDir) { - let { name, path } = getNextDirName({ - entryName: createdName, - baseDir: input.selectedDirectory.path, - }) - createdName = name - createdPath = path - await window.electron.mkdir(createdPath) - } else { - const { name, path } = getNextFileName({ - entryName: createdName, - baseDir: input.selectedDirectory.path, - }) - createdName = name - createdPath = path - const codeToWrite = newKclFile( - input.content, - settings.modeling.defaultUnit.current - ) - if (err(codeToWrite)) return Promise.reject(codeToWrite) - await window.electron.writeFile(createdPath, codeToWrite) - } - - return { - path: createdPath, - } - }), - renameFile: fromPromise(async ({ input }) => { - const { oldName, newName, isDir } = input - const name = newName - ? newName.endsWith(FILE_EXT) || isDir - ? newName - : newName + FILE_EXT - : DEFAULT_FILE_NAME - const oldPath = window.electron.path.join( - input.parentDirectory.path, - oldName - ) - const newPath = window.electron.path.join( - input.parentDirectory.path, - name - ) - - // no-op - if (oldPath === newPath) { - return { - message: `Old is the same as new.`, - newPath, - oldPath, - } - } - - // if there are any siblings with the same name, report error. - const entries = await window.electron.readdir( - window.electron.path.dirname(newPath) - ) - for (let entry of entries) { - if (entry === newName) { - return Promise.reject(new Error('Filename already exists.')) - } - } - - window.electron.rename(oldPath, newPath) - - if (!file) { - return Promise.reject(new Error('file is not defined')) - } - - if (oldPath === file.path && project?.path) { - // If we just renamed the current file, navigate to the new path - navigate(`..${PATHS.FILE}/${encodeURIComponent(newPath)}`) - } else if (file?.path.includes(oldPath)) { - // If we just renamed a directory that the current file is in, navigate to the new path - navigate( - `..${PATHS.FILE}/${encodeURIComponent( - file.path.replace(oldPath, newPath) - )}` - ) - } - - return { - message: `Successfully renamed "${oldName}" to "${name}"`, - newPath, - oldPath, - } - }), - deleteFile: fromPromise(async ({ input }) => { - const isDir = !!input.children - - if (isDir) { - await window.electron - .rm(input.path, { - recursive: true, - }) - .catch((e) => console.error('Error deleting directory', e)) - } else { - await window.electron - .rm(input.path) - .catch((e) => console.error('Error deleting file', e)) - } - - // If there are no more files at all in the project, create a main.kcl - // for when we navigate to the root. - if (!project?.path) { - return Promise.reject(new Error('Project path not set.')) - } - - const entries = await window.electron.readdir(project.path) - const hasKclEntries = - entries.filter((e: string) => e.endsWith('.kcl')).length !== 0 - if (!hasKclEntries) { - const codeToWrite = newKclFile( - undefined, - settings.modeling.defaultUnit.current - ) - if (err(codeToWrite)) return Promise.reject(codeToWrite) - const path = window.electron.path.join( - project.path, - DEFAULT_PROJECT_KCL_FILE - ) - await window.electron.writeFile(path, codeToWrite) - // Refresh the route selected above because it's possible we're on - // the same path on the navigate, which doesn't cause anything to - // refresh, leaving a stale execution state. - navigate(0) - return { - message: 'No more files in project, created main.kcl', - } - } - - // If we just deleted the current file or one of its parent directories, - // navigate to the project root - if ( - (input.path === file?.path || file?.path.includes(input.path)) && - project?.path - ) { - navigate(`../${PATHS.FILE}/${encodeURIComponent(project.path)}`) - } - - return { - message: `Successfully deleted ${isDir ? 'folder' : 'file'} "${ - input.name - }"`, - } - }), - }, - }), - { - input: { - project, - selectedDirectory: project, - }, - } - ) - // Due to the route provider, i've moved this to the FileMachineProvider instead of CommandBarProvider // This will register the commands to route to Telemetry, Home, and Settings. useEffect(() => { @@ -422,14 +98,15 @@ export const FileMachineProvider = ({ }) } - // GOTCHA: If we call navigate() while in the /file route the fileMachineProvider - // has a context.project of the original one that was loaded. It does not update - // Watch when the navigation changes, if it changes set a new Project within the fileMachine - // to load the latest state of the project you are in. - if (project) { - // TODO: Clean this up with global application state when fileMachine gets merged into SystemIOMachine - send({ type: 'Refresh with new project', data: { project } }) - } + // TODO: KEVIN DELETE + // // GOTCHA: If we call navigate() while in the /file route the fileMachineProvider + // // has a context.project of the original one that was loaded. It does not update + // // Watch when the navigation changes, if it changes set a new Project within the fileMachine + // // to load the latest state of the project you are in. + // if (project) { + // // TODO: Clean this up with global application state when fileMachine gets merged into SystemIOMachine + // send({ type: 'Refresh with new project', data: { project } }) + // } }, [location]) const cb = modelingMenuCallbackMostActions( @@ -480,7 +157,7 @@ export const FileMachineProvider = ({ }, specialPropsForInsertCommand: { providedOptions }, }) - }, [codeManager, kclManager, send, project, file]) + }, [codeManager, kclManager, project, file]) useEffect(() => { commandBarActor.send({ @@ -496,17 +173,7 @@ export const FileMachineProvider = ({ } }, [commandBarActor.send, kclCommandMemo]) - return ( - - {children} - - ) + return
{children}
} export default FileMachineProvider diff --git a/src/components/FileTree.module.css b/src/components/FileTree.module.css deleted file mode 100644 index f90491310..000000000 --- a/src/components/FileTree.module.css +++ /dev/null @@ -1,16 +0,0 @@ -.folder { - position: relative; -} - -.folder::after { - content: ""; - width: 1px; - z-index: -1; - @apply absolute top-0 bottom-0; - left: calc(var(--indent-line-left, 1rem) + 0.25rem); - @apply bg-chalkboard-30; -} - -:global(.dark) .folder::after { - @apply bg-chalkboard-80; -} diff --git a/src/components/FileTree.tsx b/src/components/FileTree.tsx deleted file mode 100644 index fb10d9575..000000000 --- a/src/components/FileTree.tsx +++ /dev/null @@ -1,845 +0,0 @@ -import { faChevronRight, faPencil } from '@fortawesome/free-solid-svg-icons' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { Disclosure } from '@headlessui/react' -import type { Dispatch } from 'react' -import { useCallback, useRef, useState } from 'react' -import { useNavigate, useRouteLoaderData } from 'react-router-dom' - -import { ActionButton } from '@src/components/ActionButton' -import { ContextMenu, ContextMenuItem } from '@src/components/ContextMenu' -import { CustomIcon } from '@src/components/CustomIcon' -import { useLspContext } from '@src/components/LspProvider' -import { DeleteConfirmationDialog } from '@src/components/ProjectCard/DeleteProjectDialog' -import Tooltip from '@src/components/Tooltip' -import { useFileContext } from '@src/hooks/useFileContext' -import { useFileSystemWatcher } from '@src/hooks/useFileSystemWatcher' -import { useModelingContext } from '@src/hooks/useModelingContext' -import usePlatform from '@src/hooks/usePlatform' -import { useKclContext } from '@src/lang/KclProvider' -import type { KCLError } from '@src/lang/errors' -import { kclErrorsByFilename } from '@src/lang/errors' -import { normalizeLineEndings } from '@src/lib/codeEditor' -import { FILE_EXT, INSERT_FOREIGN_TOAST_ID } from '@src/lib/constants' -import { sortFilesAndDirectories } from '@src/lib/desktopFS' -import useHotkeyWrapper from '@src/lib/hotkeyWrapper' -import { PATHS } from '@src/lib/paths' -import type { FileEntry } from '@src/lib/project' -import { codeManager, kclManager } from '@src/lib/singletons' -import { reportRejection } from '@src/lib/trap' -import type { IndexLoaderData } from '@src/lib/types' - -import { ToastInsert } from '@src/components/ToastInsert' -import { commandBarActor } from '@src/lib/singletons' -import toast from 'react-hot-toast' -import styles from './FileTree.module.css' - -function getIndentationCSS(level: number) { - return `calc(1rem * ${level + 1})` -} - -function TreeEntryInput(props: { - level: number - onSubmit: (value: string) => void -}) { - const [value, setValue] = useState('') - const onKeyPress = (e: React.KeyboardEvent) => { - if (e.key !== 'Enter') return - props.onSubmit(value) - } - - return ( - - ) -} - -function RenameForm({ - fileOrDir, - parentDir, - onSubmit, - level = 0, -}: { - fileOrDir: FileEntry - parentDir: FileEntry | undefined - onSubmit: () => void - level?: number -}) { - const { send } = useFileContext() - const inputRef = useRef(null) - const fileContext = useFileContext() - - function handleRenameSubmit(e: React.KeyboardEvent) { - if (e.key !== 'Enter') { - return - } - - send({ - type: 'Rename file', - data: { - oldName: fileOrDir.name || '', - newName: inputRef.current?.value || fileOrDir.name || '', - isDir: fileOrDir.children !== null, - parentDirectory: parentDir ?? fileContext.context.project, - }, - }) - - // To get out of the renaming state, without this the current file is still in renaming mode - onSubmit() - } - - function handleKeyDown(e: React.KeyboardEvent) { - if (e.key === 'Escape') { - e.stopPropagation() - onSubmit() - } else if (e.key === 'Enter') { - // This is needed to prevent events to bubble up and the form to be submitted. - // (Alternatively the form could be changed into a div.) - // Bug without this: - // - open a parent folder (close and open if it's already open) - // - right click -> rename one of its children - // - give new name and press enter - // -> new name is not applied, old name is reverted - e.preventDefault() - e.stopPropagation() - } - } - - 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 = ({ - parentDir, - project, - currentFile, - fileOrDir, - onNavigateToFile, - onClickDirectory, - onCreateFile, - onCreateFolder, - onCloneFileOrFolder, - onOpenInNewWindow, - newTreeEntry, - level = 0, - treeSelection, - setTreeSelection, - runtimeErrors, -}: { - parentDir: FileEntry | undefined - project?: IndexLoaderData['project'] - currentFile?: IndexLoaderData['file'] - fileOrDir: FileEntry - onNavigateToFile?: () => void - onClickDirectory: ( - open: boolean, - path: FileEntry, - parentDir: FileEntry | undefined - ) => void - onCreateFile: (name: string) => void - onCreateFolder: (name: string) => void - onCloneFileOrFolder: (path: string) => void - onOpenInNewWindow: (path: string) => void - newTreeEntry: TreeEntry - level?: number - treeSelection: FileEntry | undefined - setTreeSelection: Dispatch> - runtimeErrors: Map -}) => { - 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 isFileOrDirHighlighted = treeSelection?.path === fileOrDir?.path - const itemRef = useRef(null) - - const hasRuntimeError = runtimeErrors.has(fileOrDir.path) - - // Since every file or directory gets its own FileTreeItem, we can do this. - // Because subtrees only render when they are opened, that means this - // only listens when they open. Because this acts like a useEffect, when - // the ReactNodes are destroyed, so is this listener :) - useFileSystemWatcher( - async (eventType, path) => { - // Prevents a cyclic read / write causing editor problems such as - // misplaced cursor positions. - if (codeManager.writeCausedByAppCheckedInFileTreeFileSystemWatcher) { - codeManager.writeCausedByAppCheckedInFileTreeFileSystemWatcher = false - return - } - - // TODO: make this not just name based but sub path instead - const isImportedInCurrentFile = kclManager.ast.body.some( - (n) => - n.type === 'ImportStatement' && - ((n.path.type === 'Kcl' && - n.path.filename.includes(fileOrDir.name)) || - (n.path.type === 'Foreign' && n.path.path.includes(fileOrDir.name))) - ) - - if (isCurrentFile && eventType === 'change') { - let code = await window.electron.readFile(path, { encoding: 'utf-8' }) - code = normalizeLineEndings(code) - codeManager.updateCodeStateEditor(code) - await kclManager.executeCode() - } else if (isImportedInCurrentFile && eventType === 'change') { - await kclManager.executeAst() - } - fileSend({ type: 'Refresh' }) - }, - [fileOrDir.path] - ) - - const showNewTreeEntry = - newTreeEntry !== undefined && - fileOrDir.path === fileContext.selectedDirectory.path - - 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') { - void handleClick().catch(reportRejection) - } - } - - async function handleClick() { - setTreeSelection(fileOrDir) - - if (fileOrDir.children !== null) return // Don't open directories - - if (fileOrDir.name?.endsWith(FILE_EXT) === false && project?.path) { - toast.custom( - ToastInsert({ - onInsert: () => { - const relativeFilePath = fileOrDir.path.replace( - project.path + window.electron.sep, - '' - ) - commandBarActor.send({ - type: 'Find and select command', - data: { - name: 'Insert', - groupId: 'code', - argDefaultValues: { path: relativeFilePath }, - }, - }) - toast.dismiss(INSERT_FOREIGN_TOAST_ID) - }, - }), - { duration: 30000, id: INSERT_FOREIGN_TOAST_ID } - ) - } else { - // Let the lsp servers know we closed a file. - onFileClose(currentFile?.path || null, project?.path || null) - onFileOpen(fileOrDir.path, project?.path || null) - kclManager.switchedFiles = true - - // Open kcl files - navigate(`${PATHS.FILE}/${encodeURIComponent(fileOrDir.path)}`) - } - - onNavigateToFile?.() - } - - // The below handles both the "root" of all directories and all subs. It's - // why some code is duplicated. - return ( -
- {fileOrDir.children === null ? ( -
  • - {!isRenaming ? ( - - ) : ( - - )} -
  • - ) : ( - - {({ open }) => ( -
    - {!isRenaming ? ( - { - e.stopPropagation() - onClickDirectory(open, fileOrDir, parentDir) - }} - onKeyDown={(e) => e.key === 'Enter' && e.preventDefault()} - onKeyUp={handleKeyUp} - > - - {fileOrDir.name} - - ) : ( -
    - - -
    - )} - -
      { - e.stopPropagation() - onClickDirectory(open, fileOrDir, parentDir) - }} - > - {showNewTreeEntry && ( -
      - - - newTreeEntry === 'file' - ? onCreateFile(value) - : onCreateFolder(value) - } - /> -
      - )} - {sortFilesAndDirectories(fileOrDir.children || []).map( - (child) => ( - - ) - )} - {!showNewTreeEntry && fileOrDir.children?.length === 0 && ( -
      -
      No files
      -
      - )} -
    -
    -
    - )} -
    - )} - - {isConfirmingDelete && ( - - )} - setIsConfirmingDelete(true)} - onClone={() => onCloneFileOrFolder(fileOrDir.path)} - onOpenInNewWindow={() => onOpenInNewWindow(fileOrDir.path)} - /> -
    - ) -} - -interface FileTreeContextMenuProps { - itemRef: React.RefObject - onRename: () => void - onDelete: () => void - onClone: () => void - onOpenInNewWindow: () => void -} - -function FileTreeContextMenu({ - itemRef, - onRename, - onDelete, - onClone, - onOpenInNewWindow, -}: FileTreeContextMenuProps) { - const platform = usePlatform() - const metaKey = platform === 'macos' ? '⌘' : 'Ctrl' - - return ( - - Rename - , - - Delete - , - - Clone - , - - Open in new window - , - ]} - /> - ) -} - -export const FileTreeMenu = ({ - onCreateFile, - onCreateFolder, -}: { - onCreateFile: () => void - onCreateFolder: () => void -}) => { - useHotkeyWrapper(['mod + n'], onCreateFile) - useHotkeyWrapper(['mod + shift + n'], onCreateFolder) - - return ( - <> - - Create file - - - - Create folder - - - ) -} - -type TreeEntry = 'file' | 'folder' | undefined - -export const useFileTreeOperations = () => { - const { send } = useFileContext() - const { send: modelingSend } = useModelingContext() - - // As long as this is undefined, a new "file tree entry prompt" is not shown. - const [newTreeEntry, setNewTreeEntry] = useState(undefined) - - function createFile(args: { dryRun: boolean; name?: string }) { - if (args.dryRun) { - setNewTreeEntry('file') - return - } - - // Clear so that the entry prompt goes away. - setNewTreeEntry(undefined) - - if (!args.name) return - - send({ - type: 'Create file', - data: { name: args.name, makeDir: false, shouldSetToRename: false }, - }) - modelingSend({ type: 'Cancel' }) - } - - function createFolder(args: { dryRun: boolean; name?: string }) { - if (args.dryRun) { - setNewTreeEntry('folder') - return - } - - setNewTreeEntry(undefined) - - if (!args.name) return - - send({ - type: 'Create file', - data: { - name: args.name, - makeDir: true, - silent: true, - shouldSetToRename: false, - }, - }) - } - - function cloneFileOrDir(args: { path: string }) { - send({ - type: 'Create file', - data: { - name: '', - makeDir: false, - shouldSetToRename: false, - targetPathToClone: args.path, - }, - }) - } - - function openInNewWindow(args: { path: string }) { - send({ - type: 'Open file in new window', - data: { - name: args.path, - }, - }) - } - - return { - createFile, - createFolder, - cloneFileOrDir, - newTreeEntry, - openInNewWindow, - } -} - -export const FileTreeInner = ({ - onNavigateToFile, - onCreateFile, - onCreateFolder, - onCloneFileOrFolder, - onOpenInNewWindow, - newTreeEntry, -}: { - onCreateFile: (name: string) => void - onCreateFolder: (name: string) => void - onCloneFileOrFolder: (path: string) => void - onOpenInNewWindow: (path: string) => void - newTreeEntry: TreeEntry - onNavigateToFile?: () => void -}) => { - const loaderData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData - const { send: fileSend, context: fileContext } = useFileContext() - const { send: modelingSend } = useModelingContext() - const { errors } = useKclContext() - const runtimeErrors = kclErrorsByFilename(errors) - - const [treeSelection, setTreeSelection] = useState( - loaderData.file - ) - - const onNavigateToFile_ = () => { - // Reset modeling state when navigating to a new file - onNavigateToFile?.() - modelingSend({ type: 'Cancel' }) - } - - // Refresh the file tree when there are changes. - useFileSystemWatcher( - async (eventType, path) => { - // Our other watcher races with this watcher on the current file changes, - // so we need to stop this one from reacting at all, otherwise Bad Things - // Happen™. - const isCurrentFile = loaderData.file?.path === path - const hasChanged = eventType === 'change' - if (isCurrentFile && hasChanged) return - - // If it's a settings file we wrote to already from the app ignore it. - if (codeManager.writeCausedByAppCheckedInFileTreeFileSystemWatcher) { - codeManager.writeCausedByAppCheckedInFileTreeFileSystemWatcher = false - return - } - - fileSend({ type: 'Refresh' }) - }, - [loaderData?.project?.path, fileContext.selectedDirectory.path].filter( - (x: string | undefined) => x !== undefined - ) - ) - - const onTreeEntryInputSubmit = (value: string) => { - if (newTreeEntry === 'file') { - onCreateFile(value) - onNavigateToFile_() - } else { - onCreateFolder(value) - } - } - - const onClickDirectory = ( - open_: boolean, - fileOrDir: FileEntry, - parentDir: FileEntry | undefined - ) => { - // open true is closed... it's broken. Save me. I've inverted it here for - // sanity. - const open = !open_ - - const target = open ? fileOrDir : parentDir - - // We're at the root, can't select anything further - if (!target) return - - setTreeSelection(target) - fileSend({ - type: 'Set selected directory', - directory: target, - }) - } - - const showNewTreeEntry = - newTreeEntry !== undefined && - fileContext.selectedDirectory.path === loaderData.project?.path - - return ( -
    -
    -
      - {showNewTreeEntry && ( -
      - - -
      - )} - {sortFilesAndDirectories(fileContext.project?.children || []).map( - (fileOrDir) => ( - - ) - )} -
    -
    -
    - ) -} - -export const FileTreeRoot = () => { - const loaderData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData - const { project } = loaderData - - // project.path should never be empty here but I guess during initial loading - // it can be. - return ( -
    - {project?.name ?? ''} -
    - ) -} diff --git a/src/components/ModelingMachineProvider.tsx b/src/components/ModelingMachineProvider.tsx index 41a1a377d..62d9d9cb1 100644 --- a/src/components/ModelingMachineProvider.tsx +++ b/src/components/ModelingMachineProvider.tsx @@ -25,7 +25,6 @@ import { SEGMENT_BODIES_PLUS_PROFILE_START, getParentGroup, } from '@src/clientSideScene/sceneConstants' -import { useFileContext } from '@src/hooks/useFileContext' import { useMenuListener, useSketchModeMenuEnableDisable, @@ -65,7 +64,7 @@ import { import { exportMake } from '@src/lib/exportMake' import { exportSave } from '@src/lib/exportSave' import { isDesktop } from '@src/lib/isDesktop' -import type { FileEntry } from '@src/lib/project' +import type { FileEntry, Project } from '@src/lib/project' import type { WebContentSendPayload } from '@src/menu/channels' import { getPersistedContext, @@ -105,6 +104,7 @@ import { applyConstraintAbsDistance } from '@src/components/Toolbar/SetAbsDistan import type { SidebarType } from '@src/components/ModelingSidebar/ModelingPanes' import { useNetworkContext } from '@src/hooks/useNetworkContext' import { resetCameraPosition } from '@src/lib/resetCameraPosition' +import { useFolders } from '@src/machines/systemIO/hooks' export const ModelingMachineContext = createContext( {} as { @@ -127,8 +127,28 @@ export const ModelingMachineProvider = ({ app: { allowOrbitInSketchMode }, modeling: { defaultUnit, cameraProjection, cameraOrbit }, } = useSettings() - const { context } = useFileContext() - const { file } = useLoaderData() as IndexLoaderData + const loaderData = useLoaderData() as IndexLoaderData + const projects = useFolders() + const { project, file } = loaderData + const theProject = useRef(project) + useEffect(() => { + // Have no idea why the project loader data doesn't have the children from the ls on disk + // That means it is a different object or cached incorrectly? + if (!project || !file) { + return + } + + // You need to find the real project in the storage from the loader information since the loader Project is not hydrated + const foundYourProject = projects.find((p) => { + return p.name === project.name + }) + + if (!foundYourProject) { + return + } + theProject.current = foundYourProject + }, [projects, loaderData, file]) + const token = useToken() const streamRef = useRef(null) const persistedContext = useMemo(() => getPersistedContext(), []) @@ -1176,9 +1196,9 @@ export const ModelingMachineProvider = ({ } ) let basePath = '' - if (isDesktop() && context?.project?.children) { + if (isDesktop() && theProject?.current?.children) { // Use the entire project directory as the basePath for prompt to edit, do not use relative subdir paths - basePath = context?.project?.path + basePath = theProject?.current?.path const filePromises: Promise[] = [] let uploadSize = 0 const recursivelyPushFilePromises = (files: FileEntry[]) => { @@ -1234,7 +1254,7 @@ export const ModelingMachineProvider = ({ filePromises.push(filePromise) } } - recursivelyPushFilePromises(context?.project?.children) + recursivelyPushFilePromises(theProject?.current?.children) projectFiles = (await Promise.all(filePromises)).filter( isNonNullable ) @@ -1258,7 +1278,7 @@ export const ModelingMachineProvider = ({ selections: input.selection, token, artifactGraph: kclManager.artifactGraph, - projectName: context.project.name, + projectName: theProject?.current?.name || '', filePath, }) }), diff --git a/src/hooks/useFileContext.ts b/src/hooks/useFileContext.ts deleted file mode 100644 index 4e66a60fd..000000000 --- a/src/hooks/useFileContext.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { useContext } from 'react' - -import { FileContext } from '@src/components/FileMachineProvider' - -export const useFileContext = () => { - return useContext(FileContext) -}