ttrpg-tools/src/components/journal/command-completions.ts

152 lines
4.7 KiB
TypeScript

/**
* Command completions — build the dropdown items for journal input
* autocomplete based on the current raw text and completions data.
*
* Pure function with no component dependencies.
*/
import type { CompletionItem } from "./command-parser";
import type { JournalCompletions } from "./completions";
export interface CompletionsContext {
data: JournalCompletions;
isGm: boolean;
}
export function buildCompletions(
raw: string,
ctx: CompletionsContext,
): CompletionItem[] {
const { data, isGm } = ctx;
const commands = [
{ label: "/roll", kind: "command" as const, insertText: "/roll " },
{ label: "/spark", kind: "command" as const, insertText: "/spark " },
{ label: "/link", kind: "command" as const, insertText: "/link " },
{ label: "/stat", kind: "command" as const, insertText: "/stat " },
];
// Show commands when user types / or starts typing a command name
if (raw.startsWith("/") && !raw.includes(" ")) {
const prefix = raw.toLowerCase();
let matches = commands.filter((c) =>
c.label.toLowerCase().startsWith(prefix),
);
if (!isGm) {
matches = matches.filter((c) => c.label === "/stat");
}
return matches.length > 0
? matches
: [{ label: "未知命令", kind: "no-results", insertText: "" }];
}
// After /roll — show dice suggestions
if (raw.startsWith("/roll ")) {
const prefix = raw.slice("/roll ".length).toLowerCase();
const matches = data.dice
.filter(
(d) =>
d.notation.toLowerCase().includes(prefix) ||
d.label.toLowerCase().includes(prefix),
)
.slice(0, 8);
if (matches.length === 0) {
return [{ label: "未找到骰子", kind: "no-results", insertText: "" }];
}
return matches.map((d) => ({
label: d.notation,
kind: "value" as const,
insertText: "/roll " + d.notation,
}));
}
// After /spark — show spark table suggestions
if (raw.startsWith("/spark ")) {
const prefix = raw.slice("/spark ".length).toLowerCase();
const matches = data.sparkTables
.filter(
(s) =>
s.slug.toLowerCase().includes(prefix) ||
s.label.toLowerCase().includes(prefix),
)
.slice(0, 8);
if (matches.length === 0) {
return [{ label: "未找到种子表", kind: "no-results", insertText: "" }];
}
return matches.map((s) => ({
label: `${s.slug} (${s.notation})`,
kind: "value" as const,
insertText: `/spark ${s.slug}`,
}));
}
// After /link — show article and heading suggestions
if (raw.startsWith("/link ")) {
const prefix = raw.slice("/link ".length).toLowerCase();
const matches = data.links
.filter(
(l) =>
l.path.toLowerCase().includes(prefix) ||
l.label.toLowerCase().includes(prefix),
)
.slice(0, 8);
if (matches.length === 0) {
return [{ label: "未找到链接", kind: "no-results", insertText: "" }];
}
return matches.map((l) => {
const insert = l.section
? `/link ${l.path}#${l.section}`
: `/link ${l.path}`;
return { label: l.label, kind: "value" as const, insertText: insert };
});
}
// After /stat — show subcommands or stat keys
if (raw.startsWith("/stat ")) {
const rest = raw.slice("/stat ".length).trim();
if (!rest || rest.length < 3) {
const subs = ["set", "del", "roll"];
const matches = subs.filter((s) => s.startsWith(rest.toLowerCase()));
if (matches.length === 0)
return [{ label: "set/del/roll", kind: "no-results", insertText: "" }];
return matches.map((s) => ({
label: `/stat ${s}`,
kind: "value" as const,
insertText: `/stat ${s} `,
}));
}
if (rest.startsWith("set ")) {
const prefix = rest.slice("set ".length).toLowerCase();
const matches = data.stats
.filter((s) => s.key.toLowerCase().includes(prefix))
.slice(0, 8);
if (matches.length === 0)
return [{ label: "未找到属性", kind: "no-results", insertText: "" }];
return matches.map((s) => ({
label: `${s.key} (${s.label})`,
kind: "value" as const,
insertText: `/stat set ${s.key}=`,
}));
}
if (rest.startsWith("del ") || rest.startsWith("roll ")) {
const [cmd, ...restParts] = rest.split(" ");
const prefix = restParts.join(" ").toLowerCase();
const matches = data.stats
.filter((s) => s.key.toLowerCase().startsWith(prefix))
.slice(0, 8);
if (matches.length === 0)
return [{ label: "未找到属性", kind: "no-results", insertText: "" }];
return matches.map((s) => ({
label: `${s.key} (${s.label})`,
kind: "value" as const,
insertText: `/stat ${cmd} ${s.key}`,
}));
}
}
return [];
}