281 lines
9.2 KiB
TypeScript
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,
|
|
};
|
|
}
|