style(代码配色): True / False / None 和 NULL 改成关键字蓝;SQL 的 COUNT、AVG、ROUND 这些函数是函数色;编辑器的选区、行号、当前行、括号配对也换成 VS Code 的样子
Deploy / deploy (push) Canceled after 0s

True / None 原来和数字一个绿色,VS Code 里是 constant.language,和关键字同色。
SQL 的解析器把 COUNT 当关键字、把 avg 当普通名字,现在后面跟着括号的常用函数一律是函数色。

编辑器的框原来沿用 smoothy / One Dark(亮色选区是淡黄),现在照 VS Code 的默认值:选区淡蓝 /
深蓝、行号青色、当前行是一圈边框。当前行只在有焦点时画,免得自学一课十几个代码块每块第一行都框着。
暗色的底色仍用网站的。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-13 20:35:34 +08:00
1 parent 60e96f311e
commit a085351a2f
6 files changed
+109 -36

No files matched your search

+55 -6
View File
@@ -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<typeof syntaxTree>
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<CodeFamily, RegExp> = {
const FORMAT: Partial<Record<CodeFamily, RegExp>> = {
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<CodeFamily, RegExp> = {
const FORMAT_CLASS: Record<CodeFamily, string> = {
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(/(?<!\w)[A-Za-z_]\w*/g)) {
const end = m.index + m[0].length
if (SQL_FUNCTIONS.has(m[0].toLowerCase()) && /^\s*\(/.test(text.slice(end) + after)) {
marks.push({ from: m.index, to: end, cls: "tok-sql-function" })
}
}
marks.sort((a, b) => 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
+2 -1
View File
@@ -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))
}
+2 -1
View File
@@ -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")]
}
+4
View File
@@ -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);
+9 -3
View File
@@ -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
}
+37 -25
View File
@@ -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",