mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-20 00:17:44 +00:00
fix(editor): fold Elasticsearch REST requests individually
Closes #8065
This commit is contained in:
@@ -3,12 +3,12 @@ import { ensureSyntaxTree, foldable } from "@codemirror/language";
|
||||
import { Compartment, EditorState } from "@codemirror/state";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createDbxCodeMirrorSqlDialect } from "@/lib/editor/codemirrorSqlDialect";
|
||||
import { collectUnionBranchFoldRanges, sqlBlockFoldService } from "@/lib/editor/codemirrorSqlBlockFolding";
|
||||
import { collectUnionBranchFoldRanges, createSqlBlockFoldService, sqlBlockFoldService } from "@/lib/editor/codemirrorSqlBlockFolding";
|
||||
|
||||
function stateFor(doc: string, dialectName: "mysql" | "postgres" | "sqlserver" = "mysql"): EditorState {
|
||||
function stateFor(doc: string, dialectName: "mysql" | "postgres" | "sqlserver" = "mysql", folding = sqlBlockFoldService): EditorState {
|
||||
const state = EditorState.create({
|
||||
doc,
|
||||
extensions: [langSql.sql({ dialect: createDbxCodeMirrorSqlDialect(langSql, dialectName) }), sqlBlockFoldService],
|
||||
extensions: [langSql.sql({ dialect: createDbxCodeMirrorSqlDialect(langSql, dialectName) }), folding],
|
||||
});
|
||||
ensureSyntaxTree(state, doc.length, 5_000);
|
||||
return state;
|
||||
@@ -21,6 +21,65 @@ function foldedTextAtLine(state: EditorState, lineNumber: number): string | null
|
||||
}
|
||||
|
||||
describe("sqlBlockFoldService", () => {
|
||||
it.each(["elasticsearch", "easysearch", "meilisearch"] as const)("folds one %s REST request without a semicolon", (databaseType) => {
|
||||
const sql = `POST /orders/_search
|
||||
{
|
||||
"query": { "match_all": {} }
|
||||
}`;
|
||||
const state = stateFor(sql, "mysql", createSqlBlockFoldService(databaseType));
|
||||
|
||||
expect(sql).not.toContain(";");
|
||||
expect(foldedTextAtLine(state, 1)).toBe('\n{\n "query": { "match_all": {} }\n}');
|
||||
});
|
||||
|
||||
it("folds multiple REST requests independently", () => {
|
||||
const sql = `POST /orders/_search
|
||||
{
|
||||
"query": { "match_all": {} }
|
||||
}
|
||||
|
||||
GET /orders/_count
|
||||
{
|
||||
"query": { "term": { "status": "paid" } }
|
||||
}`;
|
||||
const state = stateFor(sql);
|
||||
|
||||
expect(sql).not.toContain(";");
|
||||
expect(foldedTextAtLine(state, 1)).toBe('\n{\n "query": { "match_all": {} }\n}');
|
||||
expect(foldedTextAtLine(state, 6)).toBe('\n{\n "query": { "term": { "status": "paid" } }\n}');
|
||||
});
|
||||
|
||||
it("keeps REST request comments and blank lines outside individual folds", () => {
|
||||
const sql = `# first request
|
||||
POST /orders/_search
|
||||
{
|
||||
"query": { "match_all": {} }
|
||||
}
|
||||
|
||||
// second request
|
||||
POST /orders/_search
|
||||
{
|
||||
"query": { "term": { "status": "paid" } }
|
||||
}`;
|
||||
const state = stateFor(sql);
|
||||
|
||||
expect(foldedTextAtLine(state, 2)).toBe('\n{\n "query": { "match_all": {} }\n}');
|
||||
expect(foldedTextAtLine(state, 8)).toBe('\n{\n "query": { "term": { "status": "paid" } }\n}');
|
||||
});
|
||||
|
||||
it("keeps ordinary SQL block and CASE folding unchanged", () => {
|
||||
const sql = `BEGIN
|
||||
SELECT CASE
|
||||
WHEN status = 'paid' THEN 1
|
||||
ELSE 0
|
||||
END;
|
||||
END`;
|
||||
const state = stateFor(sql);
|
||||
|
||||
expect(foldedTextAtLine(state, 1)).toContain("SELECT CASE");
|
||||
expect(foldedTextAtLine(state, 2)).toBe("\n WHEN status = 'paid' THEN 1\n ELSE 0\n ");
|
||||
});
|
||||
|
||||
it("folds a BEGIN...END block, from the end of the BEGIN line to the start of END", () => {
|
||||
const sql = "CREATE PROCEDURE p()\nBEGIN\n SELECT 1;\nEND";
|
||||
const state = stateFor(sql);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { foldService, syntaxTree } from "@codemirror/language";
|
||||
import type { EditorState } from "@codemirror/state";
|
||||
import { elasticsearchRestRequestRanges } from "@/lib/sql/sqlStatementRanges";
|
||||
import type { DatabaseType } from "@/types/database";
|
||||
|
||||
// `@lezer/common` is only a transitive dependency here (see sqlSyntaxTreeWindow.ts's comment on
|
||||
// the same pattern), so derive the node types structurally instead of importing them.
|
||||
@@ -62,7 +64,7 @@ function isSqlServerTransactionBegin(state: EditorState, tokens: SyntaxNode[], i
|
||||
// watcher), and when Lezer's incremental parser finishes covering more of a large document in the
|
||||
// background -- both produce a new `Tree` without a new `Text`, so keying on `Text` alone would
|
||||
// keep serving ranges computed from a stale or partial parse.
|
||||
const rangeCache = new WeakMap<Tree, Map<number, FoldRange>>();
|
||||
const rangeCache = new WeakMap<Tree, Map<string, Map<number, FoldRange>>>();
|
||||
|
||||
function addMultilineFoldRange(state: EditorState, ranges: Map<number, FoldRange>, openerPosition: number, endPosition: number, overwrite = false) {
|
||||
const openerLine = state.doc.lineAt(openerPosition);
|
||||
@@ -154,12 +156,32 @@ function addQueryStructureFoldRanges(state: EditorState, tree: Tree, ranges: Map
|
||||
}
|
||||
}
|
||||
|
||||
function computeBlockFoldRanges(state: EditorState): Map<number, FoldRange> {
|
||||
function addRestRequestFoldRanges(state: EditorState, ranges: Map<number, FoldRange>, databaseType?: DatabaseType): boolean {
|
||||
const requests = elasticsearchRestRequestRanges(state.doc.toString(), databaseType ?? "elasticsearch");
|
||||
if (requests.length === 0) return false;
|
||||
|
||||
for (const request of requests) {
|
||||
addMultilineFoldRange(state, ranges, request.from, request.to);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function computeBlockFoldRanges(state: EditorState, databaseType?: DatabaseType): Map<number, FoldRange> {
|
||||
const tree = syntaxTree(state);
|
||||
const cached = rangeCache.get(tree);
|
||||
const cacheByDatabaseType = rangeCache.get(tree);
|
||||
const cacheKey = databaseType ?? "auto-detect";
|
||||
const cached = cacheByDatabaseType?.get(cacheKey);
|
||||
if (cached) return cached;
|
||||
|
||||
const byOpenerLine = new Map<number, FoldRange>();
|
||||
const isRestDocument = addRestRequestFoldRanges(state, byOpenerLine, databaseType);
|
||||
if (isRestDocument) {
|
||||
const nextCacheByDatabaseType = cacheByDatabaseType ?? new Map<string, Map<number, FoldRange>>();
|
||||
nextCacheByDatabaseType.set(cacheKey, byOpenerLine);
|
||||
rangeCache.set(tree, nextCacheByDatabaseType);
|
||||
return byOpenerLine;
|
||||
}
|
||||
|
||||
const tokens: SyntaxNode[] = [];
|
||||
tree.iterate({
|
||||
enter(node) {
|
||||
@@ -197,12 +219,19 @@ function computeBlockFoldRanges(state: EditorState): Map<number, FoldRange> {
|
||||
|
||||
addQueryStructureFoldRanges(state, tree, byOpenerLine);
|
||||
|
||||
rangeCache.set(tree, byOpenerLine);
|
||||
const nextCacheByDatabaseType = cacheByDatabaseType ?? new Map<string, Map<number, FoldRange>>();
|
||||
nextCacheByDatabaseType.set(cacheKey, byOpenerLine);
|
||||
rangeCache.set(tree, nextCacheByDatabaseType);
|
||||
return byOpenerLine;
|
||||
}
|
||||
|
||||
/** Adds procedural-block, query-parenthesis, and UNION-branch folds to the SQL editor. */
|
||||
export const sqlBlockFoldService = foldService.of((state, lineStart) => {
|
||||
const range = computeBlockFoldRanges(state).get(state.doc.lineAt(lineStart).number);
|
||||
return range ?? null;
|
||||
});
|
||||
/** Creates folding for REST requests, procedural blocks, query parentheses, and UNION branches. */
|
||||
export function createSqlBlockFoldService(databaseType?: DatabaseType) {
|
||||
return foldService.of((state, lineStart) => {
|
||||
const range = computeBlockFoldRanges(state, databaseType).get(state.doc.lineAt(lineStart).number);
|
||||
return range ?? null;
|
||||
});
|
||||
}
|
||||
|
||||
/** Public compatibility extension with REST request auto-detection for text editors. */
|
||||
export const sqlBlockFoldService = createSqlBlockFoldService();
|
||||
|
||||
Reference in New Issue
Block a user