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; 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([]); const [selectedCompletion, setSelectedCompletionState] = createSignal(0); const [isFocused, setIsFocused] = createSignal(false); const [commandHistory, setCommandHistory] = createSignal([]); const [historyIndex, setHistoryIndex] = createSignal(-1); const [viewMode, setViewMode] = createSignal("history"); const [commands, setCommands] = createSignal(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, }; }