use keymap state for layer visibility (#26246)
This commit is contained in:
@@ -1,163 +0,0 @@
|
||||
import { createContext, createMemo, createSignal, useContext, type Accessor, type ParentProps } from "solid-js"
|
||||
import { DialogSelect, type DialogSelectRef } from "@tui/ui/dialog-select"
|
||||
import { useDialog, type DialogContext } from "@tui/ui/dialog"
|
||||
import {
|
||||
formatKeyBindings,
|
||||
reactiveMatcherFromSignal,
|
||||
type OpenTuiKeymap,
|
||||
useKeymapSelector,
|
||||
useOpencodeKeymap,
|
||||
} from "../keymap"
|
||||
import { useTuiConfig } from "./tui-config"
|
||||
|
||||
type SlashEntry = {
|
||||
display: string
|
||||
description?: string
|
||||
aliases?: string[]
|
||||
onSelect: () => void
|
||||
}
|
||||
|
||||
type CommandPaletteContext = {
|
||||
run(command: string): void
|
||||
show(): void
|
||||
slashes: Accessor<readonly SlashEntry[]>
|
||||
suspend(enabled: boolean): void
|
||||
readonly suspended: boolean
|
||||
matcher: ReturnType<typeof reactiveMatcherFromSignal>
|
||||
}
|
||||
|
||||
const COMMAND_PALETTE_DIALOG = "command.palette.show"
|
||||
const ctx = createContext<CommandPaletteContext>()
|
||||
type PaletteCommandEntry = ReturnType<OpenTuiKeymap["getCommandEntries"]>[number]
|
||||
|
||||
function isVisiblePaletteCommand(entry: PaletteCommandEntry) {
|
||||
return entry.command.hidden !== true && entry.command.name !== COMMAND_PALETTE_DIALOG
|
||||
}
|
||||
|
||||
function isSuggestedPaletteCommand(entry: PaletteCommandEntry) {
|
||||
const suggested = entry.command.suggested
|
||||
if (typeof suggested === "boolean") return suggested
|
||||
if (typeof suggested === "function") return suggested() === true
|
||||
return false
|
||||
}
|
||||
|
||||
export function CommandPaletteProvider(props: ParentProps) {
|
||||
const dialog = useDialog()
|
||||
const keymap = useOpencodeKeymap()
|
||||
const [suspendCount, setSuspendCount] = createSignal(0)
|
||||
const entries = useKeymapSelector((keymap: OpenTuiKeymap) =>
|
||||
keymap
|
||||
.getCommandEntries({
|
||||
visibility: "reachable",
|
||||
namespace: "palette",
|
||||
})
|
||||
.filter(isVisiblePaletteCommand),
|
||||
)
|
||||
|
||||
const run = (command: string) => {
|
||||
keymap.dispatchCommand(command)
|
||||
}
|
||||
|
||||
const slashes = createMemo<SlashEntry[]>(() =>
|
||||
entries().flatMap((entry) => {
|
||||
const slashName = entry.command.slashName
|
||||
if (typeof slashName !== "string" || !slashName) return []
|
||||
const slashAliases = entry.command.slashAliases
|
||||
return {
|
||||
display: `/${slashName}`,
|
||||
description:
|
||||
typeof entry.command.desc === "string"
|
||||
? entry.command.desc
|
||||
: typeof entry.command.title === "string"
|
||||
? entry.command.title
|
||||
: undefined,
|
||||
aliases: Array.isArray(slashAliases)
|
||||
? slashAliases.filter((alias): alias is string => typeof alias === "string").map((alias) => `/${alias}`)
|
||||
: undefined,
|
||||
onSelect: () => run(entry.command.name),
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
const value: CommandPaletteContext = {
|
||||
run,
|
||||
show() {
|
||||
dialog.replace(() => <CommandPaletteDialog run={run} />)
|
||||
},
|
||||
slashes,
|
||||
suspend(enabled: boolean) {
|
||||
setSuspendCount((count) => Math.max(0, count + (enabled ? 1 : -1)))
|
||||
},
|
||||
get suspended() {
|
||||
return suspendCount() > 0 || dialog.stack.length > 0
|
||||
},
|
||||
matcher: reactiveMatcherFromSignal(() => suspendCount() === 0 && dialog.stack.length === 0),
|
||||
}
|
||||
|
||||
return <ctx.Provider value={value}>{props.children}</ctx.Provider>
|
||||
}
|
||||
|
||||
export function useCommandPalette() {
|
||||
const value = useContext(ctx)
|
||||
if (!value) throw new Error("CommandPalette context must be used within a CommandPaletteProvider")
|
||||
return value
|
||||
}
|
||||
|
||||
function CommandPaletteDialog(props: { run(command: string): void }) {
|
||||
const config = useTuiConfig()
|
||||
const entries = useKeymapSelector((keymap: OpenTuiKeymap) => {
|
||||
const query = {
|
||||
namespace: "palette",
|
||||
}
|
||||
const reachable = keymap
|
||||
.getCommandEntries({
|
||||
...query,
|
||||
visibility: "reachable",
|
||||
})
|
||||
.filter(isVisiblePaletteCommand)
|
||||
const registeredBindings = keymap.getCommandBindings({
|
||||
visibility: "registered",
|
||||
commands: reachable.map((entry) => entry.command.name),
|
||||
})
|
||||
|
||||
return reachable.map((entry) => ({
|
||||
...entry,
|
||||
bindings: registeredBindings.get(entry.command.name) ?? entry.bindings,
|
||||
}))
|
||||
})
|
||||
const options = createMemo(() =>
|
||||
entries().map((entry) => ({
|
||||
title: typeof entry.command.title === "string" ? entry.command.title : entry.command.name,
|
||||
description: typeof entry.command.desc === "string" ? entry.command.desc : undefined,
|
||||
category: typeof entry.command.category === "string" ? entry.command.category : undefined,
|
||||
footer: formatKeyBindings(entry.bindings, config),
|
||||
value: entry.command.name,
|
||||
suggested: isSuggestedPaletteCommand(entry),
|
||||
onSelect: (dialog: DialogContext) => {
|
||||
dialog.clear()
|
||||
props.run(entry.command.name)
|
||||
},
|
||||
})),
|
||||
)
|
||||
|
||||
let ref: DialogSelectRef<string>
|
||||
const list = () => {
|
||||
if (ref?.filter) return options()
|
||||
return [
|
||||
...options()
|
||||
.filter((option) => option.suggested)
|
||||
.map((option) => ({
|
||||
...option,
|
||||
value: `suggested:${option.value}`,
|
||||
category: "Suggested",
|
||||
})),
|
||||
...options(),
|
||||
]
|
||||
}
|
||||
|
||||
return <DialogSelect ref={(value) => (ref = value)} title="Commands" options={list()} />
|
||||
}
|
||||
|
||||
export function useCommandSlashes(): Accessor<readonly SlashEntry[]> {
|
||||
return useCommandPalette().slashes
|
||||
}
|
||||
Reference in New Issue
Block a user