summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src/components/dialog-select-file.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'packages/app/src/components/dialog-select-file.tsx')
-rw-r--r--packages/app/src/components/dialog-select-file.tsx330
1 files changed, 194 insertions, 136 deletions
diff --git a/packages/app/src/components/dialog-select-file.tsx b/packages/app/src/components/dialog-select-file.tsx
index 8e221577b..f35d0564c 100644
--- a/packages/app/src/components/dialog-select-file.tsx
+++ b/packages/app/src/components/dialog-select-file.tsx
@@ -36,197 +36,200 @@ type Entry = {
type DialogSelectFileMode = "all" | "files"
-export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFile?: (path: string) => void }) {
- const command = useCommand()
- const language = useLanguage()
- const layout = useLayout()
- const file = useFile()
- const dialog = useDialog()
- const params = useParams()
- const navigate = useNavigate()
- const globalSDK = useGlobalSDK()
- const globalSync = useGlobalSync()
- const filesOnly = () => props.mode === "files"
- const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
- const tabs = createMemo(() => layout.tabs(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",
- "workspace.new",
- "session.previous",
- "session.next",
- "terminal.toggle",
- "review.toggle",
- ]
- const limit = 5
-
- const allowed = createMemo(() => {
- if (filesOnly()) return []
- return 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: language.t("palette.group.commands"),
- option,
- })
-
- const fileItem = (path: string): Entry => ({
- id: "file:" + path,
- type: "file",
- title: path,
- category: language.t("palette.group.files"),
- path,
- })
-
- const projectDirectory = createMemo(() => decode64(params.dir) ?? "")
- const project = createMemo(() => {
- const directory = projectDirectory()
- if (!directory) return
- return layout.projects.list().find((p) => p.worktree === directory || p.sandboxes?.includes(directory))
- })
- const workspaces = createMemo(() => {
- const directory = projectDirectory()
- const current = project()
- if (!current) return directory ? [directory] : []
-
- const dirs = [current.worktree, ...(current.sandboxes ?? [])]
- if (directory && !dirs.includes(directory)) return [...dirs, directory]
- return dirs
- })
- const homedir = createMemo(() => globalSync.data.path.home)
- const label = (directory: string) => {
- const current = project()
- const kind =
- current && directory === current.worktree
- ? language.t("workspace.type.local")
- : language.t("workspace.type.sandbox")
- const [store] = globalSync.child(directory, { bootstrap: false })
- const home = homedir()
- const path = home ? directory.replace(home, "~") : directory
- const name = store.vcs?.branch ?? getFilename(directory)
- return `${kind} : ${name || path}`
+const ENTRY_LIMIT = 5
+const COMMON_COMMAND_IDS = [
+ "session.new",
+ "workspace.new",
+ "session.previous",
+ "session.next",
+ "terminal.toggle",
+ "review.toggle",
+] as const
+
+const uniqueEntries = (items: Entry[]) => {
+ const seen = new Set<string>()
+ const out: Entry[] = []
+ for (const item of items) {
+ if (seen.has(item.id)) continue
+ seen.add(item.id)
+ out.push(item)
}
+ return out
+}
- const sessionItem = (input: {
+const createCommandEntry = (option: CommandOption, category: string): Entry => ({
+ id: "command:" + option.id,
+ type: "command",
+ title: option.title,
+ description: option.description,
+ keybind: option.keybind,
+ category,
+ option,
+})
+
+const createFileEntry = (path: string, category: string): Entry => ({
+ id: "file:" + path,
+ type: "file",
+ title: path,
+ category,
+ path,
+})
+
+const createSessionEntry = (
+ input: {
directory: string
id: string
title: string
description: string
archived?: number
updated?: number
- }): Entry => ({
- id: `session:${input.directory}:${input.id}`,
- type: "session",
- title: input.title,
- description: input.description,
- category: language.t("command.category.session"),
- directory: input.directory,
- sessionID: input.id,
- archived: input.archived,
- updated: input.updated,
+ },
+ category: string,
+): Entry => ({
+ id: `session:${input.directory}:${input.id}`,
+ type: "session",
+ title: input.title,
+ description: input.description,
+ category,
+ directory: input.directory,
+ sessionID: input.id,
+ archived: input.archived,
+ updated: input.updated,
+})
+
+function createCommandEntries(props: {
+ filesOnly: () => boolean
+ command: ReturnType<typeof useCommand>
+ language: ReturnType<typeof useLanguage>
+}) {
+ const allowed = createMemo(() => {
+ if (props.filesOnly()) return []
+ return props.command.options.filter(
+ (option) => !option.disabled && !option.id.startsWith("suggested.") && option.id !== "file.open",
+ )
})
- const list = createMemo(() => allowed().map(commandItem))
+ const list = createMemo(() => {
+ const category = props.language.t("palette.group.commands")
+ return allowed().map((option) => createCommandEntry(option, category))
+ })
const picks = createMemo(() => {
const all = allowed()
- const order = new Map(common.map((id, index) => [id, index]))
+ const order = new Map<string, number>(COMMON_COMMAND_IDS.map((id, index) => [id, index]))
const picked = all.filter((option) => order.has(option.id))
- const base = picked.length ? picked : all.slice(0, limit)
+ const base = picked.length ? picked : all.slice(0, ENTRY_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 category = props.language.t("palette.group.commands")
+ return sorted.map((option) => createCommandEntry(option, category))
})
+ return { allowed, list, picks }
+}
+
+function createFileEntries(props: {
+ file: ReturnType<typeof useFile>
+ tabs: () => ReturnType<ReturnType<typeof useLayout>["tabs"]>
+ language: ReturnType<typeof useLanguage>
+}) {
const recent = createMemo(() => {
- const all = tabs().all()
- const active = tabs().active()
+ const all = props.tabs().all()
+ const active = props.tabs().active()
const order = active ? [active, ...all.filter((item) => item !== active)] : all
const seen = new Set<string>()
+ const category = props.language.t("palette.group.files")
const items: Entry[] = []
for (const item of order) {
- const path = file.pathFromTab(item)
+ const path = props.file.pathFromTab(item)
if (!path) continue
if (seen.has(path)) continue
seen.add(path)
- items.push(fileItem(path))
+ items.push(createFileEntry(path, category))
}
- return items.slice(0, limit)
+ return items.slice(0, ENTRY_LIMIT)
})
const root = createMemo(() => {
- const nodes = file.tree.children("")
+ const category = props.language.t("palette.group.files")
+ const nodes = props.file.tree.children("")
const paths = nodes
.filter((node) => node.type === "file")
.map((node) => node.path)
.sort((a, b) => a.localeCompare(b))
- return paths.slice(0, limit).map(fileItem)
+ return paths.slice(0, ENTRY_LIMIT).map((path) => createFileEntry(path, category))
})
- const unique = (items: Entry[]) => {
- const seen = new Set<string>()
- const out: Entry[] = []
- for (const item of items) {
- if (seen.has(item.id)) continue
- seen.add(item.id)
- out.push(item)
- }
- return out
- }
+ return { recent, root }
+}
- const sessionToken = { value: 0 }
- let sessionInflight: Promise<Entry[]> | undefined
- let sessionAll: Entry[] | undefined
+function createSessionEntries(props: {
+ workspaces: () => string[]
+ label: (directory: string) => string
+ globalSDK: ReturnType<typeof useGlobalSDK>
+ language: ReturnType<typeof useLanguage>
+}) {
+ const state: {
+ token: number
+ inflight: Promise<Entry[]> | undefined
+ cached: Entry[] | undefined
+ } = {
+ token: 0,
+ inflight: undefined,
+ cached: undefined,
+ }
const sessions = (text: string) => {
const query = text.trim()
if (!query) {
- sessionToken.value += 1
- sessionInflight = undefined
- sessionAll = undefined
+ state.token += 1
+ state.inflight = undefined
+ state.cached = undefined
return [] as Entry[]
}
- if (sessionAll) return sessionAll
- if (sessionInflight) return sessionInflight
+ if (state.cached) return state.cached
+ if (state.inflight) return state.inflight
- const current = sessionToken.value
- const dirs = workspaces()
+ const current = state.token
+ const dirs = props.workspaces()
if (dirs.length === 0) return [] as Entry[]
- sessionInflight = Promise.all(
+ state.inflight = Promise.all(
dirs.map((directory) => {
- const description = label(directory)
- return globalSDK.client.session
+ const description = props.label(directory)
+ return props.globalSDK.client.session
.list({ directory, roots: true })
.then((x) =>
(x.data ?? [])
.filter((s) => !!s?.id)
.map((s) => ({
id: s.id,
- title: s.title ?? language.t("command.session.new"),
+ title: s.title ?? props.language.t("command.session.new"),
description,
directory,
archived: s.time?.archived,
updated: s.time?.updated,
})),
)
- .catch(() => [] as { id: string; title: string; description: string; directory: string; archived?: number }[])
+ .catch(
+ () =>
+ [] as {
+ id: string
+ title: string
+ description: string
+ directory: string
+ archived?: number
+ updated?: number
+ }[],
+ )
}),
)
.then((results) => {
- if (sessionToken.value !== current) return [] as Entry[]
+ if (state.token !== current) return [] as Entry[]
const seen = new Set<string>()
+ const category = props.language.t("command.category.session")
const next = results
.flat()
.filter((item) => {
@@ -235,18 +238,71 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
seen.add(key)
return true
})
- .map(sessionItem)
- sessionAll = next
+ .map((item) => createSessionEntry(item, category))
+ state.cached = next
return next
})
.catch(() => [] as Entry[])
.finally(() => {
- sessionInflight = undefined
+ state.inflight = undefined
})
- return sessionInflight
+ return state.inflight
}
+ return { sessions }
+}
+
+export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFile?: (path: string) => void }) {
+ const command = useCommand()
+ const language = useLanguage()
+ const layout = useLayout()
+ const file = useFile()
+ const dialog = useDialog()
+ const params = useParams()
+ const navigate = useNavigate()
+ const globalSDK = useGlobalSDK()
+ const globalSync = useGlobalSync()
+ const filesOnly = () => props.mode === "files"
+ const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
+ const tabs = createMemo(() => layout.tabs(sessionKey))
+ const view = createMemo(() => layout.view(sessionKey))
+ const state = { cleanup: undefined as (() => void) | void, committed: false }
+ const [grouped, setGrouped] = createSignal(false)
+ const commandEntries = createCommandEntries({ filesOnly, command, language })
+ const fileEntries = createFileEntries({ file, tabs, language })
+
+ const projectDirectory = createMemo(() => decode64(params.dir) ?? "")
+ const project = createMemo(() => {
+ const directory = projectDirectory()
+ if (!directory) return
+ return layout.projects.list().find((p) => p.worktree === directory || p.sandboxes?.includes(directory))
+ })
+ const workspaces = createMemo(() => {
+ const directory = projectDirectory()
+ const current = project()
+ if (!current) return directory ? [directory] : []
+
+ const dirs = [current.worktree, ...(current.sandboxes ?? [])]
+ if (directory && !dirs.includes(directory)) return [...dirs, directory]
+ return dirs
+ })
+ const homedir = createMemo(() => globalSync.data.path.home)
+ const label = (directory: string) => {
+ const current = project()
+ const kind =
+ current && directory === current.worktree
+ ? language.t("workspace.type.local")
+ : language.t("workspace.type.sandbox")
+ const [store] = globalSync.child(directory, { bootstrap: false })
+ const home = homedir()
+ const path = home ? directory.replace(home, "~") : directory
+ const name = store.vcs?.branch ?? getFilename(directory)
+ return `${kind} : ${name || path}`
+ }
+
+ const { sessions } = createSessionEntries({ workspaces, label, globalSDK, language })
+
const items = async (text: string) => {
const query = text.trim()
setGrouped(query.length > 0)
@@ -254,7 +310,7 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
if (!query && filesOnly()) {
const loaded = file.tree.state("")?.loaded
const pending = loaded ? Promise.resolve() : file.tree.list("")
- const next = unique([...recent(), ...root()])
+ const next = uniqueEntries([...fileEntries.recent(), ...fileEntries.root()])
if (loaded || next.length > 0) {
void pending
@@ -262,19 +318,21 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
}
await pending
- return unique([...recent(), ...root()])
+ return uniqueEntries([...fileEntries.recent(), ...fileEntries.root()])
}
- if (!query) return [...picks(), ...recent()]
+ if (!query) return [...commandEntries.picks(), ...fileEntries.recent()]
if (filesOnly()) {
const files = await file.searchFiles(query)
- return files.map(fileItem)
+ const category = language.t("palette.group.files")
+ return files.map((path) => createFileEntry(path, category))
}
const [files, nextSessions] = await Promise.all([file.searchFiles(query), Promise.resolve(sessions(query))])
- const entries = files.map(fileItem)
- return [...list(), ...nextSessions, ...entries]
+ const category = language.t("palette.group.files")
+ const entries = files.map((path) => createFileEntry(path, category))
+ return [...commandEntries.list(), ...nextSessions, ...entries]
}
const handleMove = (item: Entry | undefined) => {