summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src/components/file-tree.tsx
blob: 3439d366ceeb515b7da1dbbc864e94020b4a0bdc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
import { useLocal, type LocalFile } from "@/context/local"
import { Collapsible } from "@opencode-ai/ui/collapsible"
import { FileIcon } from "@opencode-ai/ui/file-icon"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { For, Match, Switch, type ComponentProps, type ParentProps } from "solid-js"
import { Dynamic } from "solid-js/web"

export default function FileTree(props: {
  path: string
  class?: string
  nodeClass?: string
  level?: number
  onFileClick?: (file: LocalFile) => void
}) {
  const local = useLocal()
  const level = props.level ?? 0

  const Node = (p: ParentProps & ComponentProps<"div"> & { node: LocalFile; as?: "div" | "button" }) => (
    <Dynamic
      component={p.as ?? "div"}
      classList={{
        "p-0.5 w-full flex items-center gap-x-2 hover:bg-background-element": true,
        // "bg-background-element": local.file.active()?.path === p.node.path,
        [props.nodeClass ?? ""]: !!props.nodeClass,
      }}
      style={`padding-left: ${level * 10}px`}
      draggable={true}
      onDragStart={(e: any) => {
        const evt = e as globalThis.DragEvent
        evt.dataTransfer!.effectAllowed = "copy"
        evt.dataTransfer!.setData("text/plain", `file:${p.node.path}`)

        // Create custom drag image without margins
        const dragImage = document.createElement("div")
        dragImage.className =
          "flex items-center gap-x-2 px-2 py-1 bg-background-element rounded-md border border-border-1"
        dragImage.style.position = "absolute"
        dragImage.style.top = "-1000px"

        // Copy only the icon and text content without padding
        const icon = e.currentTarget.querySelector("svg")
        const text = e.currentTarget.querySelector("span")
        if (icon && text) {
          dragImage.innerHTML = icon.outerHTML + text.outerHTML
        }

        document.body.appendChild(dragImage)
        evt.dataTransfer!.setDragImage(dragImage, 0, 12)
        setTimeout(() => document.body.removeChild(dragImage), 0)
      }}
      {...p}
    >
      {p.children}
      <span
        classList={{
          "text-xs whitespace-nowrap truncate": true,
          "text-text-muted/40": p.node.ignored,
          "text-text-muted/80": !p.node.ignored,
          // "!text-text": local.file.active()?.path === p.node.path,
          // "!text-primary": local.file.changed(p.node.path),
        }}
      >
        {p.node.name}
      </span>
      {/* <Show when={local.file.changed(p.node.path)}> */}
      {/*   <span class="ml-auto mr-1 w-1.5 h-1.5 rounded-full bg-primary/50 shrink-0" /> */}
      {/* </Show> */}
    </Dynamic>
  )

  return (
    <div class={`flex flex-col ${props.class}`}>
      <For each={local.file.children(props.path)}>
        {(node) => (
          <Tooltip forceMount={false} openDelay={2000} value={node.path} placement="right">
            <Switch>
              <Match when={node.type === "directory"}>
                <Collapsible
                  variant="ghost"
                  class="w-full"
                  forceMount={false}
                  // open={local.file.node(node.path)?.expanded}
                  onOpenChange={(open) => (open ? local.file.expand(node.path) : local.file.collapse(node.path))}
                >
                  <Collapsible.Trigger>
                    <Node node={node}>
                      <Collapsible.Arrow class="text-text-muted/60 ml-1" />
                      <FileIcon
                        node={node}
                        // expanded={local.file.node(node.path).expanded}
                        class="text-text-muted/60 -ml-1"
                      />
                    </Node>
                  </Collapsible.Trigger>
                  <Collapsible.Content>
                    <FileTree path={node.path} level={level + 1} onFileClick={props.onFileClick} />
                  </Collapsible.Content>
                </Collapsible>
              </Match>
              <Match when={node.type === "file"}>
                <Node node={node} as="button" onClick={() => props.onFileClick?.(node)}>
                  <div class="w-4 shrink-0" />
                  <FileIcon node={node} class="text-primary" />
                </Node>
              </Match>
            </Switch>
          </Tooltip>
        )}
      </For>
    </div>
  )
}