feat(editor): fold sql lists and execute selection from gutter

This commit is contained in:
C.J.S
2026-09-22 22:33:07 +08:00
committed by t8y2
parent a85fc7fb93
commit b728c391ba
5 changed files with 35 additions and 16 deletions
@@ -2212,9 +2212,15 @@ function executeSqlStatementFromGutter(currentView: EditorViewType, line: { from
event.preventDefault();
event.stopPropagation();
// Gutter play is always scoped to the statement/command for that line, even
// when the main editor execute action would run the full document.
// when the main editor execute action would run the full document. An explicit
// selection overlapping that statement is more specific, so preserve it; a
// selection elsewhere in the document must not hijack the click.
const editorViewportRequestId = executionViewportOwnership.beginRequest();
emitExecutionRequest({ ...sqlExecutionSnapshotForRange(currentView, statementRange), editorViewportRequestId });
const selection = currentView.state.selection.main;
const hasSelectedSql = !selection.empty && currentView.state.sliceDoc(selection.from, selection.to).trim().length > 0;
const selectionOverlapsStatement = hasSelectedSql && selection.from < statementRange.to && statementRange.from < selection.to;
const executionSnapshot = selectionOverlapsStatement ? sqlExecutionSnapshotFromView(currentView) : sqlExecutionSnapshotForRange(currentView, statementRange);
emitExecutionRequest({ ...executionSnapshot, editorViewportRequestId });
// 不主动聚焦编辑器,否则 CodeMirror 会把屏幕滚回之前的光标位置。
// currentView.focus();
return true;
@@ -32,7 +32,10 @@ describe("QueryEditor execution routing", () => {
expect(queryEditorSource).toContain("function executeFromContextMenu()");
expect(queryEditorSource).toContain("requestExecute();\n focusEditor();");
expect(queryEditorSource).toContain("function executeSqlStatementFromGutter");
expect(queryEditorSource).toContain("emitExecutionRequest({ ...sqlExecutionSnapshotForRange(currentView, statementRange), editorViewportRequestId })");
expect(queryEditorSource).toContain("const hasSelectedSql = !selection.empty && currentView.state.sliceDoc(selection.from, selection.to).trim().length > 0;");
expect(queryEditorSource).toContain("const selectionOverlapsStatement = hasSelectedSql && selection.from < statementRange.to && statementRange.from < selection.to;");
expect(queryEditorSource).toContain("const executionSnapshot = selectionOverlapsStatement ? sqlExecutionSnapshotFromView(currentView) : sqlExecutionSnapshotForRange(currentView, statementRange);");
expect(queryEditorSource).toContain("emitExecutionRequest({ ...executionSnapshot, editorViewportRequestId })");
});
it("routes the new-result-tab shortcut through the same target selection contract", () => {
@@ -97,7 +100,9 @@ describe("QueryEditor execution routing", () => {
it("preserves the source range when executing from the statement gutter", () => {
expect(queryEditorSource).toContain("const editorViewportRequestId = executionViewportOwnership.beginRequest()");
expect(queryEditorSource).toContain("emitExecutionRequest({ ...sqlExecutionSnapshotForRange(currentView, statementRange), editorViewportRequestId })");
expect(queryEditorSource).toContain("sqlExecutionSnapshotFromView(currentView)");
expect(queryEditorSource).toContain("sqlExecutionSnapshotForRange(currentView, statementRange)");
expect(queryEditorSource).toContain("emitExecutionRequest({ ...executionSnapshot, editorViewportRequestId })");
expect(queryEditorSource).not.toContain('emit("execute", statementRange.sql)');
});
@@ -221,6 +221,20 @@ LEFT JOIN (
expect(foldedTextAtLine(state, 3)).toBe("\n SELECT account_id, SUM(amount) AS total\n FROM fee\n GROUP BY account_id\n");
});
it("folds multiline parenthesized IN lists while keeping the surrounding SQL visible", () => {
const sql = `SELECT *
FROM account
WHERE account_id IN (
'7687402546616451098',
'7687402818521628718',
'7687402206655496235'
)
AND status = 'active';`;
const state = stateFor(sql);
expect(foldedTextAtLine(state, 3)).toBe("\n '7687402546616451098',\n '7687402818521628718',\n '7687402206655496235'\n");
});
it("folds SELECT branches around UNION ALL at the same query level", () => {
const sql = `WITH combined AS (
SELECT id, amount
@@ -15,7 +15,8 @@ interface FoldRange {
// `@codemirror/lang-sql`'s grammar is deliberately shallow (see its `foldNodeProp`, which only
// covers the flat `Statement` and `BlockComment` nodes). This service adds structure-aware folds
// for procedural blocks and query expressions without replacing CodeMirror's native folds.
// for procedural blocks and parenthesized query expressions without replacing CodeMirror's native
// folds.
//
// Scoped to `BEGIN...END` and `CASE...END` (issue #6574) -- `BEGIN TRY`/`END TRY` and
// `BEGIN CATCH`/`END CATCH` fall out of this for free, since `BEGIN` is matched regardless of a
@@ -94,16 +95,6 @@ function queryScopeEnd(state: EditorState, scope: SyntaxNode): number {
return end;
}
function isQueryParens(state: EditorState, node: SyntaxNode): boolean {
for (let child = node.firstChild; child; child = child.nextSibling) {
if (child.name === "(" || child.name === "LineComment" || child.name === "BlockComment") continue;
if (child.name !== "Keyword") return false;
const keyword = state.sliceDoc(child.from, child.to).toUpperCase();
return keyword === "SELECT" || keyword === "WITH";
}
return false;
}
interface QueryScopeTokens {
scope: SyntaxNode;
tokens: Array<{ from: number; keyword: "SELECT" | "UNION" }>;
@@ -131,7 +122,10 @@ function addQueryStructureFoldRanges(state: EditorState, tree: Tree, ranges: Map
const scopes = new Map<string, QueryScopeTokens>();
tree.iterate({
enter(node) {
if (node.name === "Parens" && isQueryParens(state, node.node)) {
// Fold any multiline parenthesized expression, including large `IN (...)`
// value lists. The opening line remains visible so the surrounding SQL
// structure is still readable while the contents can be collapsed.
if (node.name === "Parens") {
addMultilineFoldRange(state, ranges, node.from, queryScopeEnd(state, node.node));
}
if (node.name !== "Keyword") return;
Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB