fix(mongodb): honor current command execution mode (#2702)

This commit is contained in:
Guoyu Su 2026-07-07 00:02:34 +08:00 committed by GitHub
parent 18e5df6f5e
commit d7e2eac307
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 55 additions and 7 deletions

View File

@ -1,4 +1,5 @@
import * as api from "@/lib/backend/api";
import { mongoCommandRangeAtCursor } from "@/lib/sql/sqlStatementRanges";
import type { DatabaseType } from "@/types/database";
export type ExecuteMode = "all" | "current";
@ -52,10 +53,12 @@ export async function resolveExecutableSqlWithBackend(fullSql: string, selectedS
const trimmedSelection = selectedSql.trim();
if (trimmedSelection) return trimmedSelection;
// MongoDB uses dedicated per-command gutter actions for "current command";
// the main editor execute action keeps its long-standing "run all text"
// behavior when nothing is selected.
if (options?.databaseType === "mongodb") return fullSql;
if (options?.databaseType === "mongodb") {
if (options.mode === "current" && options.cursorPos !== undefined) {
return mongoCommandRangeAtCursor(fullSql, options.cursorPos)?.sql ?? fullSql;
}
return fullSql;
}
if (options?.mode === "current" && options.cursorPos !== undefined) {
return await api.findStatementAtCursor(fullSql, options.cursorPos, options.databaseType);

View File

@ -409,6 +409,26 @@ export function statementRangeAtCursor(sql: string, cursorPos: number, databaseT
return null;
}
export function mongoCommandRangeAtCursor(sql: string, cursorPos: number): SqlTextRange | null {
const pos = clampCursor(sql, cursorPos);
if (isCursorOnBlankLine(sql, pos)) return null;
const commands = splitMongoCommandRanges(sql);
for (let index = 0; index < commands.length; index += 1) {
const command = commands[index];
const range = { from: command.from, to: command.to, sql: command.text };
if (pos >= command.from && pos <= command.to) return range;
const next = commands[index + 1];
if (pos > command.to && (!next || pos < next.from) && isCursorInSameLineDelimiterGap(sql, command.to, pos)) return range;
if (pos < command.from && sql.slice(pos, command.from).trim() === "" && isCursorOnStatementLine(sql, pos, command)) return range;
}
return null;
}
function isCursorInSameLineDelimiterGap(sql: string, previousStatementEnd: number, cursorPos: number): boolean {
if (cursorPos <= previousStatementEnd) return false;
const between = sql.slice(previousStatementEnd, cursorPos);
@ -1450,7 +1470,7 @@ function isCursorOnBlankLine(sql: string, pos: number): boolean {
return sql.slice(lineStart, lineEnd).trim() === "";
}
function isCursorOnStatementLine(sql: string, pos: number, statement: RawStatement): boolean {
function isCursorOnStatementLine(sql: string, pos: number, statement: Pick<RawStatement, "from">): boolean {
const lineStart = sql.lastIndexOf("\n", pos - 1) + 1;
let lineEnd = sql.indexOf("\n", pos);
if (lineEnd === -1) lineEnd = sql.length;

View File

@ -13,20 +13,45 @@ beforeEach(() => {
apiMock.findStatementAtCursor.mockReset();
});
test("mongodb backend resolution keeps the full text when nothing is selected", async () => {
test("mongodb backend resolution uses the current command when configured", async () => {
apiMock.findStatementAtCursor.mockResolvedValue("db.users.insertOne({ name: 'ignored' })");
const fullSql = "db.users.insertOne({ name: 'Ada' });\ndb.users.insertOne({ name: 'Grace' });";
const fullSql = 'db.users.find({ name: "Ada" });\ndb.users.find({ name: "Grace" });\ndb.users.find({ name: "Linus" });';
const resolved = await resolveExecutableSqlWithBackend(fullSql, "", {
mode: "current",
cursorPos: fullSql.indexOf("Grace"),
databaseType: "mongodb",
});
assert.equal(resolved, 'db.users.find({ name: "Grace" })');
assert.equal(apiMock.findStatementAtCursor.mock.calls.length, 0);
});
test("mongodb backend resolution keeps the full text in all mode", async () => {
const fullSql = 'db.users.find({ name: "Ada" });\ndb.users.find({ name: "Grace" });';
const resolved = await resolveExecutableSqlWithBackend(fullSql, "", {
mode: "all",
cursorPos: fullSql.indexOf("Grace"),
databaseType: "mongodb",
});
assert.equal(resolved, fullSql);
assert.equal(apiMock.findStatementAtCursor.mock.calls.length, 0);
});
test("mongodb backend resolution prefers the manual selection", async () => {
const fullSql = 'db.users.find({ name: "Ada" });\ndb.users.find({ name: "Grace" });';
const selectedSql = 'db.users.find({ name: "Ada" })';
const resolved = await resolveExecutableSqlWithBackend(fullSql, selectedSql, {
mode: "current",
cursorPos: fullSql.indexOf("Grace"),
databaseType: "mongodb",
});
assert.equal(resolved, selectedSql);
assert.equal(apiMock.findStatementAtCursor.mock.calls.length, 0);
});
test("non-mongodb backend resolution still asks the backend for the current statement", async () => {
apiMock.findStatementAtCursor.mockResolvedValue("SELECT 2");