/** @jsxImportSource @opentui/solid */ import type { ColorInput, RGBA, ScrollBoxRenderable } from "@opentui/core" import { Locale } from "../../util/locale" import { tint } from "../../context/theme" import { createEffect, createMemo, For, Match, Switch } from "solid-js" import { buildFileTree, flattenFileTree, type FileTreeItem, type FileTreeRow } from "./diff-viewer-file-tree-utils" import { Panel } from "./diff-viewer-ui" const FILE_TREE_STATUS_WIDTH = 2 export type DiffViewerFileTreeTheme = { readonly background: RGBA readonly backgroundPanel: ColorInput readonly backgroundElement: ColorInput readonly primary: ColorInput readonly secondary: ColorInput readonly selectedListItemText: ColorInput readonly text: RGBA readonly textMuted: RGBA readonly error: ColorInput } export type DiffViewerFileTreeProps = { readonly width: number readonly files: readonly FileTreeItem[] readonly loading: boolean readonly error: unknown readonly theme: DiffViewerFileTreeTheme readonly focused?: boolean readonly highlightedNode?: number readonly selectedFileIndex?: number readonly reviewedFileNames?: ReadonlySet readonly expandedNodes?: ReadonlySet readonly onRowClick?: (row: FileTreeRow) => void } export function DiffViewerFileTree(props: DiffViewerFileTreeProps) { const tree = createMemo(() => buildFileTree(props.files)) const rows = createMemo(() => flattenFileTree(tree(), props.expandedNodes)) let scroll: ScrollBoxRenderable | undefined createEffect(() => { const node = props.highlightedNode if (node === undefined) return const selectedIndex = rows().findIndex((row) => row.id === node) if (selectedIndex === -1) return const scrollSelectedIntoView = () => scrollFileTreeRowIntoView(scroll, selectedIndex) scrollSelectedIntoView() requestAnimationFrame(scrollSelectedIntoView) }) const fadedColor = () => tint(props.theme.text, props.theme.background, 0.75) return ( (scroll = element)} verticalScrollbarOptions={{ visible: false }} horizontalScrollbarOptions={{ visible: false }} > No files 0}> {(row, index) => { const highlighted = () => props.focused && props.highlightedNode === row.id const selected = () => row.fileIndex !== undefined && props.selectedFileIndex === row.fileIndex const reviewed = () => { const file = row.fileIndex === undefined ? undefined : props.files[row.fileIndex]?.file return file !== undefined && (props.reviewedFileNames?.has(file) ?? false) } const prefix = () => fileTreeRowPrefix(rows(), index(), row, props.expandedNodes) const status = () => fileTreeRowStatus(row, props.files, reviewed()) const name = () => Locale.truncate(row.name, Math.max(1, props.width - FILE_TREE_STATUS_WIDTH - prefix().length)) return ( props.onRowClick?.(row)} > {prefix()} {name()} {status()} ) }} ) } function scrollFileTreeRowIntoView(scroll: ScrollBoxRenderable | undefined, index: number) { if (!scroll) return if (index < scroll.scrollTop) { scroll.scrollTo(index) return } if (index >= scroll.scrollTop + scroll.viewport.height) { scroll.scrollTo(index - scroll.viewport.height + 1) } } function fileTreeRowPrefix( rows: readonly FileTreeRow[], index: number, row: FileTreeRow, expandedNodes: ReadonlySet | undefined, ) { const indentation = Array.from({ length: row.depth }, (_, depth) => { if (depth === 0 && !hasLaterSibling(rows, 0, 0)) return " " return hasLaterSibling(rows, index, depth) ? "│ " : " " }).join("") const topRoot = index === 0 && row.depth === 0 const branch = topRoot ? " " : hasLaterSibling(rows, index, row.depth) ? "├─ " : "└─ " const marker = row.kind === "directory" ? (expandedNodes && !expandedNodes.has(row.id) ? "▸ " : "▾ ") : "" return `${indentation}${branch}${marker}` } function hasLaterSibling(rows: readonly FileTreeRow[], index: number, depth: number) { return rows.slice(index + 1).find((row) => row.depth <= depth)?.depth === depth } function fileTreeRowStatus(row: FileTreeRow, files: readonly FileTreeItem[], reviewed: boolean) { if (row.fileIndex === undefined) return "" const status = files[row.fileIndex]?.status const marker = status === "modified" ? "M" : status === "added" ? "A" : status === "deleted" ? "D" : "?" return `${reviewed ? "✓" : " "}${marker}`.padStart(FILE_TREE_STATUS_WIDTH) }