/** * /db favorite —— 管理已保存的查询模板。 */ import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent"; import type { DatabaseWorkspaceService } from "../state/workspace"; import type { FavoriteEntry } from "../history/store"; import { READONLY_SQL_RE } from "../connection/sql-policy"; import { padToDisplayWidth, truncateToDisplayWidth } from "../formatting/display-width"; import { executeAndDisplay } from "./query"; // ── 列表格式化 ───────────────────────────────────────────── export function formatFavoriteList(entries: FavoriteEntry[], currentDb?: string): string { if (entries.length === 0) { return currentDb ? `暂无收藏(${currentDb})。使用 /db favorite add 添加。` : "暂无收藏。使用 /db favorite add 添加。"; } const scope = currentDb ? `(${currentDb} + 全局)` : "(全局)"; const lines = [`═══ 收藏查询 ${scope} — ${entries.length} 条 ═══`, ""]; for (const e of entries) { // 名称/库标签/SQL 都按显示宽度对齐——中文名按码元 padEnd 会列错位 const sql = truncateToDisplayWidth(e.sql, 55); const dbTag = e.database ? `[${e.database}]` : "[🌐 全局]"; const desc = e.description ? ` — ${truncateToDisplayWidth(e.description, 30)}` : ""; lines.push( ` #${String(e.id).padStart(3)} ${padToDisplayWidth(e.name, 18)}${padToDisplayWidth(dbTag, 14)}${sql}`, ); if (desc) lines.push(` ${desc}`); } lines.push(""); lines.push("选择一个 # 执行、编辑或删除。"); return lines.join("\n"); } // ── 入口 ───────────────────────────────────────────────────── export async function handleFavorite( ctx: ExtensionCommandContext, ws: DatabaseWorkspaceService, pi: ExtensionAPI, rest: string[], ): Promise { const action = rest[0]; if (action === "add") { return await handleFavoriteAdd(ctx, ws, pi, rest.slice(1)); } return await handleFavoriteList(ctx, ws, pi); } // ── 添加 ───────────────────────────────────────────────────────── async function handleFavoriteAdd( ctx: ExtensionCommandContext, ws: DatabaseWorkspaceService, _pi: ExtensionAPI, args: string[], ): Promise { let name: string | undefined; let sql: string | undefined; if (args.length >= 2) { name = args[0]; sql = args.slice(1).join(" "); } else if (args.length === 1) { const nameOrSql = args[0]; if (READONLY_SQL_RE.test(nameOrSql)) { sql = nameOrSql; } else { name = nameOrSql; } } if (!name) { name = await ctx.ui.input("收藏名称", ""); if (!name || !name.trim()) return; name = name.trim(); } if (!sql) { sql = await ctx.ui.editor("SQL 模板(支持多行)", "SELECT * FROM ... LIMIT 10"); if (!sql || !sql.trim()) return; sql = sql.trim(); } const description = await ctx.ui.input("描述(可选,回车跳过)", ""); if (description === undefined) return; const entry = ws.saveFavorite(name, sql, description?.trim()); ctx.ui.notify( `已收藏 #${entry.id} "${entry.name}"${entry.database ? ` [${entry.database}]` : " [🌐 全局]"}`, "info", ); } // ── 列表 & 选择 ─────────────────────────────────────────────── async function handleFavoriteList( ctx: ExtensionCommandContext, ws: DatabaseWorkspaceService, pi: ExtensionAPI, ): Promise { const entries = ws.listFavorites(); if (entries.length === 0) { ctx.ui.notify(formatFavoriteList([], ws.current?.database), "info"); return; } const labels = entries.map((e) => { const sql = padToDisplayWidth(truncateToDisplayWidth(e.sql, 40), 40); return `#${String(e.id).padStart(3)} ${padToDisplayWidth(e.name, 18)} ${sql}`; }); const choice = await ctx.ui.select("选择一个收藏", labels); if (!choice) return; const idx = labels.indexOf(choice); const entry = entries[idx]; const action = await ctx.ui.select(`#${entry.id} ${entry.name}`, [ "▶ 直接执行", "✏️ 编辑后执行", "🗑 删除", ]); if (!action) return; if (action === "▶ 直接执行") { await executeAndDisplay(ctx, ws, pi, entry.sql); } else if (action === "✏️ 编辑后执行") { const editedSql = await ctx.ui.editor("编辑 SQL", entry.sql); if (!editedSql || !editedSql.trim()) return; // 不做预校验——执行器强制只读守卫。 await executeAndDisplay(ctx, ws, pi, editedSql.trim()); } else if (action === "🗑 删除") { const ok = await ctx.ui.confirm("确认删除", `删除收藏 "${entry.name}"?`); if (ok) { ws.deleteFavorite(entry.id); ctx.ui.notify(`已删除收藏 #${entry.id} "${entry.name}"`, "info"); } } }