diff options
Diffstat (limited to 'packages/app/src/components/dialog-select-file.tsx')
| -rw-r--r-- | packages/app/src/components/dialog-select-file.tsx | 330 |
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) => { |
