refactor: impl

This commit is contained in:
hypercross 2026-03-01 12:33:55 +08:00
parent 3e14bff1e9
commit c2c3956a82
7 changed files with 295 additions and 429 deletions

View File

@ -1,6 +1,7 @@
import type { MdCommanderCommand, MdCommanderCommandMap } from "../types"; import type { MdCommanderCommand, MdCommanderCommandMap } from "../types";
export const helpCommand: MdCommanderCommand = { export function setupHelpCommand(commands: MdCommanderCommandMap): MdCommanderCommand {
return {
command: "help", command: "help",
description: "显示帮助信息或特定命令的帮助", description: "显示帮助信息或特定命令的帮助",
parameters: [ parameters: [
@ -8,30 +9,23 @@ export const helpCommand: MdCommanderCommand = {
name: "cmd", name: "cmd",
description: "要查询的命令名", description: "要查询的命令名",
type: "enum", type: "enum",
values: [], // 运行时填充 values: Object.keys(commands).filter((k) => k !== "help"),
required: false, required: false,
}, },
], ],
handler: (args, commands) => { handler: (args, cmds) => {
const cmdName = args.params.cmd; const cmdName = args.params.cmd;
if (cmdName && commands?.[cmdName]) { if (cmdName && cmds?.[cmdName]) {
return { return {
message: `命令:${cmdName}\n描述${commands[cmdName]?.description || "无描述"}`, message: `命令:${cmdName}\n描述${cmds[cmdName]?.description || "无描述"}`,
type: "info", type: "info",
}; };
} }
const cmdList = Object.keys(commands || {}).filter(k => k !== "help").join(", "); const cmdList = Object.keys(cmds || {}).filter((k) => k !== "help").join(", ");
return { return {
message: `可用命令:${cmdList}`, message: `可用命令:${cmdList}`,
type: "info", type: "info",
}; };
}, },
}; };
export function setupHelpCommand(commands: MdCommanderCommandMap): MdCommanderCommand {
const cmd = { ...helpCommand };
if (cmd.parameters?.[0]) {
cmd.parameters[0].values = Object.keys(commands).filter(k => k !== "help");
}
return cmd;
} }

View File

@ -1,4 +1,4 @@
export { helpCommand, setupHelpCommand } from "./help"; export { setupHelpCommand } from "./help";
export { clearCommand } from "./clear"; export { clearCommand } from "./clear";
export { rollCommand } from "./roll"; export { rollCommand } from "./roll";
export { trackCommand, untrackCommand, listTrackCommand } from "./tracker"; export { trackCommand, untrackCommand, listTrackCommand } from "./tracker";

View File

