From dac97b83a6fec564917f93d53e9e0febedc90c4f Mon Sep 17 00:00:00 2001 From: zipg Date: Mon, 22 Jun 2026 17:14:36 +0800 Subject: [PATCH] feat(editor): execute single SQL directly without picker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(ui): toast 消息支持换行显示 - 添加 max-w-3xl 限制最大宽度 - 使用 whitespace-pre-wrap 允许自动换行 - 添加 break-words 防止长单词溢出 * feat(editor): 单条 SQL 直接执行 --------- Co-authored-by: t8y2 <1156263951@qq.com> Co-authored-by: staff --- .../src/components/editor/QueryEditor.vue | 4 ++-- .../lib/__tests__/sqlStatementRanges.spec.ts | 21 ++++++++++++++++++- apps/desktop/src/lib/sqlStatementRanges.ts | 18 ++++++++++++++++ 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/components/editor/QueryEditor.vue b/apps/desktop/src/components/editor/QueryEditor.vue index a2be76bd9..5daf84208 100644 --- a/apps/desktop/src/components/editor/QueryEditor.vue +++ b/apps/desktop/src/components/editor/QueryEditor.vue @@ -10,7 +10,7 @@ import SqlExecutionTargetPicker from "./SqlExecutionTargetPicker.vue"; import CustomContextMenu, { type ContextMenuItem } from "@/components/ui/CustomContextMenu.vue"; import { copyToClipboard } from "@/lib/clipboard"; import { resolveExecutableSql, type SqlExecutionSnapshot, type SqlExecutionOverride, type SqlExecutionCandidate } from "@/lib/sqlExecutionTarget"; -import { buildExecutionCandidates, supportsExecutionTargetPicker } from "@/lib/sqlStatementRanges"; +import { buildExecutionCandidates, hasMultipleExecutionTargets, supportsExecutionTargetPicker } from "@/lib/sqlStatementRanges"; import { formatSqlText, type SqlFormatDialect } from "@/lib/sqlFormatter"; import { formatMongoShellText } from "@/lib/mongoFormatter"; import { useConnectionStore } from "@/stores/connectionStore"; @@ -319,7 +319,7 @@ function requestExecute() { const cursorPos = selection.head; const candidates = buildExecutionCandidates(doc, cursorPos, props.databaseType); if (candidates.length === 0) return false; - if (!settingsStore.editorSettings.showExecutionTargetPicker) { + if (!settingsStore.editorSettings.showExecutionTargetPicker || !hasMultipleExecutionTargets(doc, props.databaseType)) { const preferredKind = settingsStore.editorSettings.executeMode === "current" ? "cursor" : "all"; const candidate = candidates.find((item) => item.kind === preferredKind) ?? candidates[0]; emit("execute", candidate.sql); diff --git a/apps/desktop/src/lib/__tests__/sqlStatementRanges.spec.ts b/apps/desktop/src/lib/__tests__/sqlStatementRanges.spec.ts index 89f05fffc..cac6d6e3f 100644 --- a/apps/desktop/src/lib/__tests__/sqlStatementRanges.spec.ts +++ b/apps/desktop/src/lib/__tests__/sqlStatementRanges.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { buildExecutionCandidates, fullSqlRange, splitSqlStatementRanges, statementRangeAtCursor, supportsExecutionTargetPicker } from "../sqlStatementRanges"; +import { buildExecutionCandidates, fullSqlRange, hasMultipleExecutionTargets, splitSqlStatementRanges, statementRangeAtCursor, supportsExecutionTargetPicker } from "../sqlStatementRanges"; function indexOf(sql: string, needle: string, occurrence = 1): number { let from = 0; @@ -285,6 +285,25 @@ describe("buildExecutionCandidates", () => { }); }); +describe("hasMultipleExecutionTargets", () => { + it("returns false for a single SQL statement", () => { + expect(hasMultipleExecutionTargets("SELECT 1;")).toBe(false); + }); + + it("returns true for multiple SQL statements", () => { + expect(hasMultipleExecutionTargets("SELECT 1;\nSELECT 2;")).toBe(true); + }); + + it("ignores comments when counting SQL statements", () => { + expect(hasMultipleExecutionTargets("-- check one thing\nSELECT 1;")).toBe(false); + }); + + it("counts executable Redis command lines", () => { + expect(hasMultipleExecutionTargets("GET user:1", "redis")).toBe(false); + expect(hasMultipleExecutionTargets("GET user:1\n# comment\nDEL user:2", "redis")).toBe(true); + }); +}); + describe("supportsExecutionTargetPicker", () => { it("enables the picker for SQL database connections and Redis", () => { expect(supportsExecutionTargetPicker("mysql")).toBe(true); diff --git a/apps/desktop/src/lib/sqlStatementRanges.ts b/apps/desktop/src/lib/sqlStatementRanges.ts index 490ac598e..a886ba2a2 100644 --- a/apps/desktop/src/lib/sqlStatementRanges.ts +++ b/apps/desktop/src/lib/sqlStatementRanges.ts @@ -17,6 +17,13 @@ export function supportsExecutionTargetPicker(databaseType?: DatabaseType): bool return !!databaseType && (databaseType === "redis" || !NON_SQL_EXECUTION_TARGET_TYPES.has(databaseType)); } +export function hasMultipleExecutionTargets(sql: string, databaseType?: DatabaseType): boolean { + if (databaseType === "redis") { + return redisExecutableCommandCount(sql) > 1; + } + return splitSqlStatementRanges(sql).length > 1; +} + interface RawStatement { /** Start offset (inclusive) of whitespace that can still target this statement. */ hitFrom: number; @@ -825,6 +832,17 @@ function candidateFromRange(range: SqlTextRange, kind: SqlExecutionCandidate["ki }; } +function redisExecutableCommandCount(sql: string): number { + let count = 0; + for (const line of sql.split(/\r?\n/)) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + count += 1; + if (count > 1) return count; + } + return count; +} + function redisCommandRangeAtCursor(sql: string, cursorPos: number): SqlTextRange | null { const pos = clampCursor(sql, cursorPos); if (isCursorOnBlankLine(sql, pos)) return null;