From 28066c7fff1719fe1750e0250439c0a7be3cc443 Mon Sep 17 00:00:00 2001 From: hypercross Date: Sat, 28 Feb 2026 17:15:41 +0800 Subject: [PATCH] fix: dfeat: command parameters --- .../md-commander/hooks/useCommander.ts | 159 +++++++++++++++--- src/components/md-commander/index.tsx | 12 ++ src/components/md-commander/types.ts | 17 +- 3 files changed, 161 insertions(+), 27 deletions(-) diff --git a/src/components/md-commander/hooks/useCommander.ts b/src/components/md-commander/hooks/useCommander.ts index 057fb09..a53c3e8 100644 --- a/src/components/md-commander/hooks/useCommander.ts +++ b/src/components/md-commander/hooks/useCommander.ts @@ -45,16 +45,16 @@ export const defaultCommands: Record = { roll: { command: "roll", description: "掷骰子 - 支持骰池、修饰符和组合", - options: { - formula: { - option: "formula", + parameters: [ + { + name: "formula", description: "骰子公式,如 3d6+{d4,d8}kh2-5", type: "string", required: true, }, - }, + ], handler: (args) => { - const formula = args.formula || "1d6"; + const formula = args.params.formula || "1d6"; const { rollSimple } = useDiceRoller(); const result = rollSimple(formula); return { @@ -68,16 +68,19 @@ export const defaultCommands: Record = { // ==================== 工具函数 ==================== -export function parseInput(input: string): { +export function parseInput(input: string, commands?: Record): { command?: string; + params: Record; options: Record; - incompleteOption?: string; + incompleteParam?: { index: number; value: string }; } { const result: { command?: string; + params: Record; options: Record; - incompleteOption?: string; + incompleteParam?: { index: number; value: string }; } = { + params: {}, options: {}, }; @@ -87,14 +90,23 @@ export function parseInput(input: string): { const parts = trimmed.split(/\s+/); let i = 0; + // 获取命令 if (parts[0] && !parts[0].startsWith("-")) { result.command = parts[0]; i = 1; } + // 获取命令的参数定义 + const cmd = result.command ? commands?.[result.command] : undefined; + const paramDefs = cmd?.parameters || []; + let paramIndex = 0; + + // 解析参数和选项 while (i < parts.length) { const part = parts[i]; + if (part.startsWith("--")) { + // 选项 --key=value 或 --key value const eqIndex = part.indexOf("="); if (eqIndex !== -1) { const key = part.slice(2, eqIndex); @@ -106,13 +118,38 @@ export function parseInput(input: string): { result.options[key] = parts[i + 1]; i++; } else { - result.incompleteOption = key; + // 未完成的选项 } } + } else if (part.startsWith("-") && part.length === 2) { + // 短选项 -k value + const key = part.slice(1); + if (i + 1 < parts.length && !parts[i + 1].startsWith("-")) { + result.options[key] = parts[i + 1]; + i++; + } + } else { + // 位置参数 + if (paramIndex < paramDefs.length) { + const paramDef = paramDefs[paramIndex]; + result.params[paramDef.name] = part; + paramIndex++; + } } i++; } + // 检查是否有未完成的位置参数 + if (paramIndex < paramDefs.length) { + const lastPart = parts[parts.length - 1]; + if (lastPart && !lastPart.startsWith("-")) { + result.incompleteParam = { + index: paramIndex, + value: lastPart, + }; + } + } + return result; } @@ -131,7 +168,7 @@ export function getCompletions( })); } - const parsed = parseInput(trimmed); + const parsed = parseInput(trimmed, commands); if (!parsed.command || !commands[parsed.command]) { const commandCompletions = Object.values(commands) @@ -148,21 +185,40 @@ export function getCompletions( } const cmd = commands[parsed.command!]; - if (!cmd || !cmd.options) return []; + if (!cmd) return []; - if (parsed.incompleteOption) { - const option = cmd.options[parsed.incompleteOption]; - if (option?.type === "enum" && option.values) { - return option.values - .filter((v) => parsed.incompleteOption && v.startsWith(parsed.incompleteOption)) + // 检查是否需要参数补全 + const paramDefs = cmd.parameters || []; + const usedParams = Object.keys(parsed.params); + + // 如果还有未填的参数,提供参数值补全 + if (paramDefs.length > usedParams.length) { + const paramDef = paramDefs[usedParams.length]; + if (paramDef.type === "enum" && paramDef.values) { + const currentValue = parsed.incompleteParam?.value || ""; + return paramDef.values + .filter((v) => v.startsWith(currentValue)) .map((v) => ({ label: v, type: "value", + description: paramDef.description, insertText: v, })); } + // 其他类型的参数,显示提示 + if (parsed.incompleteParam) { + return [{ + label: `<${paramDef.name}>`, + type: "value", + description: `${paramDef.type}${paramDef.required !== false ? " (必填)" : ""}: ${paramDef.description || ""}`, + insertText: "", + }]; + } } + // 选项补全 + if (!cmd.options) return []; + const usedOptions = Object.keys(parsed.options); return Object.values(cmd.options) .filter((opt) => !usedOptions.includes(opt.option)) @@ -208,6 +264,10 @@ export interface UseCommanderReturn { updateCompletions: () => void; acceptCompletion: () => void; commands: Record; + historyIndex: () => number; + setHistoryIndex: (v: number) => void; + commandHistory: () => string[]; + navigateHistory: (direction: 'up' | 'down') => void; } export function useCommander( @@ -219,6 +279,10 @@ export function useCommander( const [completions, setCompletions] = createSignal([]); const [selectedCompletion, setSelectedCompletion] = createSignal(0); const [isFocused, setIsFocused] = createSignal(false); + + // 命令历史 + const [commandHistory, setCommandHistory] = createSignal([]); + const [historyIndex, setHistoryIndex] = createSignal(-1); const commands = { ...defaultCommands, ...customCommands }; @@ -233,17 +297,17 @@ export function useCommander( const input = inputValue().trim(); if (!input) return; - const parsed = parseInput(input); + const parsed = parseInput(input, commands); const commandName = parsed.command; const cmd = commands[commandName!]; - let result: { message: string; type?: "success" | "error" | "warning" | "info" }; + let result: { message: string; type?: "success" | "error" | "warning" | "info"; isHtml?: boolean }; if (!cmd) { result = { message: `未知命令:${commandName}`, type: "error" }; } else if (cmd.handler) { try { - result = cmd.handler(parsed.options); + result = cmd.handler({ params: parsed.params, options: parsed.options }); } catch (e) { result = { message: `执行错误:${e instanceof Error ? e.message : String(e)}`, @@ -263,6 +327,11 @@ export function useCommander( }; setEntries((prev) => [...prev, newEntry]); + + // 添加到命令历史 + setCommandHistory((prev) => [...prev, input]); + setHistoryIndex(-1); // 重置历史索引 + setInputValue(""); setShowCompletions(false); @@ -285,7 +354,7 @@ export function useCommander( if (!comp) return; const input = inputValue(); - const parsed = parseInput(input); + const parsed = parseInput(input, commands); let newValue: string; if (comp.type === "command") { @@ -296,24 +365,58 @@ export function useCommander( .map(([k, v]) => `--${k}=${v}`) .join(" "); newValue = `${base} ${existingOptions}${existingOptions ? " " : ""}${comp.insertText}`; - } else { - const optKey = parsed.incompleteOption; - if (optKey) { + } else if (comp.type === "value") { + // 参数值补全 + const cmd = parsed.command ? commands[parsed.command] : null; + const paramDefs = cmd?.parameters || []; + const usedParams = Object.keys(parsed.params); + + if (paramDefs.length > usedParams.length) { + // 当前参数的补全 const base = parsed.command || ""; - const otherOptions = Object.entries(parsed.options) - .filter(([k]) => k !== optKey) + const existingParams = Object.values(parsed.params).join(" "); + const existingOptions = Object.entries(parsed.options) .map(([k, v]) => `--${k}=${v}`) .join(" "); - newValue = `${base} --${optKey}=${comp.insertText}${otherOptions ? " " + otherOptions : ""}`; + newValue = `${base} ${existingParams}${existingParams ? " " : ""}${comp.insertText}${existingOptions ? " " + existingOptions : ""}`; } else { newValue = input; } + } else { + newValue = input; } setInputValue(newValue.trim()); setShowCompletions(false); }; + const navigateHistory = (direction: 'up' | 'down') => { + const history = commandHistory(); + if (history.length === 0) return; + + let newIndex = historyIndex(); + if (direction === 'up') { + // 向上浏览历史(更早的命令) + if (newIndex < history.length - 1) { + newIndex++; + } + } else { + // 向下浏览历史(更新的命令) + if (newIndex > 0) { + newIndex--; + } else { + // 回到当前输入 + setInputValue(""); + setHistoryIndex(-1); + return; + } + } + + setHistoryIndex(newIndex); + // 从历史末尾获取命令(最新的在前) + setInputValue(history[history.length - 1 - newIndex]); + }; + return { inputValue, entries, @@ -330,5 +433,9 @@ export function useCommander( updateCompletions, acceptCompletion, commands, + historyIndex, + setHistoryIndex, + commandHistory, + navigateHistory, }; } diff --git a/src/components/md-commander/index.tsx b/src/components/md-commander/index.tsx index 08d73d8..c69b45c 100644 --- a/src/components/md-commander/index.tsx +++ b/src/components/md-commander/index.tsx @@ -40,6 +40,18 @@ customElement( } } + // 补全未打开时,使用上下键浏览历史 + if (e.key === "ArrowUp" && !commander.showCompletions()) { + e.preventDefault(); + commander.navigateHistory("up"); + return; + } + if (e.key === "ArrowDown" && !commander.showCompletions()) { + e.preventDefault(); + commander.navigateHistory("down"); + return; + } + if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); commander.handleCommand(); diff --git a/src/components/md-commander/types.ts b/src/components/md-commander/types.ts index 143c48b..ef1afe4 100644 --- a/src/components/md-commander/types.ts +++ b/src/components/md-commander/types.ts @@ -10,14 +10,29 @@ export interface MdCommanderProps { export interface MdCommanderCommand { command: string; description?: string; + parameters?: MdCommanderParameter[]; options?: Record; - handler?: (args: Record) => { + handler?: (args: { + params: Record; + options: Record; + }) => { message: string; type?: "success" | "error" | "info" | "warning"; isHtml?: boolean; }; } +export interface MdCommanderParameter { + name: string; + description?: string; + type: MdCommanderOptionType; + required?: boolean; + default?: string; + min?: number; + max?: number; + values?: string[]; +} + export type MdCommanderOptionType = | "string" | "number"