import { useFile } from "@/context/file" import { FileIcon } from "@opencode-ai/ui/file-icon" import "@opencode-ai/ui/v2/file-tree-v2.css" import { createEffect, createMemo, createSignal, For, Show, splitProps, type ComponentProps, type ParentProps, } from "solid-js" import { Dynamic } from "solid-js/web" import type { FileNode } from "@opencode-ai/sdk/v2" import { Icon } from "@opencode-ai/ui/v2/icon" import { pathToFileUrl, withFileDragImage, type Kind } from "@/components/file-tree" import { createVirtualizer, defaultRangeExtractor } from "@tanstack/solid-virtual" import { buildFileTreeV2Model, flattenFileTreeV2, normalizeFileTreeV2Path } from "@/components/file-tree-v2-model" import { virtualScrollElement } from "@/components/virtual-scroll-element" export type { Kind } from "@/components/file-tree" const INDENT_STEP = 16 function rowPaddingLeft(level: number, type: FileNode["type"]) { if (type === "directory") return 8 + level * INDENT_STEP if (level === 0) return 8 return 8 + level * INDENT_STEP - INDENT_STEP } function guideLineLeft(level: number) { return rowPaddingLeft(level, "directory") + 8 } export const kindLabel = (kind: Kind) => { if (kind === "add") return "A" if (kind === "del") return "D" return "" } export const kindChange = (kind: Kind) => { if (kind === "add") return "added" if (kind === "del") return "deleted" return "modified" } const FileTreeNodeV2 = ( p: ParentProps & ComponentProps<"div"> & ComponentProps<"button"> & { node: FileNode level: number active?: string draggable: boolean kinds?: ReadonlyMap as?: "div" | "button" }, ) => { const [local, rest] = splitProps(p, [ "node", "level", "active", "draggable", "kinds", "as", "children", "class", "classList", ]) const kind = () => local.kinds?.get(local.node.path) return ( { if (!local.draggable) return event.dataTransfer?.setData("text/plain", `file:${local.node.path}`) event.dataTransfer?.setData("text/uri-list", pathToFileUrl(local.node.path)) if (event.dataTransfer) event.dataTransfer.effectAllowed = "copy" withFileDragImage(event) }} {...rest} > {local.children} {local.node.name} {(() => { const value = kind() if (!value || local.node.type !== "file") return null return ( {kindLabel(value)} ) })()} ) } function GuideLines(props: { level: number }) { return ( {(_, index) => (
)} ) } export default function FileTreeV2(props: { active?: string allowed?: readonly string[] kinds?: ReadonlyMap draggable?: boolean onFileClick?: (file: FileNode) => void }) { const file = useFile() const draggable = () => props.draggable ?? true const active = () => normalizeFileTreeV2Path(props.active ?? "") const model = createMemo(() => buildFileTreeV2Model(props.allowed ?? [])) const rows = createMemo(() => flattenFileTreeV2(model(), (path) => file.tree.state(path)?.expanded ?? true)) const [root, setRoot] = createSignal() const [focused, setFocused] = createSignal() const virtualizer = createVirtualizer({ get count() { return rows().length }, getScrollElement: () => virtualScrollElement(root()), initialRect: { width: 0, height: 600 }, estimateSize: () => 28, gap: 2, overscan: 10, get getItemKey() { const current = rows() return (index: number) => current[index]?.node.path ?? index }, rangeExtractor: (range) => { const indexes = defaultRangeExtractor(range) const path = focused() const index = path ? rows().findIndex((row) => row.node.path === path) : -1 if (index < 0 || indexes.includes(index)) return indexes return [...indexes, index].sort((a, b) => a - b) }, }) createEffect(() => { const path = active() if (!path) return const index = rows().findIndex((row) => row.node.path === path) if (index < 0) return queueMicrotask(() => { if (virtualizer.range && index >= virtualizer.range.startIndex && index <= virtualizer.range.endIndex) return virtualizer.scrollToIndex(index, { align: "auto" }) }) }) const rowByKey = createMemo(() => new Map(rows().map((row) => [row.node.path, row] as const))) const virtualItemByKey = createMemo( () => new Map(virtualizer.getVirtualItems().map((item) => [item.key, item] as const)), ) const virtualRowKeys = createMemo(() => virtualizer.getVirtualItems().map((item) => item.key)) return (
{(key) => ( {(item) => (
{(row) => ( setFocused(row().node.path)} onBlur={() => setFocused(undefined)} onClick={() => props.onFileClick?.({ ...row().node, path: row().node.originalPath, absolute: row().node.originalPath, }) } > 0}>
} > setFocused(row().node.path)} onBlur={() => setFocused(undefined)} aria-expanded={file.tree.state(row().node.path)?.expanded ?? true} onClick={() => file.tree.state(row().node.path)?.expanded === false ? file.tree.expand(row().node.path, { list: false }) : file.tree.collapse(row().node.path) } >
)}
)}
)}
) }