feat(editor): execute single SQL directly without picker

* 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 <staff@qimaos-MacBook-Pro.local>
This commit is contained in:
zipg 2026-06-22 17:14:36 +08:00 committed by GitHub
parent 628fadf517
commit dac97b83a6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 40 additions and 3 deletions

View File

@ -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);

View File

@ -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);

View File

@ -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;