443 lines
12 KiB
TypeScript
443 lines
12 KiB
TypeScript
import { createSignal } from "solid-js";
|
||
import {
|
||
MdCommanderCommand,
|
||
CommanderEntry,
|
||
CompletionItem,
|
||
} from "../types";
|
||
import { rollSimple } from "./useDiceRoller";
|
||
|
||
// ==================== 默认命令 ====================
|
||
|
||
export const defaultCommands: Record<string, MdCommanderCommand> = {
|
||
help: {
|
||
command: "help",
|
||
description: "显示帮助信息或特定命令的帮助",
|
||
parameters: [
|
||
{
|
||
name: "cmd",
|
||
description: "要查询的命令名",
|
||
type: "enum",
|
||
values: [], // 运行时填充
|
||
required: false,
|
||
},
|
||
],
|
||
handler: (args) => {
|
||
const cmdName = args.params.cmd;
|
||
if (cmdName) {
|
||
return {
|
||
message: `命令:${cmdName}\n描述:${defaultCommands[cmdName]?.description || "无描述"}`,
|
||
type: "info",
|
||
};
|
||
}
|
||
const cmdList = Object.keys(defaultCommands).join(", ");
|
||
return {
|
||
message: `可用命令:${cmdList}`,
|
||
type: "info",
|
||
};
|
||
},
|
||
},
|
||
clear: {
|
||
command: "clear",
|
||
description: "清空命令历史",
|
||
handler: () => ({
|
||
message: "命令历史已清空",
|
||
type: "success",
|
||
}),
|
||
},
|
||
roll: {
|
||
command: "roll",
|
||
description: "掷骰子 - 支持骰池、修饰符和组合",
|
||
parameters: [
|
||
{
|
||
name: "formula",
|
||
description: "骰子公式,如 3d6+{d4,d8}kh2-5",
|
||
type: "string",
|
||
required: true,
|
||
},
|
||
],
|
||
handler: (args) => {
|
||
const formula = args.params.formula || "1d6";
|
||
const result = rollSimple(formula);
|
||
return {
|
||
message: result.text,
|
||
isHtml: result.isHtml,
|
||
type: result.text.startsWith("错误") ? "error" : "success",
|
||
};
|
||
},
|
||
},
|
||
};
|
||
|
||
// ==================== 工具函数 ====================
|
||
|
||
export function parseInput(input: string, commands?: Record<string, MdCommanderCommand>): {
|
||
command?: string;
|
||
params: Record<string, string>;
|
||
options: Record<string, string>;
|
||
incompleteParam?: { index: number; value: string };
|
||
} {
|
||
const result: {
|
||
command?: string;
|
||
params: Record<string, string>;
|
||
options: Record<string, string>;
|
||
incompleteParam?: { index: number; value: string };
|
||
} = {
|
||
params: {},
|
||
options: {},
|
||
};
|
||
|
||
const trimmed = input.trim();
|
||
if (!trimmed) return result;
|
||
|
||
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);
|
||
const value = part.slice(eqIndex + 1);
|
||
result.options[key] = value;
|
||
} else {
|
||
const key = part.slice(2);
|
||
if (i + 1 < parts.length && !parts[i + 1].startsWith("-")) {
|
||
result.options[key] = parts[i + 1];
|
||
i++;
|
||
} else {
|
||
// 未完成的选项
|
||
}
|
||
}
|
||
} 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;
|
||
}
|
||
|
||
export function getCompletions(
|
||
input: string,
|
||
commands: Record<string, MdCommanderCommand>,
|
||
): CompletionItem[] {
|
||
const trimmed = input.trim();
|
||
|
||
if (!trimmed || /^\s*$/.test(trimmed)) {
|
||
return Object.values(commands).map((cmd) => ({
|
||
label: cmd.command,
|
||
type: "command",
|
||
description: cmd.description,
|
||
insertText: cmd.command,
|
||
}));
|
||
}
|
||
|
||
const parsed = parseInput(trimmed, commands);
|
||
|
||
if (!parsed.command || !commands[parsed.command]) {
|
||
const commandCompletions = Object.values(commands)
|
||
.filter((cmd) => cmd.command.startsWith(parsed.command || ""))
|
||
.map((cmd) => ({
|
||
label: cmd.command,
|
||
type: "command" as "command",
|
||
description: cmd.description,
|
||
insertText: cmd.command,
|
||
}));
|
||
if (commandCompletions.length > 0) {
|
||
return commandCompletions;
|
||
}
|
||
}
|
||
|
||
const cmd = commands[parsed.command!];
|
||
if (!cmd) return [];
|
||
|
||
// 检查是否需要参数补全
|
||
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))
|
||
.map((opt) => ({
|
||
label: `--${opt.option}`,
|
||
type: "option",
|
||
description: opt.description,
|
||
insertText: `--${opt.option}=${opt.type === "boolean" ? "" : ""}`,
|
||
}));
|
||
}
|
||
|
||
export function getResultClass(
|
||
type?: "success" | "error" | "warning" | "info",
|
||
): string {
|
||
switch (type) {
|
||
case "success":
|
||
return "text-green-600";
|
||
case "error":
|
||
return "text-red-600";
|
||
case "warning":
|
||
return "text-yellow-600";
|
||
case "info":
|
||
default:
|
||
return "text-blue-600";
|
||
}
|
||
}
|
||
|
||
// ==================== Commander Hook ====================
|
||
|
||
export interface UseCommanderReturn {
|
||
inputValue: () => string;
|
||
entries: () => CommanderEntry[];
|
||
showCompletions: () => boolean;
|
||
completions: () => CompletionItem[];
|
||
selectedCompletion: () => number;
|
||
isFocused: () => boolean;
|
||
setInputValue: (v: string) => void;
|
||
setEntries: (updater: (prev: CommanderEntry[]) => CommanderEntry[]) => void;
|
||
setShowCompletions: (v: boolean) => void;
|
||
setSelectedCompletion: (v: number | ((prev: number) => number)) => void;
|
||
setIsFocused: (v: boolean) => void;
|
||
handleCommand: () => void;
|
||
updateCompletions: () => void;
|
||
acceptCompletion: () => void;
|
||
commands: Record<string, MdCommanderCommand>;
|
||
historyIndex: () => number;
|
||
setHistoryIndex: (v: number) => void;
|
||
commandHistory: () => string[];
|
||
navigateHistory: (direction: 'up' | 'down') => void;
|
||
}
|
||
|
||
export function useCommander(
|
||
customCommands?: Record<string, MdCommanderCommand>,
|
||
): UseCommanderReturn {
|
||
const [inputValue, setInputValue] = createSignal("");
|
||
const [entries, setEntries] = createSignal<CommanderEntry[]>([]);
|
||
const [showCompletions, setShowCompletions] = createSignal(false);
|
||
const [completions, setCompletions] = createSignal<CompletionItem[]>([]);
|
||
const [selectedCompletion, setSelectedCompletion] = createSignal(0);
|
||
const [isFocused, setIsFocused] = createSignal(false);
|
||
|
||
// 命令历史
|
||
const [commandHistory, setCommandHistory] = createSignal<string[]>([]);
|
||
const [historyIndex, setHistoryIndex] = createSignal(-1);
|
||
|
||
const commands = { ...defaultCommands, ...customCommands };
|
||
|
||
// 更新 help 命令的参数值
|
||
if (commands.help?.parameters?.[0]) {
|
||
commands.help.parameters[0].values = Object.keys(commands).filter(
|
||
(k) => k !== "help",
|
||
);
|
||
}
|
||
|
||
const handleCommand = () => {
|
||
const input = inputValue().trim();
|
||
if (!input) return;
|
||
|
||
const parsed = parseInput(input, commands);
|
||
const commandName = parsed.command;
|
||
const cmd = commands[commandName!];
|
||
|
||
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({ params: parsed.params, options: parsed.options });
|
||
} catch (e) {
|
||
result = {
|
||
message: `执行错误:${e instanceof Error ? e.message : String(e)}`,
|
||
type: "error",
|
||
};
|
||
}
|
||
} else {
|
||
result = { message: `命令 ${commandName} 已执行(无处理器)`, type: "info" };
|
||
}
|
||
|
||
const newEntry: CommanderEntry = {
|
||
id: Date.now().toString() + Math.random().toString(36).slice(2),
|
||
command: input,
|
||
args: parsed.options,
|
||
result,
|
||
timestamp: new Date(),
|
||
};
|
||
|
||
setEntries((prev) => [...prev, newEntry]);
|
||
|
||
// 添加到命令历史
|
||
setCommandHistory((prev) => [...prev, input]);
|
||
setHistoryIndex(-1); // 重置历史索引
|
||
|
||
setInputValue("");
|
||
setShowCompletions(false);
|
||
|
||
if (commandName === "clear") {
|
||
setEntries([]);
|
||
}
|
||
};
|
||
|
||
const updateCompletions = () => {
|
||
const input = inputValue();
|
||
const comps = getCompletions(input, commands);
|
||
setCompletions(comps);
|
||
setShowCompletions(comps.length > 0 && isFocused());
|
||
setSelectedCompletion(0);
|
||
};
|
||
|
||
const acceptCompletion = () => {
|
||
const idx = selectedCompletion();
|
||
const comp = completions()[idx];
|
||
if (!comp) return;
|
||
|
||
const input = inputValue();
|
||
const parsed = parseInput(input, commands);
|
||
|
||
let newValue: string;
|
||
if (comp.type === "command") {
|
||
newValue = comp.insertText + " ";
|
||
} else if (comp.type === "option") {
|
||
const base = parsed.command || "";
|
||
const existingOptions = Object.entries(parsed.options)
|
||
.map(([k, v]) => `--${k}=${v}`)
|
||
.join(" ");
|
||
newValue = `${base} ${existingOptions}${existingOptions ? " " : ""}${comp.insertText}`;
|
||
} 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 existingParams = Object.values(parsed.params).join(" ");
|
||
const existingOptions = Object.entries(parsed.options)
|
||
.map(([k, v]) => `--${k}=${v}`)
|
||
.join(" ");
|
||
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,
|
||
showCompletions,
|
||
completions,
|
||
selectedCompletion,
|
||
isFocused,
|
||
setInputValue,
|
||
setEntries,
|
||
setShowCompletions,
|
||
setSelectedCompletion,
|
||
setIsFocused,
|
||
handleCommand,
|
||
updateCompletions,
|
||
acceptCompletion,
|
||
commands,
|
||
historyIndex,
|
||
setHistoryIndex,
|
||
commandHistory,
|
||
navigateHistory,
|
||
};
|
||
}
|