file-tree.tsx 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. import { useLocal, type LocalFile } from "@/context/local"
  2. import { Collapsible } from "@opencode-ai/ui/collapsible"
  3. import { FileIcon } from "@opencode-ai/ui/file-icon"
  4. import { Tooltip } from "@opencode-ai/ui/tooltip"
  5. import { For, Match, Switch, type ComponentProps, type ParentProps } from "solid-js"
  6. import { Dynamic } from "solid-js/web"
  7. export default function FileTree(props: {
  8. path: string
  9. class?: string
  10. nodeClass?: string
  11. level?: number
  12. onFileClick?: (file: LocalFile) => void
  13. }) {
  14. const local = useLocal()
  15. const level = props.level ?? 0
  16. const Node = (p: ParentProps & ComponentProps<"div"> & { node: LocalFile; as?: "div" | "button" }) => (
  17. <Dynamic
  18. component={p.as ?? "div"}
  19. classList={{
  20. "p-0.5 w-full flex items-center gap-x-2 hover:bg-background-element": true,
  21. // "bg-background-element": local.file.active()?.path === p.node.path,
  22. [props.nodeClass ?? ""]: !!props.nodeClass,
  23. }}
  24. style={`padding-left: ${level * 10}px`}
  25. draggable={true}
  26. onDragStart={(e: any) => {
  27. const evt = e as globalThis.DragEvent
  28. evt.dataTransfer!.effectAllowed = "copy"
  29. evt.dataTransfer!.setData("text/plain", `file:${p.node.path}`)
  30. // Create custom drag image without margins
  31. const dragImage = document.createElement("div")
  32. dragImage.className =
  33. "flex items-center gap-x-2 px-2 py-1 bg-background-element rounded-md border border-border-1"
  34. dragImage.style.position = "absolute"
  35. dragImage.style.top = "-1000px"
  36. // Copy only the icon and text content without padding
  37. const icon = e.currentTarget.querySelector("svg")
  38. const text = e.currentTarget.querySelector("span")
  39. if (icon && text) {
  40. dragImage.innerHTML = icon.outerHTML + text.outerHTML
  41. }
  42. document.body.appendChild(dragImage)
  43. evt.dataTransfer!.setDragImage(dragImage, 0, 12)
  44. setTimeout(() => document.body.removeChild(dragImage), 0)
  45. }}
  46. {...p}
  47. >
  48. {p.children}
  49. <span
  50. classList={{
  51. "text-xs whitespace-nowrap truncate": true,
  52. "text-text-muted/40": p.node.ignored,
  53. "text-text-muted/80": !p.node.ignored,
  54. // "!text-text": local.file.active()?.path === p.node.path,
  55. // "!text-primary": local.file.changed(p.node.path),
  56. }}
  57. >
  58. {p.node.name}
  59. </span>
  60. {/* <Show when={local.file.changed(p.node.path)}> */}
  61. {/* <span class="ml-auto mr-1 w-1.5 h-1.5 rounded-full bg-primary/50 shrink-0" /> */}
  62. {/* </Show> */}
  63. </Dynamic>
  64. )
  65. return (
  66. <div class={`flex flex-col ${props.class}`}>
  67. <For each={local.file.children(props.path)}>
  68. {(node) => (
  69. <Tooltip forceMount={false} openDelay={2000} value={node.path} placement="right">
  70. <Switch>
  71. <Match when={node.type === "directory"}>
  72. <Collapsible
  73. variant="ghost"
  74. class="w-full"
  75. forceMount={false}
  76. // open={local.file.node(node.path)?.expanded}
  77. onOpenChange={(open) => (open ? local.file.expand(node.path) : local.file.collapse(node.path))}
  78. >
  79. <Collapsible.Trigger>
  80. <Node node={node}>
  81. <Collapsible.Arrow class="text-text-muted/60 ml-1" />
  82. <FileIcon
  83. node={node}
  84. // expanded={local.file.node(node.path).expanded}
  85. class="text-text-muted/60 -ml-1"
  86. />
  87. </Node>
  88. </Collapsible.Trigger>
  89. <Collapsible.Content>
  90. <FileTree path={node.path} level={level + 1} onFileClick={props.onFileClick} />
  91. </Collapsible.Content>
  92. </Collapsible>
  93. </Match>
  94. <Match when={node.type === "file"}>
  95. <Node node={node} as="button" onClick={() => props.onFileClick?.(node)}>
  96. <div class="w-4 shrink-0" />
  97. <FileIcon node={node} class="text-primary" />
  98. </Node>
  99. </Match>
  100. </Switch>
  101. </Tooltip>
  102. )}
  103. </For>
  104. </div>
  105. )
  106. }