152 lines
4.7 KiB
TypeScript
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 [];
|
|
}
|