From a085351a2fdbed8f9b94d9c8df75f6fe60785614 Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Tue, 13 Oct 2026 20:35:34 +0800 Subject: [PATCH] =?UTF-8?q?style(=E4=BB=A3=E7=A0=81=E9=85=8D=E8=89=B2):=20?= =?UTF-8?q?True=20/=20False=20/=20None=20=E5=92=8C=20NULL=20=E6=94=B9?= =?UTF-8?q?=E6=88=90=E5=85=B3=E9=94=AE=E5=AD=97=E8=93=9D=EF=BC=9BSQL=20?= =?UTF-8?q?=E7=9A=84=20COUNT=E3=80=81AVG=E3=80=81ROUND=20=E8=BF=99?= =?UTF-8?q?=E4=BA=9B=E5=87=BD=E6=95=B0=E6=98=AF=E5=87=BD=E6=95=B0=E8=89=B2?= =?UTF-8?q?=EF=BC=9B=E7=BC=96=E8=BE=91=E5=99=A8=E7=9A=84=E9=80=89=E5=8C=BA?= =?UTF-8?q?=E3=80=81=E8=A1=8C=E5=8F=B7=E3=80=81=E5=BD=93=E5=89=8D=E8=A1=8C?= =?UTF-8?q?=E3=80=81=E6=8B=AC=E5=8F=B7=E9=85=8D=E5=AF=B9=E4=B9=9F=E6=8D=A2?= =?UTF-8?q?=E6=88=90=20VS=20Code=20=E7=9A=84=E6=A0=B7=E5=AD=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit True / None 原来和数字一个绿色,VS Code 里是 constant.language,和关键字同色。 SQL 的解析器把 COUNT 当关键字、把 avg 当普通名字,现在后面跟着括号的常用函数一律是函数色。 编辑器的框原来沿用 smoothy / One Dark(亮色选区是淡黄),现在照 VS Code 的默认值:选区淡蓝 / 深蓝、行号青色、当前行是一圈边框。当前行只在有焦点时画,免得自学一课十几个代码块每块第一行都框着。 暗色的底色仍用网站的。 Co-Authored-By: Claude Opus 5.5 --- apps/web/src/shared/codeMarks.ts | 61 ++++++++++++++++++-- apps/web/src/shared/extensions/codeMarks.ts | 3 +- apps/web/src/shared/extensions/language.ts | 3 +- apps/web/src/shared/highlight.css | 4 ++ apps/web/src/shared/highlight.ts | 12 +++- apps/web/src/shared/themes/codeTheme.ts | 62 ++++++++++++--------- 6 files changed, 109 insertions(+), 36 deletions(-) diff --git a/apps/web/src/shared/codeMarks.ts b/apps/web/src/shared/codeMarks.ts index bf6fc94..5699c5c 100644 --- a/apps/web/src/shared/codeMarks.ts +++ b/apps/web/src/shared/codeMarks.ts @@ -12,12 +12,14 @@ * 不标的话 `“你好”` 会被认成变量名,看着和正常代码一样。 * - Python 的内置类型(int、str、list…):照 VS Code 一律是类型色。lezer 只按位置分, * `int(input())` 里是函数、`map(int, …)` 里是变量,同一行两个颜色。 + * - SQL 的函数(COUNT、AVG、ROUND…,后面跟着括号的):照 VS Code 是函数色。lezer 的 SQL 解析器 + * 把 COUNT 当关键字、把 avg 当普通名字,同是聚合函数一个蓝一个没颜色。 */ import type { syntaxTree } from "@codemirror/language" type Tree = ReturnType -export type CodeFamily = "c" | "python" +export type CodeFamily = "c" | "python" | "sql" export interface Mark { from: number @@ -25,7 +27,7 @@ export interface Mark { cls: string } -const FORMAT: Record = { +const FORMAT: Partial> = { c: /%(\d+\$)?[#0\- +']*(-?\d+|\*)?(\.(-?\d+|\*)?)?(hh|h|ll|l|j|t|z|L)?[diouxXeEfFgGaAcspn%]/g, python: /%(\([\w\s]*\))?[-+#0 ]*(\d+|\*)?(\.(\d+|\*)?)?[hlL]?[diouxXeEfFgGcrsa%]/g, } @@ -33,6 +35,7 @@ const FORMAT: Record = { const FORMAT_CLASS: Record = { c: "tok-format", python: "tok-format-py", + sql: "", } // 全角的 ASCII(!(),:;?a1…)、全角空格、、。〈〉《》「」【】、中文引号 @@ -61,21 +64,55 @@ const PY_BUILTIN_TYPES = new Set([ "type", ]) +// VS Code 的 SQL 语法里 support.function 的那批(SQLite 认的) +const SQL_FUNCTIONS = new Set([ + "abs", + "avg", + "coalesce", + "count", + "date", + "datetime", + "group_concat", + "ifnull", + "instr", + "length", + "lower", + "ltrim", + "max", + "min", + "nullif", + "random", + "replace", + "round", + "rtrim", + "strftime", + "substr", + "sum", + "time", + "total", + "trim", + "upper", +]) + function matches(text: string, re: RegExp, cls: string): Mark[] { return [...text.matchAll(re)].map((m) => ({ from: m.index, to: m.index + m[0].length, cls })) } /** * 一个词(lezer 吐出的一段文字和它的类名)上要补的标记,位置相对 text 开头。 - * prev 是这段文字前面那个字符,用来排除 `obj.int` 这种属性 + * prev 是这段文字前面那个字符(排除 `obj.int` 这种属性),after 是后面紧跟的一小截(看是不是 `(`) */ export function extraMarks( text: string, classes: string, family: CodeFamily, prev: string, + after: string, ): Mark[] { - if (classes.includes("tok-string")) return matches(text, FORMAT[family], FORMAT_CLASS[family]) + if (classes.includes("tok-string")) { + const re = FORMAT[family] + return re ? matches(text, re, FORMAT_CLASS[family]) : [] + } if (classes.includes("tok-comment")) return [] if ( family === "python" && @@ -85,7 +122,18 @@ export function extraMarks( ) { return [{ from: 0, to: text.length, cls: "tok-builtin-type" }] } - return matches(text, FULL_WIDTH, "tok-fullwidth") + const marks = matches(text, FULL_WIDTH, "tok-fullwidth") + // 没类名的部分是一长截(`), round(avg(score), `),所以在里面逐个找词 + if (family === "sql" && (classes === "" || classes === "tok-keyword")) { + for (const m of text.matchAll(/(? a.from - b.from) + } + return marks } /** @@ -120,10 +168,11 @@ export function splitMarks( marksFor: string, family: CodeFamily, prev: string, + after: string, put: (text: string, classes: string) => void, ) { let at = 0 - for (const m of extraMarks(text, marksFor, family, prev)) { + for (const m of extraMarks(text, marksFor, family, prev, after)) { if (m.from > at) put(text.slice(at, m.from), classes) put(text.slice(m.from, m.to), classes ? `${classes} ${m.cls}` : m.cls) at = m.to diff --git a/apps/web/src/shared/extensions/codeMarks.ts b/apps/web/src/shared/extensions/codeMarks.ts index 0ef1ffc..2940a65 100644 --- a/apps/web/src/shared/extensions/codeMarks.ts +++ b/apps/web/src/shared/extensions/codeMarks.ts @@ -6,7 +6,7 @@ import { type CodeFamily, extraMarks, fakeStrings } from "../codeMarks" import { codeHighlighter } from "../highlight" /** - * 编辑器里补的标记(%d、全角标点、Python 内置类型),规则见 shared/codeMarks.ts。 + * 编辑器里补的标记(%d、全角标点、Python 内置类型、SQL 函数),规则见 shared/codeMarks.ts。 * 和静态代码一样按 codeHighlighter 分出来的词逐个过 extraMarks();只扫看得见的部分。 */ export function codeMarks(family: CodeFamily): Extension { @@ -31,6 +31,7 @@ export function codeMarks(family: CodeFamily): Extension { classes, family, prev(start), + view.state.doc.sliceString(end, end + 20), )) { ranges.push(deco(m.cls).range(start + m.from, start + m.to)) } diff --git a/apps/web/src/shared/extensions/language.ts b/apps/web/src/shared/extensions/language.ts index b0b669c..d873ac8 100644 --- a/apps/web/src/shared/extensions/language.ts +++ b/apps/web/src/shared/extensions/language.ts @@ -14,7 +14,8 @@ import { ojPythonLanguage } from "./pythonLanguage" * Java / Golang / JavaScript 没有单独的包,落到 cpp(),是既有行为,不是遗漏。 */ export function languageExtension(language: LANGUAGE): Extension { - if (language === "SQL") return sql({ dialect: SQLite, upperCaseKeywords: true }) + if (language === "SQL") + return [sql({ dialect: SQLite, upperCaseKeywords: true }), codeMarks("sql")] if (language === "Python") { return [new LanguageSupport(ojPythonLanguage, python().support), codeMarks("python")] } diff --git a/apps/web/src/shared/highlight.css b/apps/web/src/shared/highlight.css index b1aedd0..92cf7af 100644 --- a/apps/web/src/shared/highlight.css +++ b/apps/web/src/shared/highlight.css @@ -83,6 +83,10 @@ html.dark { .tok-builtin-type * { color: var(--code-type); } +.tok-sql-function, +.tok-sql-function * { + color: var(--code-function); +} .tok-fullwidth, .tok-fullwidth * { color: var(--code-invalid); diff --git a/apps/web/src/shared/highlight.ts b/apps/web/src/shared/highlight.ts index 4664602..2fe0632 100644 --- a/apps/web/src/shared/highlight.ts +++ b/apps/web/src/shared/highlight.ts @@ -20,12 +20,16 @@ export const codeHighlighter = tagHighlighter([ // 分法照 VS Code:if / return / #include 这类管流程的词单独一色,`int` / `struct` 是普通关键字, // 自定义的类型(struct 名)才是类型色;运算符不上色,变量名上色 { tag: [t.controlKeyword, t.moduleKeyword, t.processingInstruction], class: "tok-control" }, - { tag: [t.keyword, t.modifier, t.standard(t.typeName)], class: "tok-keyword" }, + // True / False / None / self / NULL 照 VS Code 是 constant.language,和关键字一个颜色 + { + tag: [t.keyword, t.modifier, t.standard(t.typeName), t.bool, t.null, t.atom, t.self], + class: "tok-keyword", + }, { tag: t.escape, class: "tok-escape" }, { tag: [t.function(t.variableName), t.function(t.propertyName)], class: "tok-function" }, { tag: [t.variableName, t.propertyName], class: "tok-variable" }, { tag: t.string, class: "tok-string" }, - { tag: [t.number, t.bool, t.null, t.atom, t.self], class: "tok-number" }, + { tag: t.number, class: "tok-number" }, { tag: [t.typeName, t.className, t.namespace, t.standard(t.name)], class: "tok-type" }, { tag: t.comment, class: "tok-comment" }, { tag: t.invalid, class: "tok-invalid" }, @@ -59,6 +63,7 @@ function familyFor(lang?: string): CodeFamily | null { const parser = parserFor(lang) if (parser === cppLanguage.parser) return "c" if (parser === ojPythonLanguage.parser) return "python" + if (parser === SQLite.language.parser) return "sql" return null } @@ -90,7 +95,8 @@ function walk( const emit = (text: string, classes: string) => { if (family) { const marks = fake.some((r) => pos >= r.from && pos < r.to) ? "" : classes - splitMarks(text, classes, marks, family, code[pos - 1] ?? "", put) + const after = code.slice(pos + text.length, pos + text.length + 20) + splitMarks(text, classes, marks, family, code[pos - 1] ?? "", after, put) } else put(text, classes) pos += text.length } diff --git a/apps/web/src/shared/themes/codeTheme.ts b/apps/web/src/shared/themes/codeTheme.ts index 32e1fa7..e04f62f 100644 --- a/apps/web/src/shared/themes/codeTheme.ts +++ b/apps/web/src/shared/themes/codeTheme.ts @@ -6,10 +6,17 @@ import { codeHighlighter } from "../highlight" /** * 编辑器的外观:亮 / 暗两套只管底色、光标、选区、行号这些「框」, * 代码的颜色两套都是 codeHighlighter 打的 tok-* 类名,跟着 html.dark 在 highlight.css 里切。 - * 框沿用原来的 smoothy(亮)和 One Dark(暗),只把代码配色换成了 VS Code 的 Light+ / Dark+。 + * 框的颜色照 VS Code 的默认值(src/vs/editor/common/core/editorColorRegistry.ts 等), + * 只有暗色的底色用网站自己的,不用 VS Code 的 #1E1E1E。 + * + * 当前行照 VS Code 是一圈边框不是底色,并且只在编辑器有焦点时画: + * 自学一课里有十几个代码块,不然每块第一行都框着。 */ const highlighting = syntaxHighlighting(codeHighlighter) +// CodeMirror 自带的选区规则带着 > .cm-scroller > .cm-selectionLayer,选择器短了压不过它 +const focusedSelection = "&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground" + const light = EditorView.theme({ "&": { backgroundColor: "#ffffff", @@ -17,16 +24,24 @@ const light = EditorView.theme({ }, ".cm-content": { caretColor: "#000000" }, ".cm-cursor, .cm-dropCursor": { borderLeftColor: "#000000" }, - "&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": { - backgroundColor: "#FFFD0054", + ".cm-selectionBackground, .cm-content ::selection": { backgroundColor: "#ADD6FF80" }, + [focusedSelection]: { backgroundColor: "#ADD6FF" }, + ".cm-selectionMatch": { backgroundColor: "#ADD6FF80" }, + "&.cm-focused .cm-matchingBracket": { + backgroundColor: "#0064001a", + outline: "1px solid #B9B9B9", }, - ".cm-activeLine": { backgroundColor: "#00000008" }, + ".cm-searchMatch": { backgroundColor: "#EA5C0055" }, + ".cm-searchMatch.cm-searchMatch-selected": { backgroundColor: "#A8AC94" }, + ".cm-activeLine": { backgroundColor: "transparent" }, + "&.cm-focused .cm-activeLine": { boxShadow: "inset 0 0 0 2px #eeeeee" }, ".cm-gutters": { backgroundColor: "#ffffff", borderRight: "none", - color: "#00000070", + color: "#237893", }, - ".cm-activeLineGutter": { backgroundColor: "#00000008" }, + ".cm-activeLineGutter": { backgroundColor: "transparent" }, + "&.cm-focused .cm-activeLineGutter": { color: "#0B216F" }, ".cm-tooltip": { border: "1px solid #e0e0e0", backgroundColor: "#f8f8f8", @@ -40,11 +55,9 @@ const light = EditorView.theme({ }) const darkBackground = "#26262a", - highlightBackground = "#2c313a", background = "#101014", // naive-ui tooltipBackground = "#353a42", - stone = "#7d8799", - cursor = "#528bff" + highlightBackground = "#2c313a" const dark = EditorView.theme( { @@ -52,30 +65,29 @@ const dark = EditorView.theme( color: "var(--code-fg)", backgroundColor: background, }, - ".cm-content": { caretColor: cursor }, - ".cm-cursor, .cm-dropCursor": { borderLeftColor: cursor }, - "&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": { - backgroundColor: "#3E4451", + ".cm-content": { caretColor: "#AEAFAD" }, + ".cm-cursor, .cm-dropCursor": { borderLeftColor: "#AEAFAD" }, + ".cm-selectionBackground, .cm-content ::selection": { backgroundColor: "#264F7880" }, + [focusedSelection]: { backgroundColor: "#264F78" }, + ".cm-selectionMatch": { backgroundColor: "#ADD6FF26" }, + "&.cm-focused .cm-matchingBracket": { + backgroundColor: "#0064001a", + outline: "1px solid #888888", }, + ".cm-searchMatch": { backgroundColor: "#EA5C0055" }, + ".cm-searchMatch.cm-searchMatch-selected": { backgroundColor: "#515C6A" }, ".cm-panels": { backgroundColor: darkBackground, color: "var(--code-fg)" }, ".cm-panels.cm-panels-top": { borderBottom: "2px solid black" }, ".cm-panels.cm-panels-bottom": { borderTop: "2px solid black" }, - ".cm-searchMatch": { - backgroundColor: "#72a1ff59", - outline: "1px solid #457dff", - }, - ".cm-searchMatch.cm-searchMatch-selected": { backgroundColor: "#6199ff2f" }, - ".cm-activeLine": { backgroundColor: "#6699ff0b" }, - ".cm-selectionMatch": { backgroundColor: "#aafe661a" }, - "&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket": { - backgroundColor: "#bad0f847", - }, + ".cm-activeLine": { backgroundColor: "transparent" }, + "&.cm-focused .cm-activeLine": { boxShadow: "inset 0 0 0 2px #282828" }, ".cm-gutters": { backgroundColor: background, - color: stone, + color: "#858585", border: "none", }, - ".cm-activeLineGutter": { backgroundColor: highlightBackground }, + ".cm-activeLineGutter": { backgroundColor: "transparent" }, + "&.cm-focused .cm-activeLineGutter": { color: "#C6C6C6" }, ".cm-foldPlaceholder": { backgroundColor: "transparent", border: "none",