refactor(tui): simplify location and command contexts
This commit is contained in:
parent
94a23b8537
commit
2cb2f9d538
6 changed files with 78 additions and 108 deletions
|
|
@ -135,6 +135,10 @@ export interface KeymapCommand {
|
||||||
readonly name: string
|
readonly name: string
|
||||||
readonly aliases?: string[]
|
readonly aliases?: string[]
|
||||||
}
|
}
|
||||||
|
/** Hides the command from discovery UI. */
|
||||||
|
readonly hidden?: boolean
|
||||||
|
/** Promotes the command in discovery UI. */
|
||||||
|
readonly suggested?: boolean | (() => boolean)
|
||||||
/** Executes the command. Return false to let keymap dispatch continue. */
|
/** Executes the command. Return false to let keymap dispatch continue. */
|
||||||
readonly run: () => void | false | Promise<void>
|
readonly run: () => void | false | Promise<void>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@
|
||||||
"./context/epilogue": "./src/context/epilogue.tsx",
|
"./context/epilogue": "./src/context/epilogue.tsx",
|
||||||
"./context/exit": "./src/context/exit.tsx",
|
"./context/exit": "./src/context/exit.tsx",
|
||||||
"./context/log": "./src/context/log.tsx",
|
"./context/log": "./src/context/log.tsx",
|
||||||
"./context/project": "./src/context/project.tsx",
|
|
||||||
"./context/runtime": "./src/context/runtime.tsx",
|
"./context/runtime": "./src/context/runtime.tsx",
|
||||||
"./context/client": "./src/context/client.tsx",
|
"./context/client": "./src/context/client.tsx",
|
||||||
"./context/theme": "./src/context/theme.tsx",
|
"./context/theme": "./src/context/theme.tsx",
|
||||||
|
|
|
||||||
|
|
@ -1,63 +1,35 @@
|
||||||
import { createMemo } from "solid-js"
|
import { createMemo } from "solid-js"
|
||||||
import { DialogSelect, type DialogSelectRef } from "../ui/dialog-select"
|
import { DialogSelect, type DialogSelectRef } from "../ui/dialog-select"
|
||||||
import { type DialogContext } from "../ui/dialog"
|
import { type DialogContext } from "../ui/dialog"
|
||||||
import {
|
import { COMMAND_PALETTE_COMMAND } from "../keymap"
|
||||||
COMMAND_PALETTE_COMMAND,
|
import { Keymap, type KeymapCommand } from "../context/keymap"
|
||||||
formatKeyBindings,
|
|
||||||
type OpenTuiKeymap,
|
|
||||||
useKeymapSelector,
|
|
||||||
useOpencodeKeymap,
|
|
||||||
} from "../keymap"
|
|
||||||
import { useConfig } from "../config"
|
|
||||||
|
|
||||||
type PaletteCommandEntry = ReturnType<OpenTuiKeymap["getCommandEntries"]>[number]
|
function isSuggestedPaletteCommand(command: KeymapCommand) {
|
||||||
|
const suggested = command.suggested
|
||||||
function isVisiblePaletteCommand(command: PaletteCommandEntry["command"]) {
|
|
||||||
return command.hidden !== true && command.name !== COMMAND_PALETTE_COMMAND
|
|
||||||
}
|
|
||||||
|
|
||||||
function isSuggestedPaletteCommand(entry: PaletteCommandEntry) {
|
|
||||||
const suggested = entry.command.suggested
|
|
||||||
if (typeof suggested === "boolean") return suggested
|
if (typeof suggested === "boolean") return suggested
|
||||||
if (typeof suggested === "function") return suggested() === true
|
if (typeof suggested === "function") return suggested() === true
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CommandPaletteDialog() {
|
export function CommandPaletteDialog() {
|
||||||
const config = useConfig().data
|
const commands = Keymap.useCommands()
|
||||||
const keymap = useOpencodeKeymap()
|
const shortcuts = Keymap.useShortcuts()
|
||||||
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(() =>
|
const options = createMemo(() =>
|
||||||
entries().map((entry) => ({
|
commands().flatMap((command) => {
|
||||||
title: typeof entry.command.title === "string" ? entry.command.title : entry.command.name,
|
if (!command.id || !command.palette || command.hidden || command.id === COMMAND_PALETTE_COMMAND) return []
|
||||||
description: typeof entry.command.desc === "string" ? entry.command.desc : undefined,
|
return {
|
||||||
category: typeof entry.command.category === "string" ? entry.command.category : undefined,
|
title: command.title ?? command.id,
|
||||||
footer: formatKeyBindings(entry.bindings, config),
|
description: command.description,
|
||||||
value: entry.command.name,
|
category: command.group,
|
||||||
suggested: isSuggestedPaletteCommand(entry),
|
footer: shortcuts.all(command.id),
|
||||||
onSelect: (dialog: DialogContext) => {
|
value: command.id,
|
||||||
dialog.clear()
|
suggested: isSuggestedPaletteCommand(command),
|
||||||
keymap.dispatchCommand(entry.command.name)
|
onSelect: (dialog: DialogContext) => {
|
||||||
},
|
dialog.clear()
|
||||||
})),
|
command.run()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
let ref: DialogSelectRef<string>
|
let ref: DialogSelectRef<string>
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import { useTerminalDimensions } from "@opentui/solid"
|
||||||
import { Locale } from "../../util/locale"
|
import { Locale } from "../../util/locale"
|
||||||
import type { PromptInfo, PromptPartRef } from "../../prompt/history"
|
import type { PromptInfo, PromptPartRef } from "../../prompt/history"
|
||||||
import { useFrecency } from "../../prompt/frecency"
|
import { useFrecency } from "../../prompt/frecency"
|
||||||
import { useBindings, useCommandSlashes } from "../../keymap"
|
import { useBindings } from "../../keymap"
|
||||||
import { Keymap } from "../../context/keymap"
|
import { Keymap } from "../../context/keymap"
|
||||||
import { displayCharAt, mentionTriggerIndex } from "../../prompt/display"
|
import { displayCharAt, mentionTriggerIndex } from "../../prompt/display"
|
||||||
import type { FileSystemEntry } from "@opencode-ai/client"
|
import type { FileSystemEntry } from "@opencode-ai/client"
|
||||||
|
|
@ -86,8 +86,8 @@ export function Autocomplete(props: {
|
||||||
const editor = useEditorContext()
|
const editor = useEditorContext()
|
||||||
const client = useClient()
|
const client = useClient()
|
||||||
const data = useData()
|
const data = useData()
|
||||||
const slashes = useCommandSlashes()
|
|
||||||
const keymap = Keymap.use()
|
const keymap = Keymap.use()
|
||||||
|
const keymapCommands = Keymap.useCommands()
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const dimensions = useTerminalDimensions()
|
const dimensions = useTerminalDimensions()
|
||||||
const frecency = useFrecency()
|
const frecency = useFrecency()
|
||||||
|
|
@ -428,7 +428,15 @@ export function Autocomplete(props: {
|
||||||
)
|
)
|
||||||
|
|
||||||
const commands = createMemo((): AutocompleteOption[] => {
|
const commands = createMemo((): AutocompleteOption[] => {
|
||||||
const results: AutocompleteOption[] = [...slashes()]
|
const results: AutocompleteOption[] = keymapCommands().flatMap((command) => {
|
||||||
|
if (!command.slash) return []
|
||||||
|
return {
|
||||||
|
display: `/${command.slash.name}`,
|
||||||
|
description: command.description ?? command.title,
|
||||||
|
aliases: command.slash.aliases?.map((alias) => `/${alias}`),
|
||||||
|
onSelect: command.run,
|
||||||
|
}
|
||||||
|
})
|
||||||
const commandNames = new Set<string>()
|
const commandNames = new Set<string>()
|
||||||
|
|
||||||
for (const serverCommand of data.location.command.list(location.current) ?? []) {
|
for (const serverCommand of data.location.command.list(location.current) ?? []) {
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import {
|
||||||
registerManagedTextareaLayer,
|
registerManagedTextareaLayer,
|
||||||
registerTimedLeader,
|
registerTimedLeader,
|
||||||
} from "@opentui/keymap/addons/opentui"
|
} from "@opentui/keymap/addons/opentui"
|
||||||
import { formatKeySequence } from "@opentui/keymap/extras"
|
import { formatCommandBindings, formatKeySequence } from "@opentui/keymap/extras"
|
||||||
import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui"
|
import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui"
|
||||||
import { KeymapProvider, useBindings, useKeymapSelector } from "@opentui/keymap/solid"
|
import { KeymapProvider, useBindings, useKeymapSelector } from "@opentui/keymap/solid"
|
||||||
import { useRenderer } from "@opentui/solid"
|
import { useRenderer } from "@opentui/solid"
|
||||||
|
|
@ -19,6 +19,7 @@ import { TuiKeybind } from "../config/keybind"
|
||||||
|
|
||||||
declare module "@opentui/keymap" {
|
declare module "@opentui/keymap" {
|
||||||
interface Command {
|
interface Command {
|
||||||
|
opencode?: KeymapCommand
|
||||||
slash?: {
|
slash?: {
|
||||||
name: string
|
name: string
|
||||||
aliases?: string[]
|
aliases?: string[]
|
||||||
|
|
@ -177,6 +178,7 @@ function createLayer(input: () => KeymapLayer) {
|
||||||
return {
|
return {
|
||||||
...definition,
|
...definition,
|
||||||
name: id,
|
name: id,
|
||||||
|
opencode: command,
|
||||||
...(description === undefined ? {} : { desc: description }),
|
...(description === undefined ? {} : { desc: description }),
|
||||||
...(group === undefined ? {} : { category: group }),
|
...(group === undefined ? {} : { category: group }),
|
||||||
...(palette === undefined ? {} : { namespace: "palette" }),
|
...(palette === undefined ? {} : { namespace: "palette" }),
|
||||||
|
|
@ -215,12 +217,21 @@ function useShortcuts() {
|
||||||
const commands = keymap.getCommands({ visibility: "registered" }).map((command) => command.name)
|
const commands = keymap.getCommands({ visibility: "registered" }).map((command) => command.name)
|
||||||
const bindings = keymap.getCommandBindings({ visibility: "registered", commands })
|
const bindings = keymap.getCommandBindings({ visibility: "registered", commands })
|
||||||
return new Map(
|
return new Map(
|
||||||
commands.map((id) => [id, formatKeySequence(bindings.get(id)?.[0]?.sequence, formatOptions(config.data))]),
|
commands.map((id) => [
|
||||||
|
id,
|
||||||
|
{
|
||||||
|
first: formatKeySequence(bindings.get(id)?.[0]?.sequence, formatOptions(config.data)),
|
||||||
|
all: formatCommandBindings(bindings.get(id) ?? [], formatOptions(config.data)),
|
||||||
|
},
|
||||||
|
]),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
return {
|
return {
|
||||||
get(id: string) {
|
get(id: string) {
|
||||||
return shortcuts().get(id)
|
return shortcuts().get(id)?.first
|
||||||
|
},
|
||||||
|
all(id: string) {
|
||||||
|
return shortcuts().get(id)?.all
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -232,17 +243,31 @@ function useCommands(): Accessor<readonly KeymapCommand[]> {
|
||||||
.getCommandEntries({
|
.getCommandEntries({
|
||||||
visibility: "reachable",
|
visibility: "reachable",
|
||||||
})
|
})
|
||||||
.map((entry) => ({
|
.map((entry) => {
|
||||||
id: entry.command.name,
|
const command = entry.command.opencode ?? {
|
||||||
title: typeof entry.command.title === "string" ? entry.command.title : entry.command.name,
|
id: entry.command.name,
|
||||||
description: typeof entry.command.desc === "string" ? entry.command.desc : undefined,
|
title: typeof entry.command.title === "string" ? entry.command.title : undefined,
|
||||||
group: typeof entry.command.category === "string" ? entry.command.category : undefined,
|
description: typeof entry.command.desc === "string" ? entry.command.desc : undefined,
|
||||||
palette: entry.command.namespace === "palette" ? true : undefined,
|
group: typeof entry.command.category === "string" ? entry.command.category : undefined,
|
||||||
slash: entry.command.slash,
|
enabled:
|
||||||
run: () => {
|
typeof entry.command.enabled === "boolean" || typeof entry.command.enabled === "function"
|
||||||
value.keymap.dispatchCommand(entry.command.name)
|
? (entry.command.enabled as boolean | (() => boolean))
|
||||||
},
|
: undefined,
|
||||||
})),
|
palette: entry.command.namespace === "palette" ? true : undefined,
|
||||||
|
slash: entry.command.slash,
|
||||||
|
hidden: typeof entry.command.hidden === "boolean" ? entry.command.hidden : undefined,
|
||||||
|
suggested:
|
||||||
|
typeof entry.command.suggested === "boolean" || typeof entry.command.suggested === "function"
|
||||||
|
? (entry.command.suggested as boolean | (() => boolean))
|
||||||
|
: undefined,
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...command,
|
||||||
|
run: () => {
|
||||||
|
value.keymap.dispatchCommand(entry.command.name)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,14 @@ import {
|
||||||
formatKeySequence as formatKeySequenceExtra,
|
formatKeySequence as formatKeySequenceExtra,
|
||||||
} from "@opentui/keymap/extras"
|
} from "@opentui/keymap/extras"
|
||||||
import { KeymapProvider, useKeymap, useKeymapSelector, useBindings } from "@opentui/keymap/solid"
|
import { KeymapProvider, useKeymap, useKeymapSelector, useBindings } from "@opentui/keymap/solid"
|
||||||
import { createMemo, type Accessor } from "solid-js"
|
import type { Accessor } from "solid-js"
|
||||||
import { useConfig } from "./config"
|
import { useConfig } from "./config"
|
||||||
import { TuiKeybind } from "./config/keybind"
|
import { TuiKeybind } from "./config/keybind"
|
||||||
|
import type { KeymapCommand } from "@opencode-ai/plugin/v2/tui/context"
|
||||||
|
|
||||||
declare module "@opentui/keymap" {
|
declare module "@opentui/keymap" {
|
||||||
interface Command {
|
interface Command {
|
||||||
|
opencode?: KeymapCommand
|
||||||
slash?: {
|
slash?: {
|
||||||
name: string
|
name: string
|
||||||
aliases?: string[]
|
aliases?: string[]
|
||||||
|
|
@ -39,13 +41,6 @@ export const useOpencodeKeymap = useKeymap
|
||||||
|
|
||||||
export type OpenTuiKeymap = ReturnType<typeof useKeymap>
|
export type OpenTuiKeymap = ReturnType<typeof useKeymap>
|
||||||
type OpencodeModeStack = ReturnType<typeof createOpencodeModeStack>
|
type OpencodeModeStack = ReturnType<typeof createOpencodeModeStack>
|
||||||
type CommandSlashEntry = {
|
|
||||||
display: string
|
|
||||||
description?: string
|
|
||||||
aliases?: string[]
|
|
||||||
onSelect: () => void
|
|
||||||
}
|
|
||||||
type RegisteredCommand = ReturnType<OpenTuiKeymap["getCommands"]>[number]
|
|
||||||
type BindingLookup = {
|
type BindingLookup = {
|
||||||
get(command: string): readonly Binding<Renderable, KeyEvent>[]
|
get(command: string): readonly Binding<Renderable, KeyEvent>[]
|
||||||
}
|
}
|
||||||
|
|
@ -54,10 +49,6 @@ type ResolvedKeymapConfig = FormatConfig & ({ leader: { timeout: number } } | {
|
||||||
|
|
||||||
const modeStacks = new WeakMap<OpenTuiKeymap, OpencodeModeStack>()
|
const modeStacks = new WeakMap<OpenTuiKeymap, OpencodeModeStack>()
|
||||||
|
|
||||||
function isVisiblePaletteCommand(command: RegisteredCommand) {
|
|
||||||
return command.hidden !== true && command.name !== COMMAND_PALETTE_COMMAND
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createOpencodeModeStack(keymap: OpenTuiKeymap) {
|
export function createOpencodeModeStack(keymap: OpenTuiKeymap) {
|
||||||
keymap.setData(OPENCODE_MODE_KEY, OPENCODE_BASE_MODE)
|
keymap.setData(OPENCODE_MODE_KEY, OPENCODE_BASE_MODE)
|
||||||
|
|
||||||
|
|
@ -268,32 +259,3 @@ export function useCommandShortcut(command: string): Accessor<string> {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useCommandSlashes(): Accessor<readonly CommandSlashEntry[]> {
|
|
||||||
const keymap = useOpencodeKeymap()
|
|
||||||
const entries = useKeymapSelector((keymap: OpenTuiKeymap) =>
|
|
||||||
keymap.getCommandEntries({
|
|
||||||
visibility: "reachable",
|
|
||||||
namespace: "palette",
|
|
||||||
filter: isVisiblePaletteCommand,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
return createMemo<CommandSlashEntry[]>(() =>
|
|
||||||
entries().flatMap((entry) => {
|
|
||||||
const slash = entry.command.slash
|
|
||||||
if (!slash) return []
|
|
||||||
return {
|
|
||||||
display: `/${slash.name}`,
|
|
||||||
description:
|
|
||||||
typeof entry.command.desc === "string"
|
|
||||||
? entry.command.desc
|
|
||||||
: typeof entry.command.title === "string"
|
|
||||||
? entry.command.title
|
|
||||||
: undefined,
|
|
||||||
aliases: slash.aliases?.map((alias) => `/${alias}`),
|
|
||||||
onSelect: () => keymap.dispatchCommand(entry.command.name),
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue