fix(editor): highlight quoted SQL identifiers as identifiers instead of strings (#12122) (#12630)

This commit is contained in:
lxk955
2026-10-19 23:37:33 +08:00
committed by GitHub
parent 5b6fbebe90
commit 127cf84e1b
4 changed files with 95 additions and 5 deletions
@@ -145,4 +145,26 @@ describe("codemirrorSqlDialect", () => {
expect(nodeNameAt(dialect, doubleQuoteStatement, '"escaped\\"quote"')).toBe("String");
expect(nodeNameAt(dialect, doubleQuoteStatement, "col")).toBe("Identifier");
});
it("tags QuotedIdentifier as tags.name so quoted columns/tables match unquoted identifiers (#12122)", async () => {
const { highlightTree, tags } = await import("@lezer/highlight");
const { HighlightStyle } = await import("@codemirror/language");
const style = HighlightStyle.define([
{ tag: tags.keyword, color: "keyword" },
{ tag: tags.string, color: "string" },
{ tag: tags.name, color: "identifier" },
]);
const mysqlDialect = createDbxCodeMirrorSqlDialect(langSql, "mysql", "mysql");
const doc = "SELECT `quoted_col`, plain_col, 'literal_string' FROM `tbl`";
const tree = mysqlDialect.language.parser.parse(doc);
const classes = new Map<string, string>();
highlightTree(tree, style, (from, to, cls) => classes.set(doc.slice(from, to), cls));
expect(classes.get("`quoted_col`")).toBeDefined();
expect(classes.get("`quoted_col`")).toBe(classes.get("plain_col"));
expect(classes.get("`quoted_col`")).toBe(classes.get("`tbl`"));
expect(classes.get("`quoted_col`")).not.toBe(classes.get("'literal_string'"));
});
});
+50 -1
View File
@@ -1,5 +1,17 @@
import { describe, expect, it } from "vitest";
import { buildEditorFontThemeRules, buildSqlCompletionThemeRules, EDITOR_FONT_FAMILY_CSS_VAR, editorDiagnosticColors, editorThemeAppearanceFor, IDE_EDITOR_THEMES, loadEditorTheme, resolveCustomThemeBackgrounds, resolveEditorTheme, SQL_BUILTIN_HIGHLIGHT_TAG } from "@/lib/editor/editorThemes";
import {
buildEditorFontThemeRules,
buildSqlCompletionThemeRules,
EDITOR_FONT_FAMILY_CSS_VAR,
editorDiagnosticColors,
editorThemeAppearanceFor,
IDE_EDITOR_THEMES,
loadEditorTheme,
resolveCustomThemeBackgrounds,
resolveEditorTheme,
SQL_BUILTIN_HIGHLIGHT_TAG,
SQL_QUOTED_IDENTIFIER_HIGHLIGHT_TAG,
} from "@/lib/editor/editorThemes";
import { DEFAULT_APP_CUSTOM_UI_COLORS, wcagContrastRatio, type AppThemePalette } from "@/lib/app/appTheme";
import { DEFAULT_CUSTOM_THEME_COLORS, type CustomThemeColors, type EditorTheme } from "@/stores/settingsStore";
import { createDbxCodeMirrorSqlDialect } from "@/lib/editor/codemirrorSqlDialect";
@@ -306,6 +318,43 @@ describe("SQL builtin highlight tag", () => {
});
});
describe("SQL quoted identifier highlighting (#12122)", () => {
it("highlights quoted identifiers with identifier/variable color, distinct from string literals", async () => {
const { highlightTree, tags } = await import("@lezer/highlight");
const { HighlightStyle } = await import("@codemirror/language");
const style = HighlightStyle.define([
{ tag: tags.keyword, color: "keyword" },
{ tag: tags.string, color: "string" },
{ tag: [tags.name, tags.variableName, SQL_QUOTED_IDENTIFIER_HIGHLIGHT_TAG], color: "variable" },
]);
const dialects = [
{ name: "mysql", sql: "SELECT `col`, col, 'val' FROM `tbl`" },
{ name: "postgres", sql: 'SELECT "col", col, \'val\' FROM "tbl"' },
{ name: "sqlserver", sql: "SELECT [col], col, 'val' FROM [tbl]" },
];
for (const { name, sql } of dialects) {
const dialect = createDbxCodeMirrorSqlDialect(langSql, name as any, name as any);
const tree = dialect.language.parser.parse(sql);
const classesByToken = new Map<string, string>();
highlightTree(tree, style, (from, to, cls) => classesByToken.set(sql.slice(from, to), cls));
const quotedCol = name === "mysql" ? "`col`" : name === "postgres" ? '"col"' : "[col]";
const quotedTbl = name === "mysql" ? "`tbl`" : name === "postgres" ? '"tbl"' : "[tbl]";
expect(classesByToken.get(quotedCol), `${name} quoted col`).toBe(classesByToken.get("col"));
expect(classesByToken.get(quotedCol), `${name} quoted col vs string`).not.toBe(classesByToken.get("'val'"));
expect(classesByToken.get(quotedTbl), `${name} quoted tbl`).toBe(classesByToken.get("col"));
}
});
it("maps SQL_QUOTED_IDENTIFIER_HIGHLIGHT_TAG to special(string) for theme compatibility", async () => {
const { tags } = await import("@lezer/highlight");
expect(SQL_QUOTED_IDENTIFIER_HIGHLIGHT_TAG).toBe(tags.special(tags.string));
});
});
describe("editor gutters", () => {
it("does not let the gutter minimum height cycle against the scroller content height", () => {
const rules = buildEditorFontThemeRules();
@@ -1,7 +1,16 @@
import { styleTags, tags } from "@lezer/highlight";
import type { SQLDialect } from "@codemirror/lang-sql";
import type { DatabaseType } from "@/types/database";
import { driverProfileSqlBuiltinTerms } from "@/lib/database/driverProfileExtensions";
// In SQL, quoted identifiers (e.g. `col`, "col", [col]) represent column, table,
// and schema names — not string literals (#12122). CodeMirror's SQL grammar tags
// QuotedIdentifier as special(string) by default, which themes highlight as a
// string literal. We retag QuotedIdentifier as tags.name so all themes (both DBX
// custom/IDE themes and standard third-party themes) highlight quoted identifiers
// identically to unquoted identifiers.
const SQL_DIALECT_LANGUAGE_PROPS = [styleTags({ QuotedIdentifier: tags.name })];
export type CodeMirrorSqlDialectName = "mysql" | "postgres" | "sqlserver" | "clickhouse" | "soql";
export function supportsQueryEditorSqlLanguage(databaseType?: DatabaseType): boolean {
@@ -368,6 +377,8 @@ export function createDbxCodeMirrorSqlDialect(langSql: CodeMirrorSqlLanguageModu
caseInsensitiveIdentifiers: true,
identifierQuotes: "",
doubleDollarQuotedStrings: false,
}).configureLanguage({
props: SQL_DIALECT_LANGUAGE_PROPS,
});
}
const baseDialect = codeMirrorBaseDialect(langSql, dialectName, databaseType);
@@ -409,5 +420,7 @@ export function createDbxCodeMirrorSqlDialect(langSql: CodeMirrorSqlLanguageModu
: {}),
...(isPlsql ? { doubleQuotedStrings: false } : {}),
doubleDollarQuotedStrings: false,
}).configureLanguage({
props: SQL_DIALECT_LANGUAGE_PROPS,
});
}
+10 -4
View File
@@ -10,6 +10,13 @@ import { tags } from "@lezer/highlight";
// sync with what the SQL tokenizer actually emits.
export const SQL_BUILTIN_HIGHLIGHT_TAG = tags.standard(tags.name);
// @codemirror/lang-sql tags quoted identifiers (e.g. `col`, "col", [col]) as
// special(string) by default. In SQL, quoted identifiers represent columns, tables,
// and database objects — not string literals (#12122). Both the SQL dialect parser
// configuration and DBX theme highlight styles map this token to identifier/variable
// color instead of string literal color.
export const SQL_QUOTED_IDENTIFIER_HIGHLIGHT_TAG = tags.special(tags.string);
type CodeMirrorStyleSpec = Parameters<typeof import("@codemirror/view").EditorView.theme>[0];
type LucideIconNode = Array<[string, Record<string, string>]>;
@@ -160,7 +167,7 @@ function createCustomTheme(EditorView: typeof import("@codemirror/view").EditorV
{ tag: tags.moduleKeyword, color: c.keyword },
{ tag: tags.operatorKeyword, color: c.keyword },
{ tag: tags.string, color: c.string },
{ tag: tags.special(tags.string), color: c.string },
{ tag: SQL_QUOTED_IDENTIFIER_HIGHLIGHT_TAG, color: c.variable },
{ tag: tags.number, color: c.number },
{ tag: tags.integer, color: c.number },
{ tag: tags.float, color: c.number },
@@ -204,7 +211,6 @@ function createCustomTheme(EditorView: typeof import("@codemirror/view").EditorV
{ tag: tags.namespace, color: c.className },
{ tag: tags.macroName, color: c.function },
{ tag: tags.literal, color: c.string },
{ tag: tags.special(tags.string), color: c.string },
{ tag: tags.regexp, color: c.string },
{ tag: tags.escape, color: c.string },
{ tag: tags.processingInstruction, color: c.keyword },
@@ -569,11 +575,11 @@ function createIdeEditorTheme(EditorView: typeof import("@codemirror/view").Edit
const highlightStyle = HighlightStyle.define([
{ tag: [tags.keyword, tags.controlKeyword, tags.definitionKeyword, tags.moduleKeyword, tags.operatorKeyword, tags.modifier, tags.bool, tags.null], color: c.keyword, ...(c.keywordBold ? { fontWeight: "bold" } : {}) },
{ tag: [tags.string, tags.special(tags.string), tags.regexp, tags.escape, tags.inserted], color: c.string, ...(c.stringBold ? { fontWeight: "bold" } : {}) },
{ tag: [tags.string, tags.regexp, tags.escape, tags.inserted], color: c.string, ...(c.stringBold ? { fontWeight: "bold" } : {}) },
{ tag: [tags.number, tags.integer, tags.float], color: c.number, ...(c.numberBold ? { fontWeight: "bold" } : {}) },
{ tag: [tags.comment, tags.lineComment, tags.blockComment, tags.quote], color: c.comment, fontStyle: "italic" },
{ tag: [tags.typeName, tags.typeOperator, tags.unit], color: c.type },
{ tag: [tags.name, tags.variableName, tags.definition(tags.variableName)], color: c.variable },
{ tag: [tags.name, tags.variableName, tags.definition(tags.variableName), SQL_QUOTED_IDENTIFIER_HIGHLIGHT_TAG], color: c.variable },
{ tag: [tags.function(tags.variableName), tags.function(tags.propertyName), tags.function(tags.name), tags.macroName], color: c.function },
{ tag: [SQL_BUILTIN_HIGHLIGHT_TAG, tags.special(tags.name)], color: c.builtin },
{ tag: [tags.propertyName, tags.labelName, tags.annotation], color: c.property },