chore: deleted the old file tree:
This commit is contained in:
@ -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)
|
||||
|
@ -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<T extends AnyStateMachine> = {
|
||||
state: StateFrom<T>
|
||||
context: ContextFrom<T>
|
||||
send: Prop<Actor<T>, 'send'>
|
||||
}
|
||||
|
||||
export const FileContext = createContext(
|
||||
{} as MachineContext<typeof fileMachine>
|
||||
)
|
||||
|
||||
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 (
|
||||
<FileContext.Provider
|
||||
value={{
|
||||
send,
|
||||
state,
|
||||
context: state.context, // just a convenience, can remove if we need to save on memory
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</FileContext.Provider>
|
||||
)
|
||||
return <div>{children}</div>
|
||||
}
|
||||
|
||||
export default FileMachineProvider
|
||||
|
@ -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;
|
||||
}
|
@ -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<HTMLInputElement>) => {
|
||||
if (e.key !== 'Enter') return
|
||||
props.onSubmit(value)
|
||||
}
|
||||
|
||||
return (
|
||||
<label>
|
||||
<span className="sr-only">Entry input</span>
|
||||
<input
|
||||
data-testid="tree-input-field"
|
||||
type="text"
|
||||
autoFocus
|
||||
autoCapitalize="off"
|
||||
autoCorrect="off"
|
||||
className="w-full py-1 bg-transparent text-chalkboard-100 placeholder:text-chalkboard-70 dark:text-chalkboard-10 dark:placeholder:text-chalkboard-50 focus:outline-none focus:ring-0"
|
||||
onBlur={() => props.onSubmit(value)}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
onKeyPress={onKeyPress}
|
||||
style={{ paddingInlineStart: getIndentationCSS(props.level) }}
|
||||
value={value}
|
||||
/>
|
||||
</label>
|
||||
)
|
||||
}
|
||||
|
||||
function RenameForm({
|
||||
fileOrDir,
|
||||
parentDir,
|
||||
onSubmit,
|
||||
level = 0,
|
||||
}: {
|
||||
fileOrDir: FileEntry
|
||||
parentDir: FileEntry | undefined
|
||||
onSubmit: () => void
|
||||
level?: number
|
||||
}) {
|
||||
const { send } = useFileContext()
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const fileContext = useFileContext()
|
||||
|
||||
function handleRenameSubmit(e: React.KeyboardEvent<HTMLElement>) {
|
||||
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<HTMLInputElement>) {
|
||||
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 (
|
||||
<form onKeyUp={handleRenameSubmit}>
|
||||
<label>
|
||||
<span className="sr-only">Rename file</span>
|
||||
<input
|
||||
data-testid="file-rename-field"
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
autoFocus
|
||||
autoCapitalize="off"
|
||||
autoCorrect="off"
|
||||
placeholder={fileOrDir.name}
|
||||
className="w-full py-1 bg-transparent text-chalkboard-100 placeholder:text-chalkboard-70 dark:text-chalkboard-10 dark:placeholder:text-chalkboard-50 focus:outline-none focus:ring-0"
|
||||
onKeyDown={handleKeyDown}
|
||||
onBlur={onSubmit}
|
||||
style={{ paddingInlineStart: getIndentationCSS(level) }}
|
||||
/>
|
||||
</label>
|
||||
<button className="sr-only" type="submit">
|
||||
Submit
|
||||
</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
function DeleteFileTreeItemDialog({
|
||||
fileOrDir,
|
||||
setIsOpen,
|
||||
}: {
|
||||
fileOrDir: FileEntry
|
||||
setIsOpen: Dispatch<React.SetStateAction<boolean>>
|
||||
}) {
|
||||
const { send } = useFileContext()
|
||||
return (
|
||||
<DeleteConfirmationDialog
|
||||
title={`Delete ${fileOrDir.children !== null ? 'folder' : 'file'}`}
|
||||
onDismiss={() => setIsOpen(false)}
|
||||
onConfirm={() => {
|
||||
send({ type: 'Delete file', data: fileOrDir })
|
||||
setIsOpen(false)
|
||||
}}
|
||||
>
|
||||
<p className="my-4">
|
||||
This will permanently delete "{fileOrDir.name || 'this file'}"
|
||||
{fileOrDir.children !== null ? ' and all of its contents. ' : '. '}
|
||||
</p>
|
||||
<p className="my-4">
|
||||
Are you sure you want to delete "{fileOrDir.name || 'this file'}
|
||||
"? This action cannot be undone.
|
||||
</p>
|
||||
</DeleteConfirmationDialog>
|
||||
)
|
||||
}
|
||||
|
||||
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<React.SetStateAction<FileEntry | undefined>>
|
||||
runtimeErrors: Map<string, KCLError[]>
|
||||
}) => {
|
||||
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<HTMLButtonElement>) {
|
||||
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 (
|
||||
<div className="contents" data-testid="file-tree-item" ref={itemRef}>
|
||||
{fileOrDir.children === null ? (
|
||||
<li
|
||||
className={
|
||||
'group m-0 p-0 border-solid border-0 hover:bg-primary/5 focus-within:bg-primary/5 dark:hover:bg-primary/20 dark:focus-within:bg-primary/20 ' +
|
||||
(isFileOrDirHighlighted || isCurrentFile
|
||||
? '!bg-primary/10 !text-primary dark:!bg-primary/20 dark:!text-inherit'
|
||||
: '')
|
||||
}
|
||||
>
|
||||
{!isRenaming ? (
|
||||
<button
|
||||
className="relative flex gap-1 items-center py-0.5 rounded-none border-none p-0 m-0 text-sm w-full hover:!bg-transparent text-left !text-inherit"
|
||||
style={{ paddingInlineStart: getIndentationCSS(level) }}
|
||||
onClick={(e) => {
|
||||
e.currentTarget.focus()
|
||||
void handleClick().catch(reportRejection)
|
||||
}}
|
||||
onKeyUp={handleKeyUp}
|
||||
>
|
||||
{hasRuntimeError && (
|
||||
<p
|
||||
className={
|
||||
'absolute m-0 p-0 bottom-3 left-6 w-3 h-3 flex items-center justify-center text-[9px] font-semibold text-white bg-primary hue-rotate-90 rounded-full border border-chalkboard-10 dark:border-chalkboard-80 z-50 hover:cursor-pointer hover:scale-[2] transition-transform duration-200'
|
||||
}
|
||||
title={`Click to view notifications`}
|
||||
>
|
||||
<span>x</span>
|
||||
</p>
|
||||
)}
|
||||
<CustomIcon
|
||||
name={fileOrDir.name?.endsWith(FILE_EXT) ? 'kcl' : 'file'}
|
||||
className="inline-block w-3 text-current"
|
||||
/>
|
||||
<span className="pl-1">{fileOrDir.name}</span>
|
||||
</button>
|
||||
) : (
|
||||
<RenameForm
|
||||
fileOrDir={fileOrDir}
|
||||
parentDir={parentDir}
|
||||
onSubmit={removeCurrentItemFromRenaming}
|
||||
level={level}
|
||||
/>
|
||||
)}
|
||||
</li>
|
||||
) : (
|
||||
<Disclosure defaultOpen={currentFile?.path.includes(fileOrDir.path)}>
|
||||
{({ open }) => (
|
||||
<div className="group">
|
||||
{!isRenaming ? (
|
||||
<Disclosure.Button
|
||||
className={
|
||||
' group border-none text-sm rounded-none p-0 m-0 flex items-center justify-start w-full py-0.5 hover:text-primary hover:bg-primary/5 dark:hover:text-inherit dark:hover:bg-primary/10' +
|
||||
(isFileOrDirHighlighted ? ' ui-open:bg-primary/10' : '')
|
||||
}
|
||||
style={{ paddingInlineStart: getIndentationCSS(level) }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onClickDirectory(open, fileOrDir, parentDir)
|
||||
}}
|
||||
onKeyDown={(e) => e.key === 'Enter' && e.preventDefault()}
|
||||
onKeyUp={handleKeyUp}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faChevronRight}
|
||||
className={
|
||||
'inline-block mr-2 m-0 p-0 w-2 h-2 ' +
|
||||
(open ? 'transform rotate-90' : '')
|
||||
}
|
||||
/>
|
||||
{fileOrDir.name}
|
||||
</Disclosure.Button>
|
||||
) : (
|
||||
<div
|
||||
className="flex items-center"
|
||||
style={{ paddingInlineStart: getIndentationCSS(level) }}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faChevronRight}
|
||||
className={
|
||||
'inline-block mr-2 m-0 p-0 w-2 h-2 ' +
|
||||
(open ? 'transform rotate-90' : '')
|
||||
}
|
||||
/>
|
||||
<RenameForm
|
||||
fileOrDir={fileOrDir}
|
||||
parentDir={parentDir}
|
||||
onSubmit={removeCurrentItemFromRenaming}
|
||||
level={-1}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Disclosure.Panel
|
||||
className={styles.folder}
|
||||
style={
|
||||
{
|
||||
'--indent-line-left': getIndentationCSS(level),
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<ul
|
||||
className="m-0 p-0"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onClickDirectory(open, fileOrDir, parentDir)
|
||||
}}
|
||||
>
|
||||
{showNewTreeEntry && (
|
||||
<div
|
||||
className="flex items-center"
|
||||
style={{
|
||||
paddingInlineStart: getIndentationCSS(level + 1),
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faPencil}
|
||||
className="inline-block mr-2 m-0 p-0 w-2 h-2"
|
||||
/>
|
||||
<TreeEntryInput
|
||||
level={-1}
|
||||
onSubmit={(value: string) =>
|
||||
newTreeEntry === 'file'
|
||||
? onCreateFile(value)
|
||||
: onCreateFolder(value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{sortFilesAndDirectories(fileOrDir.children || []).map(
|
||||
(child) => (
|
||||
<FileTreeItem
|
||||
parentDir={fileOrDir}
|
||||
fileOrDir={child}
|
||||
project={project}
|
||||
currentFile={currentFile}
|
||||
onCreateFile={onCreateFile}
|
||||
onCreateFolder={onCreateFolder}
|
||||
onCloneFileOrFolder={onCloneFileOrFolder}
|
||||
onOpenInNewWindow={onOpenInNewWindow}
|
||||
newTreeEntry={newTreeEntry}
|
||||
onClickDirectory={onClickDirectory}
|
||||
onNavigateToFile={onNavigateToFile}
|
||||
level={level + 1}
|
||||
key={level + '-' + child.path}
|
||||
treeSelection={treeSelection}
|
||||
setTreeSelection={setTreeSelection}
|
||||
runtimeErrors={runtimeErrors}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
{!showNewTreeEntry && fileOrDir.children?.length === 0 && (
|
||||
<div
|
||||
className="flex items-center text-chalkboard-50"
|
||||
style={{
|
||||
paddingInlineStart: getIndentationCSS(level + 1),
|
||||
}}
|
||||
>
|
||||
<div>No files</div>
|
||||
</div>
|
||||
)}
|
||||
</ul>
|
||||
</Disclosure.Panel>
|
||||
</div>
|
||||
)}
|
||||
</Disclosure>
|
||||
)}
|
||||
|
||||
{isConfirmingDelete && (
|
||||
<DeleteFileTreeItemDialog
|
||||
fileOrDir={fileOrDir}
|
||||
setIsOpen={setIsConfirmingDelete}
|
||||
/>
|
||||
)}
|
||||
<FileTreeContextMenu
|
||||
itemRef={itemRef}
|
||||
onRename={addCurrentItemToRenaming}
|
||||
onDelete={() => setIsConfirmingDelete(true)}
|
||||
onClone={() => onCloneFileOrFolder(fileOrDir.path)}
|
||||
onOpenInNewWindow={() => onOpenInNewWindow(fileOrDir.path)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface FileTreeContextMenuProps {
|
||||
itemRef: React.RefObject<HTMLElement>
|
||||
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 (
|
||||
<ContextMenu
|
||||
menuTargetElement={itemRef}
|
||||
items={[
|
||||
<ContextMenuItem
|
||||
data-testid="context-menu-rename"
|
||||
onClick={onRename}
|
||||
hotkey="Enter"
|
||||
>
|
||||
Rename
|
||||
</ContextMenuItem>,
|
||||
<ContextMenuItem
|
||||
data-testid="context-menu-delete"
|
||||
onClick={onDelete}
|
||||
hotkey={metaKey + ' + Del'}
|
||||
>
|
||||
Delete
|
||||
</ContextMenuItem>,
|
||||
<ContextMenuItem
|
||||
data-testid="context-menu-clone"
|
||||
onClick={onClone}
|
||||
hotkey=""
|
||||
>
|
||||
Clone
|
||||
</ContextMenuItem>,
|
||||
<ContextMenuItem
|
||||
data-testid="context-menu-open-in-new-window"
|
||||
onClick={onOpenInNewWindow}
|
||||
>
|
||||
Open in new window
|
||||
</ContextMenuItem>,
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const FileTreeMenu = ({
|
||||
onCreateFile,
|
||||
onCreateFolder,
|
||||
}: {
|
||||
onCreateFile: () => void
|
||||
onCreateFolder: () => void
|
||||
}) => {
|
||||
useHotkeyWrapper(['mod + n'], onCreateFile)
|
||||
useHotkeyWrapper(['mod + shift + n'], onCreateFolder)
|
||||
|
||||
return (
|
||||
<>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
data-testid="create-file-button"
|
||||
iconStart={{
|
||||
icon: 'filePlus',
|
||||
iconClassName: '!text-current',
|
||||
bgClassName: 'bg-transparent',
|
||||
}}
|
||||
className="!p-0 !bg-transparent hover:text-primary border-transparent hover:border-primary !outline-none"
|
||||
onClick={onCreateFile}
|
||||
>
|
||||
<Tooltip position="bottom-right">Create file</Tooltip>
|
||||
</ActionButton>
|
||||
|
||||
<ActionButton
|
||||
Element="button"
|
||||
data-testid="create-folder-button"
|
||||
iconStart={{
|
||||
icon: 'folderPlus',
|
||||
iconClassName: '!text-current',
|
||||
bgClassName: 'bg-transparent',
|
||||
}}
|
||||
className="!p-0 !bg-transparent hover:text-primary border-transparent hover:border-primary !outline-none"
|
||||
onClick={onCreateFolder}
|
||||
>
|
||||
<Tooltip position="bottom-right">Create folder</Tooltip>
|
||||
</ActionButton>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
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<TreeEntry>(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<FileEntry | undefined>(
|
||||
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 (
|
||||
<div className="relative">
|
||||
<div
|
||||
className="overflow-auto pb-12 absolute inset-0"
|
||||
data-testid="file-pane-scroll-container"
|
||||
>
|
||||
<ul className="m-0 p-0 text-sm">
|
||||
{showNewTreeEntry && (
|
||||
<div
|
||||
className="flex items-center"
|
||||
style={{ paddingInlineStart: getIndentationCSS(0) }}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faPencil}
|
||||
className="inline-block mr-2 m-0 p-0 w-2 h-2"
|
||||
/>
|
||||
<TreeEntryInput level={-1} onSubmit={onTreeEntryInputSubmit} />
|
||||
</div>
|
||||
)}
|
||||
{sortFilesAndDirectories(fileContext.project?.children || []).map(
|
||||
(fileOrDir) => (
|
||||
<FileTreeItem
|
||||
parentDir={fileContext.project}
|
||||
project={fileContext.project}
|
||||
currentFile={loaderData?.file}
|
||||
fileOrDir={fileOrDir}
|
||||
onCreateFile={onCreateFile}
|
||||
onCreateFolder={onCreateFolder}
|
||||
onCloneFileOrFolder={onCloneFileOrFolder}
|
||||
onOpenInNewWindow={onOpenInNewWindow}
|
||||
newTreeEntry={newTreeEntry}
|
||||
onClickDirectory={onClickDirectory}
|
||||
onNavigateToFile={onNavigateToFile_}
|
||||
key={fileOrDir.path}
|
||||
treeSelection={treeSelection}
|
||||
setTreeSelection={setTreeSelection}
|
||||
runtimeErrors={runtimeErrors}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<div
|
||||
className="max-w-xs text-ellipsis overflow-hidden cursor-pointer"
|
||||
title={project?.path ?? ''}
|
||||
>
|
||||
{project?.name ?? ''}
|
||||
</div>
|
||||
)
|
||||
}
|
@ -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 | undefined>(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<HTMLDivElement>(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<FileMeta | null>[] = []
|
||||
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,
|
||||
})
|
||||
}),
|
||||
|
@ -1,7 +0,0 @@
|
||||
import { useContext } from 'react'
|
||||
|
||||
import { FileContext } from '@src/components/FileMachineProvider'
|
||||
|
||||
export const useFileContext = () => {
|
||||
return useContext(FileContext)
|
||||
}
|
Reference in New Issue
Block a user