ttrpg-tools/src/components/md-commander/hooks/completions.ts

183 lines
5.2 KiB
TypeScript

import type { MdCommanderCommandMap, CompletionItem } from "../types";
export interface ParsedCommand {
command?: string;
params: Record<string, string>;
options: Record<string, string>;
incompleteParam?: { index: number; value: string };
}
/**
* 解析命令行输入
*/
export function parseInput(input: string, commands?: MdCommanderCommandMap): ParsedCommand {
const result: ParsedCommand = { 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("--")) {
const eqIndex = part.indexOf("=");
if (eqIndex !== -1) {
result.options[part.slice(2, eqIndex)] = part.slice(eqIndex + 1);
} else {
const key = part.slice(2);
if (i + 1 < parts.length && !parts[i + 1].startsWith("-")) {
result.options[key] = parts[i + 1];
i++;
}
}
} else if (part.startsWith("-") && part.length === 2) {
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) {
result.params[paramDefs[paramIndex].name] = part;
paramIndex++;
}
i++;
}
// 检查是否有未完成的位置参数
if (paramIndex < paramDefs.length) {
const lastPart = parts[parts.length - 1];
if (lastPart && !lastPart.startsWith("-") && parts.length > 1) {
result.incompleteParam = { index: paramIndex, value: lastPart };
}
}
return result;
}
/**
* 获取自动补全建议
*/
export function getCompletions(input: string, commands: MdCommanderCommandMap): 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]) {
return Object.values(commands)
.filter((cmd) => cmd.command.startsWith(parsed.command || ""))
.map((cmd) => ({
label: cmd.command,
type: "command",
description: cmd.description,
insertText: cmd.command,
}));
}
const cmd = commands[parsed.command];
if (!cmd) return [];
const paramDefs = cmd.parameters || [];
const usedParams = Object.keys(parsed.params);
// 判断是否正在输入最后一个参数
const isTypingLastParam = paramDefs.length === usedParams.length &&
trimmed.length > 0 &&
!trimmed.endsWith(" ") &&
!trimmed.split(/\s+/)[trimmed.split(/\s+/).length - 1].startsWith("-");
// 参数补全
if (paramDefs.length > usedParams.length || parsed.incompleteParam || isTypingLastParam) {
let paramIndex = parsed.incompleteParam?.index ?? (isTypingLastParam ? paramDefs.length - 1 : usedParams.length);
const paramDef = paramDefs[paramIndex];
if (!paramDef) return [];
let currentValue = parsed.incompleteParam?.value || "";
if (isTypingLastParam && !parsed.incompleteParam) {
const parts = trimmed.split(/\s+/);
currentValue = parts[parts.length - 1] || "";
}
// 模板补全
if (paramDef.templates) {
return paramDef.templates
.filter((t) => t.label.toLowerCase().includes(currentValue.toLowerCase()))
.map((t) => ({
label: t.label,
type: "value",
description: t.description,
insertText: t.insertText,
}));
}
// 枚举补全
if (paramDef.type === "enum" && paramDef.values) {
return paramDef.values
.filter((v) => v.startsWith(currentValue))
.map((v) => ({
label: v,
type: "value",
description: paramDef.description,
insertText: v,
}));
}
// 其他类型显示提示
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" ? "" : ""}`,
}));
}
/**
* 根据结果类型获取 CSS 类名
*/
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";
default: return "text-blue-600";
}
}