diff --git a/apps/desktop/src/lib/__tests__/editorThemes.spec.ts b/apps/desktop/src/lib/__tests__/editorThemes.spec.ts index 5311d459de..572fc20753 100644 --- a/apps/desktop/src/lib/__tests__/editorThemes.spec.ts +++ b/apps/desktop/src/lib/__tests__/editorThemes.spec.ts @@ -1,7 +1,9 @@ import { describe, expect, it } from "vitest"; -import { buildEditorFontThemeRules, buildSqlCompletionThemeRules, editorDiagnosticColors, editorThemeAppearanceFor, resolveCustomThemeBackgrounds, resolveEditorTheme } from "@/lib/editor/editorThemes"; +import { buildEditorFontThemeRules, buildSqlCompletionThemeRules, editorDiagnosticColors, editorThemeAppearanceFor, resolveCustomThemeBackgrounds, resolveEditorTheme, SQL_BUILTIN_HIGHLIGHT_TAG } from "@/lib/editor/editorThemes"; import { DEFAULT_APP_CUSTOM_UI_COLORS, wcagContrastRatio, type AppThemePalette } from "@/lib/app/appTheme"; import type { EditorTheme } from "@/stores/settingsStore"; +import { createDbxCodeMirrorSqlDialect } from "@/lib/editor/codemirrorSqlDialect"; +import * as langSql from "@codemirror/lang-sql"; describe("resolveEditorTheme", () => { it("maps only the follow-app editor theme to application IDE palettes", () => { @@ -191,6 +193,34 @@ describe("SQL completion theme", () => { }); }); +describe("SQL builtin highlight tag", () => { + // #7950: count/date_format/etc. were added to the dialect builtin word lists (#7222) but + // never actually rendered in a distinct color, because the theme's highlight rule matched + // standard(variableName) while @codemirror/lang-sql tags builtin words as standard(name) — + // variableName is a *child* tag of name, so a rule keyed on the child never matches the + // token's actual (parent) tag. + it("gives builtin SQL functions their own highlight class, distinct from plain identifiers and keywords", async () => { + const { highlightTree } = await import("@lezer/highlight"); + const { HighlightStyle } = await import("@codemirror/language"); + const { tags } = await import("@lezer/highlight"); + const style = HighlightStyle.define([ + { tag: tags.keyword, color: "keyword" }, + { tag: [tags.name, tags.variableName], color: "variable" }, + { tag: SQL_BUILTIN_HIGHLIGHT_TAG, color: "builtin" }, + ]); + + const dialect = createDbxCodeMirrorSqlDialect(langSql, "postgres", "postgres"); + const doc = "select count(*) from t"; + const tree = dialect.language.parser.parse(doc); + const classesByToken = new Map(); + highlightTree(tree, style, (from, to, cls) => classesByToken.set(doc.slice(from, to), cls)); + + expect(classesByToken.get("count")).toBeDefined(); + expect(classesByToken.get("count")).not.toBe(classesByToken.get("t")); + expect(classesByToken.get("count")).not.toBe(classesByToken.get("select")); + }); +}); + describe("editor gutters", () => { it("keeps single line numbers vertically centered in the base rule", () => { const rules = buildEditorFontThemeRules(); diff --git a/apps/desktop/src/lib/editor/editorThemes.ts b/apps/desktop/src/lib/editor/editorThemes.ts index fa84263bb2..61da9441f4 100644 --- a/apps/desktop/src/lib/editor/editorThemes.ts +++ b/apps/desktop/src/lib/editor/editorThemes.ts @@ -4,6 +4,12 @@ import { customUiAppearance, type AppCustomUiColors, type AppThemeAppearance, ty import { HighlightStyle, syntaxHighlighting } from "@codemirror/language"; import { tags } from "@lezer/highlight"; +// @codemirror/lang-sql tags dialect builtin-list words (COUNT, DATE_FORMAT, ...) as +// standard(name) — not standard(variableName), which is a *child* tag of name and so never +// matches a token tagged with the parent. Shared so both editor theme builders below stay in +// sync with what the SQL tokenizer actually emits. +export const SQL_BUILTIN_HIGHLIGHT_TAG = tags.standard(tags.name); + type CodeMirrorStyleSpec = Parameters[0]; type LucideIconNode = Array<[string, Record]>; @@ -165,7 +171,7 @@ function createCustomTheme(EditorView: typeof import("@codemirror/view").EditorV { tag: tags.definition(tags.variableName), color: c.variable }, { tag: tags.function(tags.variableName), color: c.function }, { tag: tags.function(tags.propertyName), color: c.function }, - { tag: tags.standard(tags.variableName), color: c.builtin }, + { tag: SQL_BUILTIN_HIGHLIGHT_TAG, color: c.builtin }, { tag: tags.propertyName, color: c.property }, { tag: tags.operator, color: c.operator }, { tag: tags.compareOperator, color: c.operator }, @@ -563,7 +569,7 @@ function createIdeEditorTheme(EditorView: typeof import("@codemirror/view").Edit { tag: [tags.typeName, tags.typeOperator, tags.unit], color: c.type }, { tag: [tags.name, tags.variableName, tags.definition(tags.variableName)], color: c.variable }, { tag: [tags.function(tags.variableName), tags.function(tags.propertyName), tags.function(tags.name), tags.macroName], color: c.function }, - { tag: [tags.standard(tags.variableName), tags.special(tags.name)], color: c.builtin }, + { tag: [SQL_BUILTIN_HIGHLIGHT_TAG, tags.special(tags.name)], color: c.builtin }, { tag: [tags.propertyName, tags.labelName, tags.annotation], color: c.property }, { tag: [tags.operator, tags.compareOperator, tags.logicOperator, tags.arithmeticOperator, tags.derefOperator], color: c.operator }, { tag: [tags.punctuation, tags.separator, tags.paren, tags.brace, tags.bracket, tags.angleBracket], color: c.punctuation },