Files
ttrpg-tools/src/components/md-commander/hooks/useCommander.ts
T
2026-03-01 13:54:48 +08:00

281 lines
9.2 KiB
TypeScript

import { createSignal } from "solid-js";
import type {
CommanderEntry,
CompletionItem,
TrackerItem,
TrackerAttribute,
TrackerViewMode,
MdCommanderCommandMap,
TrackerCommand,
} from "../types";
import { parseInput, getCompletions } from "./completions";
import {
addTrackerItem,
getTrackerHistory,
getTrackerItems,
moveTrackerItem,
removeTrackerClass,
removeTrackerItem,
updateTrackerAttribute,
updateTrackerClasses,
} from "../stores";
import { addEntry, getEntries, clearEntries } from "../stores/entriesStore";
// ==================== Commander Hook ====================
export interface UseCommanderReturn {
inputValue: () => string;
entries: () => CommanderEntry[];
showCompletions: () => boolean;
completions: () => CompletionItem[];
selectedCompletion: () => number;
isFocused: () => boolean;
setInputValue: (v: string) => void;
setShowCompletions: (v: boolean) => void;
setSelectedCompletion: (v: number | ((prev: number) => number)) => void;
setIsFocused: (v: boolean) => void;
handleCommand: () => void;
updateCompletions: () => void;
acceptCompletion: () => void;
commands: () => MdCommanderCommandMap;
setCommands: (cmds: MdCommanderCommandMap) => void;
historyIndex: () => number;
setHistoryIndex: (v: number) => void;
commandHistory: () => string[];
navigateHistory: (direction: "up" | "down") => void;
// Tracker 相关
viewMode: () => TrackerViewMode;
setViewMode: (mode: TrackerViewMode) => void;
trackerItems: () => TrackerItem[];
trackerHistory: () => TrackerCommand[];
addTrackerItem: (item: Omit<TrackerItem, "id">) => TrackerItem;
removeTrackerItem: (emmet: string) => boolean;
removeTrackerItemByIndex: (index: number) => void;
updateTrackerAttribute: (emmet: string, attrName: string, attr: TrackerAttribute) => boolean;
updateTrackerAttributeByIndex: (index: number, attrName: string, attr: TrackerAttribute) => void;
updateTrackerClassesByIndex: (index: number, classes: string[]) => void;
moveTrackerItem: (emmet: string, direction: "up" | "down") => boolean;
moveTrackerItemByIndex: (index: number, direction: "up" | "down") => void;
removeTrackerItemClass: (emmet: string, className: string) => boolean;
removeTrackerItemClassByIndex: (index: number, className: string) => void;
}
export function useCommander(initialCommands?: MdCommanderCommandMap): UseCommanderReturn {
const [inputValue, setInputValue] = createSignal("");
const [showCompletions, setShowCompletions] = createSignal(false);
const [completions, setCompletions] = createSignal<CompletionItem[]>([]);
const [selectedCompletion, setSelectedCompletionState] = createSignal(0);
const [isFocused, setIsFocused] = createSignal(false);
const [commandHistory, setCommandHistory] = createSignal<string[]>([]);
const [historyIndex, setHistoryIndex] = createSignal(-1);
const [viewMode, setViewMode] = createSignal<TrackerViewMode>("history");
const [commands, setCommands] = createSignal<MdCommanderCommandMap>(initialCommands || {});
// 从 store 获取 entries
const entries = getEntries;
// ==================== 命令执行 ====================
const handleCommand = () => {
const input = inputValue().trim();
if (!input) return;
const cmds = commands();
const parsed = parseInput(input, cmds);
const commandName = parsed.command;
const cmd = cmds[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 }, cmds);
} 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(),
};
// 使用 store 添加记录
addEntry(newEntry);
setCommandHistory((prev) => [...prev, input]);
setHistoryIndex(-1);
setInputValue("");
setShowCompletions(false);
if (commandName === "clear") {
clearEntries();
}
};
// ==================== 自动补全 ====================
const updateCompletions = () => {
const comps = getCompletions(inputValue(), commands());
setCompletions(comps);
setShowCompletions(comps.length > 0 && isFocused());
setSelectedCompletionState(0);
};
const setSelectedCompletion = (v: number | ((prev: number) => number)) => {
const maxIdx = completions().length - 1;
if (typeof v === "function") {
setSelectedCompletionState((prev) => {
const next = v(prev);
return Math.max(0, Math.min(next, maxIdx));
});
} else {
setSelectedCompletionState(Math.max(0, Math.min(v, maxIdx)));
}
};
const acceptCompletion = () => {
const idx = selectedCompletion();
const comps = completions();
const comp = comps[Math.min(idx, comps.length - 1)];
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 existingOptions = Object.entries(parsed.options)
.map(([k, v]) => `--${k}=${v}`)
.join(" ");
newValue = `${parsed.command || ""} ${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);
const isTypingLastParam = paramDefs.length === usedParams.length && !input.endsWith(" ");
if (isTypingLastParam) {
const parts = input.trim().split(/\s+/);
parts.pop();
newValue = `${parsed.command || ""}${parts.length > 1 ? " " + parts.slice(1).join(" ") : ""} ${comp.insertText}`;
} else if (paramDefs.length > usedParams.length) {
const existingParams = Object.values(parsed.params).join(" ");
newValue = `${parsed.command || ""} ${existingParams}${existingParams ? " " : ""}${comp.insertText}`;
} 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]);
};
// ==================== Tracker 操作 ====================
const getEmmetFromIndex = (index: number): string | null => {
const items = getTrackerItems();
if (index < 0 || index >= items.length) return null;
const item = items[index];
return `${item.tag}${item.id ? "#" + item.id : ""}${item.classes.length > 0 ? "." + item.classes.join(".") : ""}`;
};
const removeTrackerItemByIndex = (index: number) => {
const emmet = getEmmetFromIndex(index);
if (emmet) removeTrackerItem(emmet);
};
const updateTrackerAttributeByIndex = (index: number, attrName: string, attr: TrackerAttribute) => {
const emmet = getEmmetFromIndex(index);
if (emmet) updateTrackerAttribute(emmet, attrName, attr);
};
const updateTrackerClassesByIndex = (index: number, classes: string[]) => {
const emmet = getEmmetFromIndex(index);
if (emmet) updateTrackerClasses(emmet, classes);
};
const moveTrackerItemByIndex = (index: number, direction: "up" | "down") => {
const emmet = getEmmetFromIndex(index);
if (emmet) moveTrackerItem(emmet, direction);
};
const removeTrackerItemClassByIndex = (index: number, className: string) => {
const emmet = getEmmetFromIndex(index);
if (emmet) removeTrackerClass(emmet, className);
};
return {
inputValue,
entries,
showCompletions,
completions,
selectedCompletion,
isFocused,
setInputValue,
setShowCompletions,
setSelectedCompletion,
setIsFocused,
handleCommand,
updateCompletions,
acceptCompletion,
commands,
setCommands,
historyIndex,
setHistoryIndex,
commandHistory,
navigateHistory,
viewMode,
setViewMode,
trackerItems: getTrackerItems,
trackerHistory: getTrackerHistory,
addTrackerItem,
removeTrackerItem,
removeTrackerItemByIndex,
updateTrackerAttribute,
updateTrackerAttributeByIndex,
updateTrackerClassesByIndex,
moveTrackerItem,
moveTrackerItemByIndex,
removeTrackerItemClass: removeTrackerClass,
removeTrackerItemClassByIndex,
};
}