feat: better composer etc
This commit is contained in:
@@ -24,7 +24,12 @@ interface FileBrowserPaneProps {
|
||||
export function FileBrowserPane({ onActivateFile, onChangeCwd }: FileBrowserPaneProps) {
|
||||
const currentCwd = useStore($currentCwd).trim()
|
||||
const hasCwd = currentCwd.length > 0
|
||||
const cwdName = hasCwd ? (currentCwd.split(/[\\/]+/).filter(Boolean).pop() ?? currentCwd) : 'No folder selected'
|
||||
const cwdName = hasCwd
|
||||
? (currentCwd
|
||||
.split(/[\\/]+/)
|
||||
.filter(Boolean)
|
||||
.pop() ?? currentCwd)
|
||||
: 'No folder selected'
|
||||
const { data, loadChildren, openState, refreshRoot, rootError, rootLoading, setNodeOpen } = useProjectTree(currentCwd)
|
||||
|
||||
const chooseFolder = async () => {
|
||||
|
||||
@@ -17,7 +17,9 @@ function decodeDataUrl(dataUrl: string) {
|
||||
const data = match?.[1] || ''
|
||||
const isBase64 = dataUrl.slice(0, dataUrl.indexOf(',')).includes(';base64')
|
||||
|
||||
if (!isBase64) {return decodeURIComponent(data)}
|
||||
if (!isBase64) {
|
||||
return decodeURIComponent(data)
|
||||
}
|
||||
|
||||
const bytes = Uint8Array.from(atob(data), ch => ch.charCodeAt(0))
|
||||
|
||||
@@ -33,7 +35,9 @@ function relativeTo(root: string, child: string) {
|
||||
const r = clean(root)
|
||||
const c = clean(child)
|
||||
|
||||
if (c === r) {return ''}
|
||||
if (c === r) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return c.startsWith(`${r}/`) ? c.slice(r.length + 1) : null
|
||||
}
|
||||
@@ -43,7 +47,9 @@ function ancestorDirs(root: string, dir: string) {
|
||||
const r = clean(root)
|
||||
const rel = relativeTo(r, dir)
|
||||
|
||||
if (rel === null || rel === '') {return [r]}
|
||||
if (rel === null || rel === '') {
|
||||
return [r]
|
||||
}
|
||||
|
||||
const dirs = [r]
|
||||
let current = r
|
||||
@@ -57,7 +63,9 @@ function ancestorDirs(root: string, dir: string) {
|
||||
}
|
||||
|
||||
async function gitRootFor(start: string) {
|
||||
if (!window.hermesDesktop?.gitRoot) {return null}
|
||||
if (!window.hermesDesktop?.gitRoot) {
|
||||
return null
|
||||
}
|
||||
|
||||
const key = clean(start)
|
||||
let cached = gitRootCache.get(key)
|
||||
@@ -72,12 +80,16 @@ async function gitRootFor(start: string) {
|
||||
|
||||
/** Read .gitignore at `dir` if it actually exists — never probe missing files. */
|
||||
async function readGitignore(dir: string): Promise<GitignoreRule | null> {
|
||||
if (!window.hermesDesktop?.readDir || !window.hermesDesktop.readFileDataUrl) {return null}
|
||||
if (!window.hermesDesktop?.readDir || !window.hermesDesktop.readFileDataUrl) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const listing = await window.hermesDesktop.readDir(dir)
|
||||
|
||||
if (!listing.entries.some(e => e.name === '.gitignore' && !e.isDirectory)) {return null}
|
||||
if (!listing.entries.some(e => e.name === '.gitignore' && !e.isDirectory)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const text = decodeDataUrl(await window.hermesDesktop.readFileDataUrl(`${dir}/.gitignore`))
|
||||
|
||||
@@ -103,7 +115,9 @@ function ignoredBy(rules: GitignoreRule[], entry: HermesReadDirEntry) {
|
||||
return rules.some(rule => {
|
||||
const rel = relativeTo(rule.base, entry.path)
|
||||
|
||||
if (rel === null || rel === '') {return false}
|
||||
if (rel === null || rel === '') {
|
||||
return false
|
||||
}
|
||||
|
||||
return rule.ig.ignores(entry.isDirectory ? `${rel}/` : rel)
|
||||
})
|
||||
@@ -112,17 +126,21 @@ function ignoredBy(rules: GitignoreRule[], entry: HermesReadDirEntry) {
|
||||
async function filterIgnored(entries: HermesReadDirEntry[], rootPath: string, dirPath: string) {
|
||||
const root = await gitRootFor(rootPath)
|
||||
|
||||
if (!root) {return entries}
|
||||
if (!root) {
|
||||
return entries
|
||||
}
|
||||
|
||||
const rules = (await Promise.all(ancestorDirs(root, dirPath).map(gitignoreFor))).filter(
|
||||
(r): r is GitignoreRule => Boolean(r)
|
||||
const rules = (await Promise.all(ancestorDirs(root, dirPath).map(gitignoreFor))).filter((r): r is GitignoreRule =>
|
||||
Boolean(r)
|
||||
)
|
||||
|
||||
return rules.length > 0 ? entries.filter(entry => !ignoredBy(rules, entry)) : entries
|
||||
}
|
||||
|
||||
export async function readProjectDir(dirPath: string, rootPath = dirPath): Promise<HermesReadDirResult> {
|
||||
if (!window.hermesDesktop) {return { entries: [], error: 'no-bridge' }}
|
||||
if (!window.hermesDesktop) {
|
||||
return { entries: [], error: 'no-bridge' }
|
||||
}
|
||||
|
||||
const result = await window.hermesDesktop.readDir(dirPath)
|
||||
|
||||
|
||||
@@ -33,7 +33,9 @@ export function ProjectTree({
|
||||
useEffect(() => {
|
||||
const el = containerRef.current
|
||||
|
||||
if (!el || typeof ResizeObserver === 'undefined') {return}
|
||||
if (!el || typeof ResizeObserver === 'undefined') {
|
||||
return
|
||||
}
|
||||
|
||||
const observer = new ResizeObserver(([entry]) => {
|
||||
const { height, width } = entry.contentRect
|
||||
@@ -49,7 +51,9 @@ export function ProjectTree({
|
||||
(id: string) => {
|
||||
const node = treeRef.current?.get(id)
|
||||
|
||||
if (!node) {return}
|
||||
if (!node) {
|
||||
return
|
||||
}
|
||||
|
||||
onNodeOpenChange(id, node.isOpen)
|
||||
|
||||
@@ -121,7 +125,9 @@ function ProjectTreeRow({
|
||||
onClick={event => {
|
||||
event.stopPropagation()
|
||||
|
||||
if (isPlaceholder) {return}
|
||||
if (isPlaceholder) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isFolder) {
|
||||
node.toggle()
|
||||
|
||||
@@ -25,10 +25,14 @@ function makeNode(path: string, name: string, isDirectory: boolean): TreeNode {
|
||||
}
|
||||
|
||||
function patchNode(nodes: TreeNode[] | undefined | null, id: string, patch: (n: TreeNode) => TreeNode): TreeNode[] {
|
||||
if (!nodes) {return []}
|
||||
if (!nodes) {
|
||||
return []
|
||||
}
|
||||
|
||||
return nodes.map(n => {
|
||||
if (n.id === id) {return patch(n)}
|
||||
if (n.id === id) {
|
||||
return patch(n)
|
||||
}
|
||||
|
||||
if (n.children && n.children.length > 0) {
|
||||
return { ...n, children: patchNode(n.children, id, patch) }
|
||||
@@ -170,37 +174,46 @@ export function useProjectTree(cwd: string): UseProjectTreeResult {
|
||||
[cwd]
|
||||
)
|
||||
|
||||
const loadChildren = useCallback(async (id: string) => {
|
||||
if (!cwd || inflight.has(id)) {return}
|
||||
inflight.add(id)
|
||||
|
||||
setProjectTree(current => {
|
||||
if (current.cwd !== cwd) {return current}
|
||||
|
||||
return {
|
||||
...current,
|
||||
data: patchNode(current.data, id, n => ({ ...n, loading: true, children: [placeholderChild(n.id)] }))
|
||||
const loadChildren = useCallback(
|
||||
async (id: string) => {
|
||||
if (!cwd || inflight.has(id)) {
|
||||
return
|
||||
}
|
||||
})
|
||||
inflight.add(id)
|
||||
|
||||
const { entries, error } = await readProjectDir(id, cwd)
|
||||
setProjectTree(current => {
|
||||
if (current.cwd !== cwd) {
|
||||
return current
|
||||
}
|
||||
|
||||
inflight.delete(id)
|
||||
return {
|
||||
...current,
|
||||
data: patchNode(current.data, id, n => ({ ...n, loading: true, children: [placeholderChild(n.id)] }))
|
||||
}
|
||||
})
|
||||
|
||||
setProjectTree(current => {
|
||||
if (current.cwd !== cwd) {return current}
|
||||
const { entries, error } = await readProjectDir(id, cwd)
|
||||
|
||||
return {
|
||||
...current,
|
||||
data: patchNode(current.data, id, n => ({
|
||||
...n,
|
||||
loading: false,
|
||||
error: error || undefined,
|
||||
children: error ? [] : entries.map(e => makeNode(e.path, e.name, e.isDirectory))
|
||||
}))
|
||||
}
|
||||
})
|
||||
}, [cwd])
|
||||
inflight.delete(id)
|
||||
|
||||
setProjectTree(current => {
|
||||
if (current.cwd !== cwd) {
|
||||
return current
|
||||
}
|
||||
|
||||
return {
|
||||
...current,
|
||||
data: patchNode(current.data, id, n => ({
|
||||
...n,
|
||||
loading: false,
|
||||
error: error || undefined,
|
||||
children: error ? [] : entries.map(e => makeNode(e.path, e.name, e.isDirectory))
|
||||
}))
|
||||
}
|
||||
})
|
||||
},
|
||||
[cwd]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
void loadRoot(cwd)
|
||||
@@ -216,6 +229,16 @@ export function useProjectTree(cwd: string): UseProjectTreeResult {
|
||||
rootLoading: state.cwd === cwd ? state.rootLoading : false,
|
||||
setNodeOpen
|
||||
}),
|
||||
[cwd, loadChildren, refreshRoot, setNodeOpen, state.cwd, state.data, state.openState, state.rootError, state.rootLoading]
|
||||
[
|
||||
cwd,
|
||||
loadChildren,
|
||||
refreshRoot,
|
||||
setNodeOpen,
|
||||
state.cwd,
|
||||
state.data,
|
||||
state.openState,
|
||||
state.rootError,
|
||||
state.rootLoading
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user