From 8305d63ea1dcd9068dfa52ce01a9c67f8b65a5d3 Mon Sep 17 00:00:00 2001 From: zipg Date: Wed, 2 Sep 2026 20:32:59 +0800 Subject: [PATCH] perf(editor): single-pass table name semantic coloring --- .../lib/__tests__/sql/semantic/model.spec.ts | 10 +++++++++ apps/desktop/src/lib/sql/semantic/model.ts | 21 +++++++------------ 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/apps/desktop/src/lib/__tests__/sql/semantic/model.spec.ts b/apps/desktop/src/lib/__tests__/sql/semantic/model.spec.ts index e16fbc03d2..4f13c01449 100644 --- a/apps/desktop/src/lib/__tests__/sql/semantic/model.spec.ts +++ b/apps/desktop/src/lib/__tests__/sql/semantic/model.spec.ts @@ -358,6 +358,16 @@ describe("sqlSemanticModel baseline fixtures", () => { expect(spans.map((span) => sql.slice(span.start, span.end))).toEqual(["users", "orders", "audit_log", "events"]); }); + it("keeps table highlighting linear for a 24,000-line Oracle select list", () => { + const sql = `SELECT\n${Array.from({ length: 23_997 }, (_, index) => ` t.col_${index} AS alias_${index},`).join("\n")}\n t.final_col\nFROM app.huge_table t`; + const startedAt = performance.now(); + const spans = sqlSemanticTableNameSpans(sql, { databaseType: "oracle" }); + const elapsedMs = performance.now() - startedAt; + + expect(spans.map((span) => sql.slice(span.start, span.end))).toEqual(["huge_table"]); + expect(elapsedMs).toBeLessThan(2_000); + }); + it("highlights mutation targets but ignores strings, comments, and table functions", () => { const sql = "UPDATE users SET name = 'FROM fake'; INSERT INTO audit_log(id) VALUES (1); -- FROM ignored\nSELECT * FROM read_csv('events.csv') e"; const spans = sqlSemanticTableNameSpans(sql); diff --git a/apps/desktop/src/lib/sql/semantic/model.ts b/apps/desktop/src/lib/sql/semantic/model.ts index ac19273194..816c2661fb 100644 --- a/apps/desktop/src/lib/sql/semantic/model.ts +++ b/apps/desktop/src/lib/sql/semantic/model.ts @@ -131,18 +131,6 @@ function updateIntroducesMutationTarget(tokens: readonly SqlSemanticToken[], upd return true; } -function commaContinuesTableList(tokens: readonly SqlSemanticToken[], commaIndex: number): boolean { - const comma = tokens[commaIndex]; - if (comma?.text !== ",") return false; - for (let index = commaIndex - 1; index >= 0; index -= 1) { - const item = tokens[index]; - if (!item || item.depth !== comma.depth || item.kind !== "word") continue; - if (item.normalized === "from") return true; - if (item.normalized === "select" || item.normalized === "join" || TABLE_INTRODUCERS.has(item.normalized) || CLAUSE_BOUNDARIES.has(item.normalized)) return false; - } - return false; -} - /** * Finds concrete table-name tokens for visual highlighting without consulting * metadata. Only the final identifier in a qualified name is returned, so @@ -153,11 +141,18 @@ export function sqlSemanticTableNameSpans(sql: string, options: SqlSemanticBuild const tokens = significantTokens(tokenizeSqlSemantic(sql, dialect.id)); const spans: SqlSemanticSpan[] = []; const seen = new Set(); + const commaContinuesTableListByDepth = new Map(); for (let index = 0; index < tokens.length; index += 1) { const item = tokens[index]; + if (!item) continue; + const commaContinuesTableList = item.text === "," && commaContinuesTableListByDepth.get(item.depth) === true; + if (item.kind === "word") { + if (item.normalized === "from") commaContinuesTableListByDepth.set(item.depth, true); + else if (item.normalized === "select" || item.normalized === "join" || TABLE_INTRODUCERS.has(item.normalized) || CLAUSE_BOUNDARIES.has(item.normalized)) commaContinuesTableListByDepth.set(item.depth, false); + } const introduced = item?.kind === "word" && TABLE_INTRODUCERS.has(item.normalized) && (item.normalized !== "update" || updateIntroducesMutationTarget(tokens, index)); - if (!introduced && !commaContinuesTableList(tokens, index)) continue; + if (!introduced && !commaContinuesTableList) continue; let target = index + 1; while (TABLE_TARGET_MODIFIERS.has(tokens[target]?.normalized ?? "")) target += 1;