|
@@ -4,11 +4,26 @@ import { FileIcon } from "@opencode-ai/ui/file-icon"
|
|
|
import { List } from "@opencode-ai/ui/list"
|
|
import { List } from "@opencode-ai/ui/list"
|
|
|
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
|
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
|
|
import { useParams } from "@solidjs/router"
|
|
import { useParams } from "@solidjs/router"
|
|
|
-import { createMemo } from "solid-js"
|
|
|
|
|
|
|
+import { createMemo, createSignal, onCleanup, Show } from "solid-js"
|
|
|
|
|
+import { formatKeybind, useCommand, type CommandOption } from "@/context/command"
|
|
|
import { useLayout } from "@/context/layout"
|
|
import { useLayout } from "@/context/layout"
|
|
|
import { useFile } from "@/context/file"
|
|
import { useFile } from "@/context/file"
|
|
|
|
|
|
|
|
|
|
+type EntryType = "command" | "file"
|
|
|
|
|
+
|
|
|
|
|
+type Entry = {
|
|
|
|
|
+ id: string
|
|
|
|
|
+ type: EntryType
|
|
|
|
|
+ title: string
|
|
|
|
|
+ description?: string
|
|
|
|
|
+ keybind?: string
|
|
|
|
|
+ category: "Commands" | "Files"
|
|
|
|
|
+ option?: CommandOption
|
|
|
|
|
+ path?: string
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
export function DialogSelectFile() {
|
|
export function DialogSelectFile() {
|
|
|
|
|
+ const command = useCommand()
|
|
|
const layout = useLayout()
|
|
const layout = useLayout()
|
|
|
const file = useFile()
|
|
const file = useFile()
|
|
|
const dialog = useDialog()
|
|
const dialog = useDialog()
|
|
@@ -16,35 +31,148 @@ export function DialogSelectFile() {
|
|
|
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
|
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
|
|
const tabs = createMemo(() => layout.tabs(sessionKey()))
|
|
const tabs = createMemo(() => layout.tabs(sessionKey()))
|
|
|
const view = createMemo(() => layout.view(sessionKey()))
|
|
const view = createMemo(() => layout.view(sessionKey()))
|
|
|
|
|
+ const state = { cleanup: undefined as (() => void) | void, committed: false }
|
|
|
|
|
+ const [grouped, setGrouped] = createSignal(false)
|
|
|
|
|
+ const common = ["session.new", "session.previous", "session.next", "terminal.toggle", "review.toggle"]
|
|
|
|
|
+ const limit = 5
|
|
|
|
|
+
|
|
|
|
|
+ const allowed = createMemo(() =>
|
|
|
|
|
+ command.options.filter(
|
|
|
|
|
+ (option) => !option.disabled && !option.id.startsWith("suggested.") && option.id !== "file.open",
|
|
|
|
|
+ ),
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ const commandItem = (option: CommandOption): Entry => ({
|
|
|
|
|
+ id: "command:" + option.id,
|
|
|
|
|
+ type: "command",
|
|
|
|
|
+ title: option.title,
|
|
|
|
|
+ description: option.description,
|
|
|
|
|
+ keybind: option.keybind,
|
|
|
|
|
+ category: "Commands",
|
|
|
|
|
+ option,
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ const fileItem = (path: string): Entry => ({
|
|
|
|
|
+ id: "file:" + path,
|
|
|
|
|
+ type: "file",
|
|
|
|
|
+ title: path,
|
|
|
|
|
+ category: "Files",
|
|
|
|
|
+ path,
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ const list = createMemo(() => allowed().map(commandItem))
|
|
|
|
|
+
|
|
|
|
|
+ const picks = createMemo(() => {
|
|
|
|
|
+ const all = allowed()
|
|
|
|
|
+ const order = new Map(common.map((id, index) => [id, index]))
|
|
|
|
|
+ const picked = all.filter((option) => order.has(option.id))
|
|
|
|
|
+ const base = picked.length ? picked : all.slice(0, limit)
|
|
|
|
|
+ const sorted = picked.length ? [...base].sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0)) : base
|
|
|
|
|
+ return sorted.map(commandItem)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ const recent = createMemo(() => {
|
|
|
|
|
+ const all = tabs().all()
|
|
|
|
|
+ const active = tabs().active()
|
|
|
|
|
+ const order = active ? [active, ...all.filter((item) => item !== active)] : all
|
|
|
|
|
+ const seen = new Set<string>()
|
|
|
|
|
+ const items: Entry[] = []
|
|
|
|
|
+
|
|
|
|
|
+ for (const item of order) {
|
|
|
|
|
+ const path = file.pathFromTab(item)
|
|
|
|
|
+ if (!path) continue
|
|
|
|
|
+ if (seen.has(path)) continue
|
|
|
|
|
+ seen.add(path)
|
|
|
|
|
+ items.push(fileItem(path))
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return items.slice(0, limit)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ const items = async (filter: string) => {
|
|
|
|
|
+ const query = filter.trim()
|
|
|
|
|
+ setGrouped(query.length > 0)
|
|
|
|
|
+ if (!query) return [...picks(), ...recent()]
|
|
|
|
|
+ const files = await file.searchFiles(query)
|
|
|
|
|
+ const entries = files.map(fileItem)
|
|
|
|
|
+ return [...list(), ...entries]
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const handleMove = (item: Entry | undefined) => {
|
|
|
|
|
+ state.cleanup?.()
|
|
|
|
|
+ if (!item) return
|
|
|
|
|
+ if (item.type !== "command") return
|
|
|
|
|
+ state.cleanup = item.option?.onHighlight?.()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const open = (path: string) => {
|
|
|
|
|
+ const value = file.tab(path)
|
|
|
|
|
+ tabs().open(value)
|
|
|
|
|
+ file.load(path)
|
|
|
|
|
+ view().reviewPanel.open()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const handleSelect = (item: Entry | undefined) => {
|
|
|
|
|
+ if (!item) return
|
|
|
|
|
+ state.committed = true
|
|
|
|
|
+ state.cleanup = undefined
|
|
|
|
|
+ dialog.close()
|
|
|
|
|
+
|
|
|
|
|
+ if (item.type === "command") {
|
|
|
|
|
+ item.option?.onSelect?.("palette")
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!item.path) return
|
|
|
|
|
+ open(item.path)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ onCleanup(() => {
|
|
|
|
|
+ if (state.committed) return
|
|
|
|
|
+ state.cleanup?.()
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
return (
|
|
return (
|
|
|
- <Dialog title="Select file">
|
|
|
|
|
|
|
+ <Dialog title="Search">
|
|
|
<List
|
|
<List
|
|
|
- search={{ placeholder: "Search files", autofocus: true }}
|
|
|
|
|
- emptyMessage="No files found"
|
|
|
|
|
- items={file.searchFiles}
|
|
|
|
|
- key={(x) => x}
|
|
|
|
|
- onSelect={(path) => {
|
|
|
|
|
- if (path) {
|
|
|
|
|
- const value = file.tab(path)
|
|
|
|
|
- tabs().open(value)
|
|
|
|
|
- file.load(path)
|
|
|
|
|
- view().reviewPanel.open()
|
|
|
|
|
- }
|
|
|
|
|
- dialog.close()
|
|
|
|
|
- }}
|
|
|
|
|
|
|
+ search={{ placeholder: "Search files and commands", autofocus: true }}
|
|
|
|
|
+ emptyMessage="No results found"
|
|
|
|
|
+ items={items}
|
|
|
|
|
+ key={(item) => item.id}
|
|
|
|
|
+ filterKeys={["title", "description", "category"]}
|
|
|
|
|
+ groupBy={(item) => (grouped() ? item.category : "")}
|
|
|
|
|
+ onMove={handleMove}
|
|
|
|
|
+ onSelect={handleSelect}
|
|
|
>
|
|
>
|
|
|
- {(i) => (
|
|
|
|
|
- <div class="w-full flex items-center justify-between rounded-md">
|
|
|
|
|
- <div class="flex items-center gap-x-3 grow min-w-0">
|
|
|
|
|
- <FileIcon node={{ path: i, type: "file" }} class="shrink-0 size-4" />
|
|
|
|
|
- <div class="flex items-center text-14-regular">
|
|
|
|
|
- <span class="text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
|
|
|
|
|
- {getDirectory(i)}
|
|
|
|
|
- </span>
|
|
|
|
|
- <span class="text-text-strong whitespace-nowrap">{getFilename(i)}</span>
|
|
|
|
|
|
|
+ {(item) => (
|
|
|
|
|
+ <Show
|
|
|
|
|
+ when={item.type === "command"}
|
|
|
|
|
+ fallback={
|
|
|
|
|
+ <div class="w-full flex items-center justify-between rounded-md">
|
|
|
|
|
+ <div class="flex items-center gap-x-3 grow min-w-0">
|
|
|
|
|
+ <FileIcon node={{ path: item.path ?? "", type: "file" }} class="shrink-0 size-4" />
|
|
|
|
|
+ <div class="flex items-center text-14-regular">
|
|
|
|
|
+ <span class="text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
|
|
|
|
|
+ {getDirectory(item.path ?? "")}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <span class="text-text-strong whitespace-nowrap">{getFilename(item.path ?? "")}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ }
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="w-full flex items-center justify-between gap-4">
|
|
|
|
|
+ <div class="flex items-center gap-2 min-w-0">
|
|
|
|
|
+ <span class="text-14-regular text-text-strong whitespace-nowrap">{item.title}</span>
|
|
|
|
|
+ <Show when={item.description}>
|
|
|
|
|
+ <span class="text-14-regular text-text-weak truncate">{item.description}</span>
|
|
|
|
|
+ </Show>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ <Show when={item.keybind}>
|
|
|
|
|
+ <span class="text-12-regular text-text-subtle shrink-0">{formatKeybind(item.keybind ?? "")}</span>
|
|
|
|
|
+ </Show>
|
|
|
</div>
|
|
</div>
|
|
|
- </div>
|
|
|
|
|
|
|
+ </Show>
|
|
|
)}
|
|
)}
|
|
|
</List>
|
|
</List>
|
|
|
</Dialog>
|
|
</Dialog>
|