feat: implement command autocomplete for journal input
Refactor journal input command parsing into a dedicated module and add a `CompletionsDropdown` component to provide real-time suggestions for commands (/roll, /spark, /link, /stat) and their arguments.
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Command parser — parse a raw input string into a typed ParsedInput.
|
||||
*
|
||||
* Pure function with no component dependencies, so it can live in its own
|
||||
* module and keep JournalInput small.
|
||||
*/
|
||||
|
||||
export interface CompletionItem {
|
||||
label: string;
|
||||
kind: "command" | "value" | "no-results";
|
||||
insertText: string;
|
||||
}
|
||||
|
||||
export interface ParsedInput {
|
||||
type: "chat" | "roll" | "spark" | "link" | "stat";
|
||||
payload: Record<string, unknown>;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export function parseInput(raw: string): ParsedInput {
|
||||
if (raw.startsWith("/roll ")) {
|
||||
const notation = raw.slice("/roll ".length).trim();
|
||||
if (!notation)
|
||||
return { type: "roll", payload: {}, error: "需要骰子表达式" };
|
||||
return { type: "roll", payload: { notation, label: notation } };
|
||||
}
|
||||
|
||||
if (raw.startsWith("/spark ")) {
|
||||
const key = raw.slice("/spark ".length).trim();
|
||||
if (!key) return { type: "spark", payload: {}, error: "需要种子表键名" };
|
||||
return { type: "spark", payload: { key } };
|
||||
}
|
||||
|
||||
if (raw.startsWith("/link ")) {
|
||||
const arg = raw.slice("/link ".length).trim();
|
||||
if (!arg) return { type: "link", payload: {}, error: "需要路径" };
|
||||
const hashIdx = arg.indexOf("#");
|
||||
const path = hashIdx === -1 ? arg : arg.slice(0, hashIdx);
|
||||
const section =
|
||||
hashIdx === -1 ? undefined : arg.slice(hashIdx + 1) || undefined;
|
||||
return { type: "link", payload: { path, section } };
|
||||
}
|
||||
|
||||
if (raw.startsWith("/stat ")) {
|
||||
const arg = raw.slice("/stat ".length).trim();
|
||||
if (!arg)
|
||||
return { type: "stat", payload: {}, error: "需要 stat 命令 (set/del/roll)" };
|
||||
|
||||
if (arg.startsWith("set ")) {
|
||||
const rest = arg.slice("set ".length).trim();
|
||||
const eqIdx = rest.indexOf("=");
|
||||
if (eqIdx === -1)
|
||||
return { type: "stat", payload: {}, error: "格式: /stat set key=value" };
|
||||
const key = rest.slice(0, eqIdx).trim();
|
||||
const value = rest.slice(eqIdx + 1).trim();
|
||||
if (!key || !value)
|
||||
return { type: "stat", payload: {}, error: "key 和 value 不能为空" };
|
||||
return { type: "stat", payload: { action: "set", key, value } };
|
||||
}
|
||||
|
||||
if (arg.startsWith("del ")) {
|
||||
const key = arg.slice("del ".length).trim();
|
||||
if (!key)
|
||||
return { type: "stat", payload: {}, error: "格式: /stat del key" };
|
||||
return { type: "stat", payload: { action: "del", key } };
|
||||
}
|
||||
|
||||
if (arg.startsWith("roll ")) {
|
||||
const key = arg.slice("roll ".length).trim();
|
||||
if (!key)
|
||||
return { type: "stat", payload: {}, error: "格式: /stat roll key" };
|
||||
return { type: "stat", payload: { action: "roll", key } };
|
||||
}
|
||||
|
||||
return { type: "stat", payload: {}, error: "未知 stat 子命令: set/del/roll" };
|
||||
}
|
||||
|
||||
if (raw === "/roll" || raw === "/spark" || raw === "/link" || raw === "/stat") {
|
||||
return { type: "chat", payload: {}, error: "请补全命令" };
|
||||
}
|
||||
|
||||
return { type: "chat", payload: { text: raw } };
|
||||
}
|
||||
Reference in New Issue
Block a user