mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-04-05 00:23:10 +00:00
171 lines
4.1 KiB
TypeScript
171 lines
4.1 KiB
TypeScript
import { createStore, produce, reconcile } from "solid-js/store"
|
|
import type { FileNode } from "@opencode-ai/sdk/v2"
|
|
|
|
type DirectoryState = {
|
|
expanded: boolean
|
|
loaded?: boolean
|
|
loading?: boolean
|
|
error?: string
|
|
children?: string[]
|
|
}
|
|
|
|
type TreeStoreOptions = {
|
|
scope: () => string
|
|
normalizeDir: (input: string) => string
|
|
list: (input: string) => Promise<FileNode[]>
|
|
onError: (message: string) => void
|
|
}
|
|
|
|
export function createFileTreeStore(options: TreeStoreOptions) {
|
|
const [tree, setTree] = createStore<{
|
|
node: Record<string, FileNode>
|
|
dir: Record<string, DirectoryState>
|
|
}>({
|
|
node: {},
|
|
dir: { "": { expanded: true } },
|
|
})
|
|
|
|
const inflight = new Map<string, Promise<void>>()
|
|
|
|
const reset = () => {
|
|
inflight.clear()
|
|
setTree("node", reconcile({}))
|
|
setTree("dir", reconcile({}))
|
|
setTree("dir", "", { expanded: true })
|
|
}
|
|
|
|
const ensureDir = (path: string) => {
|
|
if (tree.dir[path]) return
|
|
setTree("dir", path, { expanded: false })
|
|
}
|
|
|
|
const listDir = (input: string, opts?: { force?: boolean }) => {
|
|
const dir = options.normalizeDir(input)
|
|
ensureDir(dir)
|
|
|
|
const current = tree.dir[dir]
|
|
if (!opts?.force && current?.loaded) return Promise.resolve()
|
|
|
|
const pending = inflight.get(dir)
|
|
if (pending) return pending
|
|
|
|
setTree(
|
|
"dir",
|
|
dir,
|
|
produce((draft) => {
|
|
draft.loading = true
|
|
draft.error = undefined
|
|
}),
|
|
)
|
|
|
|
const directory = options.scope()
|
|
|
|
const promise = options
|
|
.list(dir)
|
|
.then((nodes) => {
|
|
if (options.scope() !== directory) return
|
|
const prevChildren = tree.dir[dir]?.children ?? []
|
|
const nextChildren = nodes.map((node) => node.path)
|
|
const nextSet = new Set(nextChildren)
|
|
|
|
setTree(
|
|
"node",
|
|
produce((draft) => {
|
|
const removedDirs: string[] = []
|
|
|
|
for (const child of prevChildren) {
|
|
if (nextSet.has(child)) continue
|
|
const existing = draft[child]
|
|
if (existing?.type === "directory") removedDirs.push(child)
|
|
delete draft[child]
|
|
}
|
|
|
|
if (removedDirs.length > 0) {
|
|
const keys = Object.keys(draft)
|
|
for (const key of keys) {
|
|
for (const removed of removedDirs) {
|
|
if (!key.startsWith(removed + "/")) continue
|
|
delete draft[key]
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const node of nodes) {
|
|
draft[node.path] = node
|
|
}
|
|
}),
|
|
)
|
|
|
|
setTree(
|
|
"dir",
|
|
dir,
|
|
produce((draft) => {
|
|
draft.loaded = true
|
|
draft.loading = false
|
|
draft.children = nextChildren
|
|
}),
|
|
)
|
|
})
|
|
.catch((e) => {
|
|
if (options.scope() !== directory) return
|
|
setTree(
|
|
"dir",
|
|
dir,
|
|
produce((draft) => {
|
|
draft.loading = false
|
|
draft.error = e.message
|
|
}),
|
|
)
|
|
options.onError(e.message)
|
|
})
|
|
.finally(() => {
|
|
inflight.delete(dir)
|
|
})
|
|
|
|
inflight.set(dir, promise)
|
|
return promise
|
|
}
|
|
|
|
const expandDir = (input: string) => {
|
|
const dir = options.normalizeDir(input)
|
|
ensureDir(dir)
|
|
setTree("dir", dir, "expanded", true)
|
|
void listDir(dir)
|
|
}
|
|
|
|
const collapseDir = (input: string) => {
|
|
const dir = options.normalizeDir(input)
|
|
ensureDir(dir)
|
|
setTree("dir", dir, "expanded", false)
|
|
}
|
|
|
|
const dirState = (input: string) => {
|
|
const dir = options.normalizeDir(input)
|
|
return tree.dir[dir]
|
|
}
|
|
|
|
const children = (input: string) => {
|
|
const dir = options.normalizeDir(input)
|
|
const ids = tree.dir[dir]?.children
|
|
if (!ids) return []
|
|
const out: FileNode[] = []
|
|
for (const id of ids) {
|
|
const node = tree.node[id]
|
|
if (node) out.push(node)
|
|
}
|
|
return out
|
|
}
|
|
|
|
return {
|
|
listDir,
|
|
expandDir,
|
|
collapseDir,
|
|
dirState,
|
|
children,
|
|
node: (path: string) => tree.node[path],
|
|
isLoaded: (path: string) => Boolean(tree.dir[path]?.loaded),
|
|
reset,
|
|
}
|
|
}
|