diff --git a/apps/desktop/src/lib/__tests__/sql/sqlStatementRanges.spec.ts b/apps/desktop/src/lib/__tests__/sql/sqlStatementRanges.spec.ts index ff748b4adf..3f8b66d2c8 100644 --- a/apps/desktop/src/lib/__tests__/sql/sqlStatementRanges.spec.ts +++ b/apps/desktop/src/lib/__tests__/sql/sqlStatementRanges.spec.ts @@ -584,6 +584,43 @@ GET /_cat/indices`; expect(range?.sql.trim()).toBe("SELECT *\nFROM system_dept"); }); + it("returns the previous statement when the cursor sits after a trailing line comment behind its semicolon", () => { + // 复现场景:每条语句以 `; -- 备注` 结尾,光标停在第一条的行尾注释之后 + const sql = "SELECT T.*,T.ROWID FROM offer t WHERE t.offer_name = 'V3 Red Mi'; -- 114808\nSELECT T.*,T.ROWID FROM offer t WHERE t.offer_name = 'Emery Mann-6264-postpaid-2'; -- 127923"; + const pos = sql.indexOf("\n"); + const range = statementRangeAtCursor(sql, pos, "oracle"); + expect(range?.sql).toBe("SELECT T.*,T.ROWID FROM offer t WHERE t.offer_name = 'V3 Red Mi'"); + }); + + it("returns the current statement when the cursor is inside the trailing line comment", () => { + const sql = "SELECT 1; -- 114808\nSELECT 2; -- 127923"; + const pos = sql.indexOf("114808") + 2; + const range = statementRangeAtCursor(sql, pos); + expect(range?.sql.trim()).toBe("SELECT 1"); + }); + + it("returns the current statement when the cursor is after a trailing block comment behind its semicolon", () => { + const sql = "SELECT 1; /* 备注 */\nSELECT 2;"; + const pos = sql.indexOf("\n"); + const range = statementRangeAtCursor(sql, pos); + expect(range?.sql.trim()).toBe("SELECT 1"); + }); + + it("returns the current statement after a multiline trailing block comment", () => { + const sql = "SELECT 1; /* 第一行\n第二行 */\nSELECT 2;"; + const pos = sql.indexOf("*/") + 2; + const range = statementRangeAtCursor(sql, pos); + expect(range?.sql.trim()).toBe("SELECT 1"); + }); + + it("still returns the next statement when the cursor is at the start of its line after a trailing comment", () => { + // 回归:换行后的光标不再归属上一条语句 + const sql = "SELECT 1; -- 114808\nSELECT 2; -- 127923"; + const pos = sql.indexOf("SELECT 2"); + const range = statementRangeAtCursor(sql, pos); + expect(range?.sql.trim()).toBe("SELECT 2"); + }); + it("keeps a semicolon-line-end cursor on the current multi-line statement", () => { const sql = "SELECT *\nFROM system_dept;"; const gapPos = sql.indexOf(";") + 1; diff --git a/apps/desktop/src/lib/sql/statementDelimiter.ts b/apps/desktop/src/lib/sql/statementDelimiter.ts index 01667e2bc1..250b61e266 100644 --- a/apps/desktop/src/lib/sql/statementDelimiter.ts +++ b/apps/desktop/src/lib/sql/statementDelimiter.ts @@ -24,7 +24,49 @@ export function cursorBelongsToTrailingStatementDelimiter(source: StatementDelim if (cursorPos <= delimiterPos + 1) return true; const afterDelimiter = sliceSource(source, delimiterPos + 1, cursorPos); - return !afterDelimiter.includes("\n") && afterDelimiter.trim() === ""; + // 分号到光标之间允许是空白或尾部注释(如 `SELECT 1; -- 备注`), + // 块注释自身可以跨行,但注释之外的换行仍表示光标已进入下一行 + // 否则行尾注释后的光标会被下一条语句的前置区域错误认领 + return trailingGapIsWhitespaceOrComment(afterDelimiter); +} + +/** + * 判断语句分号与光标之间的间隙是否只包含空白和注释。 + * 块注释内部允许换行;注释之外的换行表示光标已离开分号所在行。 + */ +function trailingGapIsWhitespaceOrComment(gap: string): boolean { + let i = 0; + while (i < gap.length) { + const ch = gap[i] ?? ""; + const next = gap[i + 1] ?? ""; + + // 普通空白直接跳过 + if (ch === " " || ch === "\t" || ch === "\r") { + i += 1; + continue; + } + + if (ch === "\n") return false; + + // 行注释:`--`(SQL 标准)或 `//`(MongoDB shell),其后内容直到行尾都是注释 + if ((ch === "-" && next === "-") || (ch === "/" && next === "/")) return true; + + // `#` 行注释(MySQL 等),但 `#{` 是 MyBatis 参数语法,不是注释 + if (ch === "#" && next !== "{") return true; + + // 块注释 /* ... */:跳过整段后继续检查剩余间隙 + if (ch === "/" && next === "*") { + const close = gap.indexOf("*/", i + 2); + // 未闭合的块注释:剩余部分全部视为注释 + if (close === -1) return true; + i = close + 2; + continue; + } + + // 出现其他实际内容,说明光标后面还有语句文本,不能归属上一条语句 + return false; + } + return true; } function sliceSource(source: StatementDelimiterSource, from: number, to: number): string {