@ -0,0 +1,182 @@
import type { MdCommanderCommandMap, CompletionItem } from "../types";
export interface ParsedCommand {
command?: string;
params: Record<string, string>;
options: Record<string, string>;
incompleteParam?: { index: number; value: string };
}
/**
*
*/
export function parseInput(input: string, commands?: MdCommanderCommandMap): ParsedCommand {
const result: ParsedCommand = { params: {}, options: {} };
const trimmed = input.trim();
if (!trimmed) return result;
const parts = trimmed.split(/\s+/);
let i = 0;
// 获取命令
if (parts[0] && !parts[0].startsWith("-")) {
result.command = parts[0];
i = 1;
}
// 获取命令的参数定义
const cmd = result.command ? commands?.[result.command] : undefined;
const paramDefs = cmd?.parameters || [];
let paramIndex = 0;
// 解析参数和选项
while (i < parts.length) {
const part = parts[i];
if (part.startsWith("--")) {
const eqIndex = part.indexOf("=");
if (eqIndex !== -1) {
result.options[part.slice(2, eqIndex)] = part.slice(eqIndex + 1);
} else {
const key = part.slice(2);
if (i + 1 < parts.length && !parts[i + 1].startsWith("-")) {
result.options[key] = parts[i + 1];
i++;
}
}
} else if (part.startsWith("-") && part.length === 2) {
const key = part.slice(1);
if (i + 1 < parts.length && !parts[i + 1].startsWith("-")) {
result.options[key] = parts[i + 1];
i++;
}
} else if (paramIndex < paramDefs.length) {
result.params[paramDefs[paramIndex].name] = part;
paramIndex++;
}
i++;
}
// 检查是否有未完成的位置参数
if (paramIndex < paramDefs.length) {
const lastPart = parts[parts.length - 1];
if (lastPart && !lastPart.startsWith("-") && parts.length > 1) {
result.incompleteParam = { index: paramIndex, value: lastPart };
}
}
return result;
}
/**
*
*/
export function getCompletions(input: string, commands: MdCommanderCommandMap): CompletionItem[] {
const trimmed = input.trim();
// 空输入时返回所有命令
if (!trimmed || /^\s*$/.test(trimmed)) {
return Object.values(commands).map((cmd) => ({
label: cmd.command,
type: "command",
description: cmd.description,
insertText: cmd.command,
}));
}
const parsed = parseInput(trimmed, commands);
// 命令补全
if (!parsed.command || !commands[parsed.command]) {
return Object.values(commands)
.filter((cmd) => cmd.command.startsWith(parsed.command || ""))
.map((cmd) => ({
label: cmd.command,
type: "command",
description: cmd.description,
insertText: cmd.command,
}));
}
const cmd = commands[parsed.command];
if (!cmd) return [];
const paramDefs = cmd.parameters || [];
const usedParams = Object.keys(parsed.params);
// 判断是否正在输入最后一个参数
const isTypingLastParam = paramDefs.length === usedParams.length &&
trimmed.length > 0 &&
!trimmed.endsWith(" ") &&
!trimmed.split(/\s+/)[trimmed.split(/\s+/).length - 1].startsWith("-");
// 参数补全
if (paramDefs.length > usedParams.length || parsed.incompleteParam || isTypingLastParam) {
let paramIndex = parsed.incompleteParam?.index ?? (isTypingLastParam ? paramDefs.length - 1 : usedParams.length);
const paramDef = paramDefs[paramIndex];
if (!paramDef) return [];
let currentValue = parsed.incompleteParam?.value || "";
if (isTypingLastParam && !parsed.incompleteParam) {
const parts = trimmed.split(/\s+/);
currentValue = parts[parts.length - 1] || "";
}
// 模板补全
if (paramDef.templates) {
return paramDef.templates
.filter((t) => t.label.toLowerCase().includes(currentValue.toLowerCase()))
.map((t) => ({
label: t.label,
type: "value",
description: t.description,
insertText: t.insertText,
}));
}
// 枚举补全
if (paramDef.type === "enum" && paramDef.values) {
return paramDef.values
.filter((v) => v.startsWith(currentValue))
.map((v) => ({
label: v,
type: "value",
description: paramDef.description,
insertText: v,
}));
}
// 其他类型显示提示
return [{
label: `<${paramDef.name}>`,
type: "value",
description: `${paramDef.type}${paramDef.required !== false ? " (必填)" : ""}: ${paramDef.description || ""}`,
insertText: "",
}];
}
// 选项补全
if (!cmd.options) return [];
const usedOptions = Object.keys(parsed.options);
return Object.values(cmd.options)
.filter((opt) => !usedOptions.includes(opt.option))
.map((opt) => ({
label: `--${opt.option}`,
type: "option",
description: opt.description,
insertText: `--${opt.option}=${opt.type === "boolean" ? "" : ""}`,
}));
}
/**
* CSS
*/
export function getResultClass(type?: "success" | "error" | "warning" | "info"): string {
switch (type) {
case "success": return "text-green-600";
case "error": return "text-red-600";
case "warning": return "text-yellow-600";
default: return "text-blue-600";
}
}

View File

@ -1,13 +1,6 @@
export { useCommander, defaultCommands, parseInput, getCompletions, getResultClass } from './useCommander'; export { useCommander, initializeCommands, defaultCommands } from "./useCommander";
export type { UseCommanderReturn } from './useCommander'; export type { UseCommanderReturn } from "./useCommander";
export { rollFormula, rollSimple } from './useDiceRoller'; export { rollSimple, rollFormula } from "./useDiceRoller";
export * from './dice-engine'; export type { DiceRollerResult } from "./useDiceRoller";
export { parseInput, getCompletions, getResultClass } from "./completions";
// Tracker 相关导出 export type { ParsedCommand } from "./completions";
export type {
TrackerItem,
TrackerAttribute,
TrackerAttributeType,
TrackerCommand,
TrackerViewMode,
} from '../types';

View File

@ -1,5 +1,5 @@
import { createSignal } from "solid-js"; import { createSignal } from "solid-js";
import { import type {
MdCommanderCommand, MdCommanderCommand,
MdCommanderCommandMap, MdCommanderCommandMap,
CommanderEntry, CommanderEntry,
@ -9,8 +9,8 @@ import {
TrackerCommand, TrackerCommand,
TrackerViewMode, TrackerViewMode,
} from "../types"; } from "../types";
import { rollSimple } from "./useDiceRoller"; import { parseInput, getCompletions } from "./completions";
import { helpCommand, setupHelpCommand, clearCommand, rollCommand, trackCommand, untrackCommand, listTrackCommand } from "../commands"; import { setupHelpCommand, clearCommand, rollCommand, trackCommand, untrackCommand, listTrackCommand } from "../commands";
import { import {
addTrackerItem as addTracker, addTrackerItem as addTracker,
removeTrackerItem as removeTracker, removeTrackerItem as removeTracker,
@ -26,7 +26,7 @@ import {
// ==================== 默认命令 ==================== // ==================== 默认命令 ====================
export const defaultCommands: MdCommanderCommandMap = { export const defaultCommands: MdCommanderCommandMap = {
help: setupHelpCommand({}), help: { command: "help" }, // 占位,稍后初始化
clear: clearCommand, clear: clearCommand,
roll: rollCommand, roll: rollCommand,
track: trackCommand, track: trackCommand,
@ -34,228 +34,11 @@ export const defaultCommands: MdCommanderCommandMap = {
list: listTrackCommand, list: listTrackCommand,
}; };
// 初始化默认命令 // 初始化默认命令(包括 help
Object.keys(defaultCommands).forEach(key => { export function initializeCommands(customCommands?: MdCommanderCommandMap): MdCommanderCommandMap {
if (key === "help") { const commands = { ...defaultCommands, ...customCommands };
const help = setupHelpCommand(defaultCommands); commands.help = setupHelpCommand(commands);
defaultCommands.help = help; return commands;
}
});
// ==================== 工具函数 ====================
export function parseInput(input: string, commands?: MdCommanderCommandMap): {
command?: string;
params: Record<string, string>;
options: Record<string, string>;
incompleteParam?: { index: number; value: string };
} {
const result: {
command?: string;
params: Record<string, string>;
options: Record<string, string>;
incompleteParam?: { index: number; value: string };
} = {
params: {},
options: {},
};
const trimmed = input.trim();
if (!trimmed) return result;
const parts = trimmed.split(/\s+/);
let i = 0;
// 获取命令
if (parts[0] && !parts[0].startsWith("-")) {
result.command = parts[0];
i = 1;
}
// 获取命令的参数定义
const cmd = result.command ? commands?.[result.command] : undefined;
const paramDefs = cmd?.parameters || [];
let paramIndex = 0;
// 解析参数和选项
while (i < parts.length) {
const part = parts[i];
if (part.startsWith("--")) {
// 选项 --key=value 或 --key value
const eqIndex = part.indexOf("=");
if (eqIndex !== -1) {
const key = part.slice(2, eqIndex);
const value = part.slice(eqIndex + 1);
result.options[key] = value;
} else {
const key = part.slice(2);
if (i + 1 < parts.length && !parts[i + 1].startsWith("-")) {
result.options[key] = parts[i + 1];
i++;
} else {
// 未完成的选项
}
}
} else if (part.startsWith("-") && part.length === 2) {
// 短选项 -k value
const key = part.slice(1);
if (i + 1 < parts.length && !parts[i + 1].startsWith("-")) {
result.options[key] = parts[i + 1];
i++;
}
} else {
// 位置参数
if (paramIndex < paramDefs.length) {
const paramDef = paramDefs[paramIndex];
result.params[paramDef.name] = part;
paramIndex++;
}
}
i++;
}
// 检查是否有未完成的位置参数
if (paramIndex < paramDefs.length) {
const lastPart = parts[parts.length - 1];
// 只有当最后一个部分不是命令本身时,才设置为未完成参数
if (lastPart && !lastPart.startsWith("-") && parts.length > 1) {
result.incompleteParam = {
index: paramIndex,
value: lastPart,
};
}
}
return result;
}
export function getCompletions(
input: string,
commands: MdCommanderCommandMap,
): CompletionItem[] {
const trimmed = input.trim();
if (!trimmed || /^\s*$/.test(trimmed)) {
return Object.values(commands).map((cmd) => ({
label: cmd.command,
type: "command",
description: cmd.description,
insertText: cmd.command,
}));
}
const parsed = parseInput(trimmed, commands);
if (!parsed.command || !commands[parsed.command]) {
const commandCompletions = Object.values(commands)
.filter((cmd) => cmd.command.startsWith(parsed.command || ""))
.map((cmd) => ({
label: cmd.command,
type: "command" as "command",
description: cmd.description,
insertText: cmd.command,
}));
if (commandCompletions.length > 0) {
return commandCompletions;
}
}
const cmd = commands[parsed.command!];
if (!cmd) return [];
// 检查是否需要参数补全
const paramDefs = cmd.parameters || [];
const usedParams = Object.keys(parsed.params);
// 判断是否正在输入最后一个参数(没有尾随空格)
const isTypingLastParam = paramDefs.length === usedParams.length &&
trimmed.length > 0 &&
!trimmed.endsWith(" ") &&
!trimmed.split(/\s+/)[trimmed.split(/\s+/).length - 1].startsWith("-");
// 如果还有未填的参数,或者当前参数可能未完成
if (paramDefs.length > usedParams.length || parsed.incompleteParam || isTypingLastParam) {
// 确定当前参数的索引
let paramIndex: number;
if (parsed.incompleteParam !== undefined) {
paramIndex = parsed.incompleteParam.index;
} else if (isTypingLastParam) {
// 正在编辑最后一个参数
paramIndex = paramDefs.length - 1;
} else {
paramIndex = usedParams.length;
}
const paramDef = paramDefs[paramIndex];
if (!paramDef) return [];
// 获取当前输入值
let currentValue = parsed.incompleteParam?.value || "";
if (isTypingLastParam && !parsed.incompleteParam) {
const parts = trimmed.split(/\s+/);
currentValue = parts[parts.length - 1] || "";
}
// 模板补全
if (paramDef.templates) {
return paramDef.templates
.filter((t) => t.label.toLowerCase().includes(currentValue.toLowerCase()))
.map((t) => ({
label: t.label,
type: "value" as "value",
description: t.description,
insertText: t.insertText,
}));
}
if (paramDef.type === "enum" && paramDef.values) {
return paramDef.values
.filter((v) => v.startsWith(currentValue))
.map((v) => ({
label: v,
type: "value" as "value",
description: paramDef.description,
insertText: v,
}));
}
// 其他类型的参数,显示提示
return [{
label: `<${paramDef.name}>`,
type: "value" as "value",
description: `${paramDef.type}${paramDef.required !== false ? " (必填)" : ""}: ${paramDef.description || ""}`,
insertText: "",
}];
}
// 选项补全
if (!cmd.options) return [];
const usedOptions = Object.keys(parsed.options);
return Object.values(cmd.options)
.filter((opt) => !usedOptions.includes(opt.option))
.map((opt) => ({
label: `--${opt.option}`,
type: "option",
description: opt.description,
insertText: `--${opt.option}=${opt.type === "boolean" ? "" : ""}`,
}));
}
export function getResultClass(
type?: "success" | "error" | "warning" | "info",
): string {
switch (type) {
case "success":
return "text-green-600";
case "error":
return "text-red-600";
case "warning":
return "text-yellow-600";
case "info":
default:
return "text-blue-600";
}
} }
// ==================== Commander Hook ==================== // ==================== Commander Hook ====================
@ -281,50 +64,37 @@ export interface UseCommanderReturn {
commandHistory: () => string[]; commandHistory: () => string[];
navigateHistory: (direction: 'up' | 'down') => void; navigateHistory: (direction: 'up' | 'down') => void;
// Tracker 相关 // Tracker 相关 - 直接暴露 store API
viewMode: () => TrackerViewMode; viewMode: () => TrackerViewMode;
setViewMode: (mode: TrackerViewMode) => void; setViewMode: (mode: TrackerViewMode) => void;
trackerItems: () => TrackerItem[]; trackerItems: () => TrackerItem[];
setTrackerItems: (updater: (prev: TrackerItem[]) => TrackerItem[]) => void;
trackerHistory: () => TrackerCommand[]; trackerHistory: () => TrackerCommand[];
addTrackerItem: (item: Omit<TrackerItem, 'id'>) => void; addTrackerItem: typeof addTracker;
removeTrackerItem: (emmet: string) => boolean; removeTrackerItem: typeof removeTracker;
removeTrackerItemByIndex: (index: number) => void; removeTrackerItemByIndex: (index: number) => void;
updateTrackerAttribute: (emmet: string, attrName: string, attr: TrackerAttribute) => boolean; updateTrackerAttribute: typeof updateTrackerAttr;
updateTrackerAttributeByIndex: (index: number, attrName: string, attr: TrackerAttribute) => void; updateTrackerAttributeByIndex: (index: number, attrName: string, attr: TrackerAttribute) => void;
updateTrackerClassesByIndex: (index: number, classes: string[]) => void; updateTrackerClassesByIndex: (index: number, classes: string[]) => void;
moveTrackerItem: (emmet: string, direction: 'up' | 'down') => boolean; moveTrackerItem: typeof moveTracker;
moveTrackerItemByIndex: (index: number, direction: 'up' | 'down') => void; moveTrackerItemByIndex: (index: number, direction: 'up' | 'down') => void;
removeTrackerItemClass: (emmet: string, className: string) => boolean; removeTrackerItemClass: typeof removeClassFromTracker;
removeTrackerItemClassByIndex: (index: number, className: string) => void; removeTrackerItemClassByIndex: (index: number, className: string) => void;
recordTrackerCommand: (cmd: Omit<TrackerCommand, 'timestamp'>) => void;
} }
export function useCommander( export function useCommander(customCommands?: MdCommanderCommandMap): UseCommanderReturn {
customCommands?: MdCommanderCommandMap,
): UseCommanderReturn {
const [inputValue, setInputValue] = createSignal(""); const [inputValue, setInputValue] = createSignal("");
const [entries, setEntries] = createSignal<CommanderEntry[]>([]); const [entries, setEntries] = createSignal<CommanderEntry[]>([]);
const [showCompletions, setShowCompletions] = createSignal(false); const [showCompletions, setShowCompletions] = createSignal(false);
const [completions, setCompletions] = createSignal<CompletionItem[]>([]); const [completions, setCompletions] = createSignal<CompletionItem[]>([]);
const [selectedCompletion, setSelectedCompletionState] = createSignal(0); const [selectedCompletion, setSelectedCompletionState] = createSignal(0);
const [isFocused, setIsFocused] = createSignal(false); const [isFocused, setIsFocused] = createSignal(false);
// 命令历史
const [commandHistory, setCommandHistory] = createSignal<string[]>([]); const [commandHistory, setCommandHistory] = createSignal<string[]>([]);
const [historyIndex, setHistoryIndex] = createSignal(-1); const [historyIndex, setHistoryIndex] = createSignal(-1);
// Tracker 相关
const [viewMode, setViewMode] = createSignal<TrackerViewMode>("history"); const [viewMode, setViewMode] = createSignal<TrackerViewMode>("history");
const commands = { ...defaultCommands, ...customCommands }; const commands = initializeCommands(customCommands);
// 更新 help 命令的参数值 // ==================== 命令执行 ====================
if (commands.help?.parameters?.[0]) {
commands.help.parameters[0].values = Object.keys(commands).filter(
(k) => k !== "help",
);
}
const handleCommand = () => { const handleCommand = () => {
const input = inputValue().trim(); const input = inputValue().trim();
@ -360,11 +130,8 @@ export function useCommander(
}; };
setEntries((prev) => [...prev, newEntry]); setEntries((prev) => [...prev, newEntry]);
// 添加到命令历史
setCommandHistory((prev) => [...prev, input]); setCommandHistory((prev) => [...prev, input]);
setHistoryIndex(-1); // 重置历史索引 setHistoryIndex(-1);
setInputValue(""); setInputValue("");
setShowCompletions(false); setShowCompletions(false);
@ -373,18 +140,17 @@ export function useCommander(
} }
}; };
// ==================== 自动补全 ====================
const updateCompletions = () => { const updateCompletions = () => {
const input = inputValue(); const comps = getCompletions(inputValue(), commands);
const comps = getCompletions(input, commands);
setCompletions(comps); setCompletions(comps);
setShowCompletions(comps.length > 0 && isFocused()); setShowCompletions(comps.length > 0 && isFocused());
setSelectedCompletionState(0); setSelectedCompletionState(0);
}; };
const setSelectedCompletion = (v: number | ((prev: number) => number)) => { const setSelectedCompletion = (v: number | ((prev: number) => number)) => {
const comps = completions(); const maxIdx = completions().length - 1;
const maxIdx = comps.length - 1;
if (typeof v === 'function') { if (typeof v === 'function') {
setSelectedCompletionState(prev => { setSelectedCompletionState(prev => {
const next = v(prev); const next = v(prev);
@ -398,49 +164,33 @@ export function useCommander(
const acceptCompletion = () => { const acceptCompletion = () => {
const idx = selectedCompletion(); const idx = selectedCompletion();
const comps = completions(); const comps = completions();
const validIdx = Math.min(idx, comps.length - 1); const comp = comps[Math.min(idx, comps.length - 1)];
const comp = comps[validIdx];
if (!comp) return; if (!comp) return;
const input = inputValue(); const input = inputValue();
const trimmed = input.trim();
const parsed = parseInput(input, commands); const parsed = parseInput(input, commands);
let newValue: string; let newValue: string;
if (comp.type === "command") { if (comp.type === "command") {
newValue = comp.insertText + " "; newValue = comp.insertText + " ";
} else if (comp.type === "option") { } else if (comp.type === "option") {
const base = parsed.command || "";
const existingOptions = Object.entries(parsed.options) const existingOptions = Object.entries(parsed.options)
.map(([k, v]) => `--${k}=${v}`) .map(([k, v]) => `--${k}=${v}`)
.join(" "); .join(" ");
newValue = `${base} ${existingOptions}${existingOptions ? " " : ""}${comp.insertText}`; newValue = `${parsed.command || ""} ${existingOptions}${existingOptions ? " " : ""}${comp.insertText}`;
} else if (comp.type === "value") { } else if (comp.type === "value") {
// 参数值补全
const cmd = parsed.command ? commands[parsed.command] : null; const cmd = parsed.command ? commands[parsed.command] : null;
const paramDefs = cmd?.parameters || []; const paramDefs = cmd?.parameters || [];
const usedParams = Object.keys(parsed.params); const usedParams = Object.keys(parsed.params);
const isTypingLastParam = paramDefs.length === usedParams.length && !input.endsWith(" ");
const base = parsed.command || "";
const existingOptions = Object.entries(parsed.options)
.map(([k, v]) => `--${k}=${v}`)
.join(" ");
// 判断是否正在编辑最后一个参数(使用原始 input 检查尾随空格)
const isTypingLastParam = paramDefs.length === usedParams.length &&
input.length > 0 &&
!input.endsWith(" ");
if (isTypingLastParam) { if (isTypingLastParam) {
// 替换最后一个参数 const parts = input.trim().split(/\s+/);
const parts = trimmed.split(/\s+/); parts.pop();
parts.pop(); // 移除正在输入的参数 newValue = `${parsed.command || ""}${parts.length > 1 ? " " + parts.slice(1).join(" ") : ""} ${comp.insertText}`;
const existingParams = parts.slice(1).join(" "); // 跳过命令
newValue = `${base}${existingParams ? " " + existingParams : ""} ${comp.insertText}${existingOptions ? " " + existingOptions : ""}`;
} else if (paramDefs.length > usedParams.length) { } else if (paramDefs.length > usedParams.length) {
// 当前参数的补全
const existingParams = Object.values(parsed.params).join(" "); const existingParams = Object.values(parsed.params).join(" ");
newValue = `${base} ${existingParams}${existingParams ? " " : ""}${comp.insertText}${existingOptions ? " " + existingOptions : ""}`; newValue = `${parsed.command || ""} ${existingParams}${existingParams ? " " : ""}${comp.insertText}`;
} else { } else {
newValue = input; newValue = input;
} }
@ -452,22 +202,19 @@ export function useCommander(
setShowCompletions(false); setShowCompletions(false);
}; };
// ==================== 命令历史 ====================
const navigateHistory = (direction: 'up' | 'down') => { const navigateHistory = (direction: 'up' | 'down') => {
const history = commandHistory(); const history = commandHistory();
if (history.length === 0) return; if (history.length === 0) return;
let newIndex = historyIndex(); let newIndex = historyIndex();
if (direction === 'up') { if (direction === 'up') {
// 向上浏览历史(更早的命令) if (newIndex < history.length - 1) newIndex++;
if (newIndex < history.length - 1) {
newIndex++;
}
} else { } else {
// 向下浏览历史(更新的命令)
if (newIndex > 0) { if (newIndex > 0) {
newIndex--; newIndex--;
} else { } else {
// 回到当前输入
setInputValue(""); setInputValue("");
setHistoryIndex(-1); setHistoryIndex(-1);
return; return;
@ -475,92 +222,41 @@ export function useCommander(
} }
setHistoryIndex(newIndex); setHistoryIndex(newIndex);
// 从历史末尾获取命令(最新的在前)
setInputValue(history[history.length - 1 - newIndex]); setInputValue(history[history.length - 1 - newIndex]);
}; };
// ==================== Tracker 方法 ==================== // ==================== Tracker 操作 ====================
const addTrackerItem = (item: Omit<TrackerItem, 'id'>) => { const getEmmetFromIndex = (index: number): string | null => {
return addTracker(item); const items = getTrackerItems();
}; if (index < 0 || index >= items.length) return null;
const item = items[index];
const removeTrackerItem = (emmet: string) => { return `${item.tag}${item.id ? '#' + item.id : ''}${item.classes.length > 0 ? '.' + item.classes.join('.') : ''}`;
return removeTracker(emmet);
}; };
const removeTrackerItemByIndex = (index: number) => { const removeTrackerItemByIndex = (index: number) => {
const items = trackerItems(); const emmet = getEmmetFromIndex(index);
if (index >= 0 && index < items.length) { if (emmet) removeTracker(emmet);
const item = items[index];
const emmet = `${item.tag}${item.id ? '#' + item.id : ''}${item.classes.length > 0 ? '.' + item.classes.join('.') : ''}`;
removeTracker(emmet);
}
};
const updateTrackerAttribute = (emmet: string, attrName: string, attr: TrackerAttribute) => {
return updateTrackerAttr(emmet, attrName, attr);
}; };
const updateTrackerAttributeByIndex = (index: number, attrName: string, attr: TrackerAttribute) => { const updateTrackerAttributeByIndex = (index: number, attrName: string, attr: TrackerAttribute) => {
const items = trackerItems(); const emmet = getEmmetFromIndex(index);
if (index >= 0 && index < items.length) { if (emmet) updateTrackerAttr(emmet, attrName, attr);
const item = items[index];
const emmet = `${item.tag}${item.id ? '#' + item.id : ''}${item.classes.length > 0 ? '.' + item.classes.join('.') : ''}`;
updateTrackerAttr(emmet, attrName, attr);
}
}; };
const updateTrackerClassesByIndex = (index: number, classes: string[]) => { const updateTrackerClassesByIndex = (index: number, classes: string[]) => {
const items = trackerItems(); const emmet = getEmmetFromIndex(index);
if (index >= 0 && index < items.length) { if (emmet) updateTrackerClassesStore(emmet, classes);
const item = items[index];
const emmet = `${item.tag}${item.id ? '#' + item.id : ''}${item.classes.length > 0 ? '.' + item.classes.join('.') : ''}`;
updateTrackerClassesStore(emmet, classes);
}
};
const moveTrackerItem = (emmet: string, direction: 'up' | 'down') => {
return moveTracker(emmet, direction);
}; };
const moveTrackerItemByIndex = (index: number, direction: 'up' | 'down') => { const moveTrackerItemByIndex = (index: number, direction: 'up' | 'down') => {
const items = trackerItems(); const emmet = getEmmetFromIndex(index);
if (index >= 0 && index < items.length) { if (emmet) moveTracker(emmet, direction);
const item = items[index];
const emmet = `${item.tag}${item.id ? '#' + item.id : ''}${item.classes.length > 0 ? '.' + item.classes.join('.') : ''}`;
moveTracker(emmet, direction);
}
};
const removeTrackerItemClass = (emmet: string, className: string) => {
return removeClassFromTracker(emmet, className);
}; };
const removeTrackerItemClassByIndex = (index: number, className: string) => { const removeTrackerItemClassByIndex = (index: number, className: string) => {
const items = trackerItems(); const emmet = getEmmetFromIndex(index);
if (index >= 0 && index < items.length) { if (emmet) removeClassFromTracker(emmet, className);
const item = items[index];
const emmet = `${item.tag}${item.id ? '#' + item.id : ''}${item.classes.length > 0 ? '.' + item.classes.join('.') : ''}`;
removeClassFromTracker(emmet, className);
}
};
const trackerItems = () => getTrackerItems();
const trackerHistory = () => getTrackerHistory();
const setTrackerItems = (updater: (prev: TrackerItem[]) => TrackerItem[]) => {
// 直接操作 store
const current = getTrackerItems();
const updated = updater(current);
// 计算差异并记录
if (updated.length !== current.length) {
// 长度变化,可能是批量操作
}
};
const recordTrackerCommand = (cmd: Omit<TrackerCommand, 'timestamp'>) => {
// store 已经自动记录
}; };
return { return {
@ -585,19 +281,17 @@ export function useCommander(
navigateHistory, navigateHistory,
viewMode, viewMode,
setViewMode, setViewMode,
trackerItems, trackerItems: getTrackerItems,
setTrackerItems, trackerHistory: getTrackerHistory,
trackerHistory, addTrackerItem: addTracker,
addTrackerItem, removeTrackerItem: removeTracker,
removeTrackerItem,
removeTrackerItemByIndex, removeTrackerItemByIndex,
updateTrackerAttribute, updateTrackerAttribute: updateTrackerAttr,
updateTrackerAttributeByIndex, updateTrackerAttributeByIndex,
updateTrackerClassesByIndex, updateTrackerClassesByIndex,
moveTrackerItem, moveTrackerItem: moveTracker,
moveTrackerItemByIndex, moveTrackerItemByIndex,
removeTrackerItemClass, removeTrackerItemClass: removeClassFromTracker,
removeTrackerItemClassByIndex, removeTrackerItemClassByIndex,
recordTrackerCommand,
}; };
} }

View File

@ -1,6 +1,6 @@
import { customElement, noShadowDOM } from "solid-element"; import { customElement, noShadowDOM } from "solid-element";
import { onMount, onCleanup, Show, createEffect } from "solid-js"; import { onMount, onCleanup, Show, createEffect } from "solid-js";
import { useCommander } from "./hooks"; import { useCommander, initializeCommands } from "./hooks";
import { CommanderInput } from "./CommanderInput"; import { CommanderInput } from "./CommanderInput";
import { CommanderEntries } from "./CommanderEntries"; import { CommanderEntries } from "./CommanderEntries";
import { TrackerView } from "./TrackerView"; import { TrackerView } from "./TrackerView";
@ -16,9 +16,11 @@ customElement<MdCommanderProps>(
noShadowDOM(); noShadowDOM();
const { articlePath, rawSrc } = loadElementSrc(element as any); const { articlePath, rawSrc } = loadElementSrc(element as any);
const commander = useCommander(props.commands); const commander = useCommander(props.commands);
createEffect(async () => { createEffect(async () => {
if (!rawSrc) return; if (!rawSrc) return;
await loadCommandTemplates(commander.commands, resolvePath(articlePath, rawSrc)); const commands = initializeCommands(props.commands);
await loadCommandTemplates(commands, resolvePath(articlePath, rawSrc));
}); });
const handleKeyDown = (e: KeyboardEvent) => { const handleKeyDown = (e: KeyboardEvent) => {
@ -48,7 +50,6 @@ customElement<MdCommanderProps>(
} }
} }
// 补全未打开时,使用上下键浏览历史
if (e.key === "ArrowUp" && !commander.showCompletions()) { if (e.key === "ArrowUp" && !commander.showCompletions()) {
e.preventDefault(); e.preventDefault();
commander.navigateHistory("up"); commander.navigateHistory("up");
@ -83,13 +84,8 @@ customElement<MdCommanderProps>(
class={`md-commander flex flex-col border border-gray-300 rounded-lg overflow-hidden ${props.class || ""}`} class={`md-commander flex flex-col border border-gray-300 rounded-lg overflow-hidden ${props.class || ""}`}
style={{ height: heightStyle() }} style={{ height: heightStyle() }}
> >
{/* 标签页导航 */} <TabBar mode={commander.viewMode} onModeChange={commander.setViewMode} />
<TabBar
mode={commander.viewMode}
onModeChange={commander.setViewMode}
/>
{/* 内容区域:历史或追踪 */}
<Show <Show
when={commander.viewMode() === "history"} when={commander.viewMode() === "history"}
fallback={ fallback={
@ -101,7 +97,9 @@ customElement<MdCommanderProps>(
onClassesChange={(index, classes) => onClassesChange={(index, classes) =>
commander.updateTrackerClassesByIndex(index, classes) commander.updateTrackerClassesByIndex(index, classes)
} }
onRemoveClass={commander.removeTrackerItemClassByIndex} onRemoveClass={(index, className) =>
commander.removeTrackerItemClassByIndex(index, className)
}
onMoveUp={(index) => commander.moveTrackerItemByIndex(index, "up")} onMoveUp={(index) => commander.moveTrackerItemByIndex(index, "up")}
onMoveDown={(index) => commander.moveTrackerItemByIndex(index, "down")} onMoveDown={(index) => commander.moveTrackerItemByIndex(index, "down")}
onRemove={(index) => commander.removeTrackerItemByIndex(index)} onRemove={(index) => commander.removeTrackerItemByIndex(index)}
@ -114,7 +112,6 @@ customElement<MdCommanderProps>(
/> />
</Show> </Show>
{/* 命令输入框 */}
<div class="relative border-t border-gray-300"> <div class="relative border-t border-gray-300">
<CommanderInput <CommanderInput
placeholder={props.placeholder} placeholder={props.placeholder}

View File

@ -1,5 +1,7 @@
import type { Component } from "solid-js"; import type { Component } from "solid-js";
// ==================== 组件 Props ====================
export interface MdCommanderProps { export interface MdCommanderProps {
placeholder?: string; placeholder?: string;
class?: string; class?: string;
@ -8,6 +10,8 @@ export interface MdCommanderProps {
commandTemplates?: string | string[]; // CSV 文件路径或路径数组 commandTemplates?: string | string[]; // CSV 文件路径或路径数组
} }
// ==================== 命令定义 ====================
export interface MdCommanderCommandMap { export interface MdCommanderCommandMap {
[key: string]: MdCommanderCommand; [key: string]: MdCommanderCommand;
} }
@ -64,6 +68,8 @@ export interface MdCommanderOption {
EntryComponent?: Component<any>; EntryComponent?: Component<any>;
} }
// ==================== 命令执行记录 ====================
export interface CommanderEntry { export interface CommanderEntry {
id: string; id: string;
command: string; command: string;
@ -83,7 +89,9 @@ export interface CompletionItem {
insertText: string; insertText: string;
} }
// ==================== Tracker 类型 ==================== // ==================== Tracker ====================
export type TrackerViewMode = "history" | "tracker";
export type TrackerAttributeType = "progress" | "count" | "string"; export type TrackerAttributeType = "progress" | "count" | "string";
@ -95,13 +103,11 @@ export interface TrackerAttribute {
export interface TrackerItem { export interface TrackerItem {
tag: string; tag: string;
id?: string; // Emmet ID (#id) id?: string;
classes: string[]; classes: string[];
attributes: Record<string, TrackerAttribute>; attributes: Record<string, TrackerAttribute>;
} }
export type TrackerViewMode = "history" | "tracker";
export interface TrackerCommand { export interface TrackerCommand {
type: "add" | "remove" | "update" | "reorder"; type: "add" | "remove" | "update" | "reorder";
itemId?: string; itemId?: string;