style(代码配色): printf 里的 %d、%.2f 和 Python f-string 的 { } 有了自己的颜色;Python 的 from / as / with 和 import 一个颜色,跟 VS Code 一样
lezer 把整个字符串当一个词,占位符另外扫一遍(shared/formatPlaceholder.ts),编辑器挂装饰、 静态代码拆 span,用同一份正则。Python 解析器补了几条 styleTags,编辑器和静态代码共用。 CodeEditor.vue 原来自己抄了一份语言选择,改成用 languageExtension。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a559af0aa9
commit
3b04afc275
6 files changed
+134
-14
No files matched your search
@@ -1,7 +1,4 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { cpp } from "@codemirror/lang-cpp"
|
|
||||||
import { python } from "@codemirror/lang-python"
|
|
||||||
import { sql, SQLite } from "@codemirror/lang-sql"
|
|
||||||
import { bracketMatching } from "@codemirror/language"
|
import { bracketMatching } from "@codemirror/language"
|
||||||
import CodeMirror from "./CodeMirror.vue"
|
import CodeMirror from "./CodeMirror.vue"
|
||||||
import type { Extension } from "@codemirror/state"
|
import type { Extension } from "@codemirror/state"
|
||||||
@@ -10,6 +7,7 @@ import type { LANGUAGE } from "utils/types"
|
|||||||
import { darkCodeTheme, lightCodeTheme } from "../themes/codeTheme"
|
import { darkCodeTheme, lightCodeTheme } from "../themes/codeTheme"
|
||||||
import { styleTheme } from "shared/extensions/baseTheme"
|
import { styleTheme } from "shared/extensions/baseTheme"
|
||||||
import { enhanceCompletion } from "shared/extensions/autocompletion"
|
import { enhanceCompletion } from "shared/extensions/autocompletion"
|
||||||
|
import { languageExtension } from "shared/extensions/language"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
language?: LANGUAGE
|
language?: LANGUAGE
|
||||||
@@ -33,10 +31,7 @@ const code = defineModel<string>("value")
|
|||||||
|
|
||||||
const isDark = useDark()
|
const isDark = useDark()
|
||||||
|
|
||||||
const langExtension = computed(() => {
|
const langExtension = computed(() => languageExtension(language))
|
||||||
if (language === "SQL") return sql({ dialect: SQLite, upperCaseKeywords: true })
|
|
||||||
return language === "Python" ? python() : cpp()
|
|
||||||
})
|
|
||||||
|
|
||||||
const extensions = computed(() => [
|
const extensions = computed(() => [
|
||||||
styleTheme,
|
styleTheme,
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { syntaxTree } from "@codemirror/language"
|
||||||
|
import { RangeSetBuilder, type Extension } from "@codemirror/state"
|
||||||
|
import { Decoration, type DecorationSet, type EditorView, ViewPlugin } from "@codemirror/view"
|
||||||
|
import { FORMAT_CLASS, type FormatFamily, formatSpans } from "../formatPlaceholder"
|
||||||
|
|
||||||
|
/** 编辑器里给字符串中的 %d 这类占位符上色,正则见 shared/formatPlaceholder.ts。只扫看得见的部分 */
|
||||||
|
export function formatPlaceholders(family: FormatFamily): Extension {
|
||||||
|
const mark = Decoration.mark({ class: FORMAT_CLASS[family] })
|
||||||
|
|
||||||
|
function build(view: EditorView): DecorationSet {
|
||||||
|
const builder = new RangeSetBuilder<Decoration>()
|
||||||
|
let last = 0
|
||||||
|
for (const { from, to } of view.visibleRanges) {
|
||||||
|
syntaxTree(view.state).iterate({
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
enter: (node) => {
|
||||||
|
if (node.name !== "String" && node.name !== "FormatString") return
|
||||||
|
// 同一个字符串跨两段可见区(中间折叠了)时只加一次,builder 要求从前往后
|
||||||
|
if (node.from >= last) {
|
||||||
|
const text = view.state.doc.sliceString(node.from, node.to)
|
||||||
|
for (const s of formatSpans(text, family)) {
|
||||||
|
builder.add(node.from + s.from, node.from + s.to, mark)
|
||||||
|
}
|
||||||
|
last = node.to
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return builder.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
return ViewPlugin.define(
|
||||||
|
(view) => ({
|
||||||
|
decorations: build(view),
|
||||||
|
update(u) {
|
||||||
|
if (u.docChanged || u.viewportChanged || syntaxTree(u.startState) !== syntaxTree(u.state)) {
|
||||||
|
this.decorations = build(u.view)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{ decorations: (v) => v.decorations },
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,8 +1,25 @@
|
|||||||
import { cpp } from "@codemirror/lang-cpp"
|
import { cpp } from "@codemirror/lang-cpp"
|
||||||
import { python } from "@codemirror/lang-python"
|
import { python, pythonLanguage } from "@codemirror/lang-python"
|
||||||
import { sql, SQLite } from "@codemirror/lang-sql"
|
import { sql, SQLite } from "@codemirror/lang-sql"
|
||||||
|
import { LanguageSupport } from "@codemirror/language"
|
||||||
import type { Extension } from "@codemirror/state"
|
import type { Extension } from "@codemirror/state"
|
||||||
|
import { styleTags, tags as t } from "@lezer/highlight"
|
||||||
import type { LANGUAGE } from "utils/types"
|
import type { LANGUAGE } from "utils/types"
|
||||||
|
import { formatPlaceholders } from "./formatPlaceholder"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Python 的解析器在上色上补几处,向 VS Code 看齐:`from` / `as` / `with` 和 `if`、`import` 一样算管流程
|
||||||
|
* 的词(lezer 把 `from` 和 `def` 归成一类);f-string 的 `{ }` 和 `:.2f` 一样是格式占位符的颜色。
|
||||||
|
* 静态代码(shared/highlight.ts)也用这一份,编辑器和题面的颜色才一样。
|
||||||
|
*/
|
||||||
|
export const ojPythonLanguage = pythonLanguage.configure({
|
||||||
|
props: [
|
||||||
|
styleTags({
|
||||||
|
"from as with": t.controlKeyword,
|
||||||
|
"FormatReplacement/{ FormatReplacement/} FormatSpec/:": t.modifier,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 语言对应的高亮扩展。学生端和教师端共用同一份(两边都是题目页上的 SyncCodeEditor) ——
|
* 语言对应的高亮扩展。学生端和教师端共用同一份(两边都是题目页上的 SyncCodeEditor) ——
|
||||||
@@ -12,5 +29,8 @@ import type { LANGUAGE } from "utils/types"
|
|||||||
*/
|
*/
|
||||||
export function languageExtension(language: LANGUAGE): Extension {
|
export function languageExtension(language: LANGUAGE): Extension {
|
||||||
if (language === "SQL") return sql({ dialect: SQLite, upperCaseKeywords: true })
|
if (language === "SQL") return sql({ dialect: SQLite, upperCaseKeywords: true })
|
||||||
return language === "Python" ? python() : cpp()
|
if (language === "Python") {
|
||||||
|
return [new LanguageSupport(ojPythonLanguage, python().support), formatPlaceholders("python")]
|
||||||
|
}
|
||||||
|
return [cpp(), formatPlaceholders("c")]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* 字符串里的格式占位符:printf 的 %d、%.2f、%%,Python 的 "%d 个" % n。
|
||||||
|
* lezer 把整个字符串当一个词,不分出占位符,所以另外扫一遍。静态代码在 shared/highlight.ts 里拆,
|
||||||
|
* 编辑器在 shared/extensions/formatPlaceholder.ts 里挂装饰,两边用这一份正则。
|
||||||
|
*
|
||||||
|
* 正则和颜色照 VS Code 的语法文件:C 是 constant.other.placeholder(变量色),
|
||||||
|
* Python 是 constant.character.format.placeholder(关键字色),所以两种语言类名不同。
|
||||||
|
*/
|
||||||
|
export type FormatFamily = "c" | "python"
|
||||||
|
|
||||||
|
const PATTERNS: Record<FormatFamily, 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,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FORMAT_CLASS: Record<FormatFamily, string> = {
|
||||||
|
c: "tok-format",
|
||||||
|
python: "tok-format-py",
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 字符串里每个占位符的位置(相对 text 开头) */
|
||||||
|
export function formatSpans(text: string, family: FormatFamily): { from: number; to: number }[] {
|
||||||
|
return [...text.matchAll(PATTERNS[family])].map((m) => ({
|
||||||
|
from: m.index,
|
||||||
|
to: m.index + m[0].length,
|
||||||
|
}))
|
||||||
|
}
|
||||||
@@ -67,6 +67,19 @@ html.dark {
|
|||||||
color: var(--code-invalid);
|
color: var(--code-invalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 字符串里的 %d,见 shared/formatPlaceholder.ts。静态代码是同一个 span 上的两个类名,
|
||||||
|
编辑器里是两层 span、谁包谁不一定,所以三种写法都要 */
|
||||||
|
.tok-format,
|
||||||
|
.tok-format .tok-string,
|
||||||
|
.tok-string .tok-format {
|
||||||
|
color: var(--code-variable);
|
||||||
|
}
|
||||||
|
.tok-format-py,
|
||||||
|
.tok-format-py .tok-string,
|
||||||
|
.tok-string .tok-format-py {
|
||||||
|
color: var(--code-keyword);
|
||||||
|
}
|
||||||
|
|
||||||
/* 以下只有 Markdown 编辑器用得到 */
|
/* 以下只有 Markdown 编辑器用得到 */
|
||||||
.tok-heading {
|
.tok-heading {
|
||||||
color: var(--code-heading);
|
color: var(--code-heading);
|
||||||
|
|||||||
@@ -9,10 +9,11 @@
|
|||||||
* 只认判题机认的几种语言,别的语言和不标语言的代码块按原文显示。
|
* 只认判题机认的几种语言,别的语言和不标语言的代码块按原文显示。
|
||||||
*/
|
*/
|
||||||
import { cppLanguage } from "@codemirror/lang-cpp"
|
import { cppLanguage } from "@codemirror/lang-cpp"
|
||||||
import { pythonLanguage } from "@codemirror/lang-python"
|
|
||||||
import { SQLite } from "@codemirror/lang-sql"
|
import { SQLite } from "@codemirror/lang-sql"
|
||||||
import { highlightCode, tagHighlighter, tags as t } from "@lezer/highlight"
|
import { highlightCode, tagHighlighter, tags as t } from "@lezer/highlight"
|
||||||
import type { ConfigProviderProps } from "naive-ui"
|
import type { ConfigProviderProps } from "naive-ui"
|
||||||
|
import { ojPythonLanguage } from "./extensions/language"
|
||||||
|
import { FORMAT_CLASS, type FormatFamily, formatSpans } from "./formatPlaceholder"
|
||||||
|
|
||||||
/** 词的种类 → 类名。编辑器在 shared/themes/codeTheme.ts 里挂的也是它 */
|
/** 词的种类 → 类名。编辑器在 shared/themes/codeTheme.ts 里挂的也是它 */
|
||||||
export const codeHighlighter = tagHighlighter([
|
export const codeHighlighter = tagHighlighter([
|
||||||
@@ -41,9 +42,9 @@ type Parser = typeof cppLanguage.parser
|
|||||||
|
|
||||||
// C 和编辑器一样用 C++ 的解析器
|
// C 和编辑器一样用 C++ 的解析器
|
||||||
const PARSERS: Record<string, Parser> = {
|
const PARSERS: Record<string, Parser> = {
|
||||||
python: pythonLanguage.parser,
|
python: ojPythonLanguage.parser,
|
||||||
py: pythonLanguage.parser,
|
py: ojPythonLanguage.parser,
|
||||||
python3: pythonLanguage.parser,
|
python3: ojPythonLanguage.parser,
|
||||||
c: cppLanguage.parser,
|
c: cppLanguage.parser,
|
||||||
cpp: cppLanguage.parser,
|
cpp: cppLanguage.parser,
|
||||||
"c++": cppLanguage.parser,
|
"c++": cppLanguage.parser,
|
||||||
@@ -54,6 +55,13 @@ function parserFor(lang?: string): Parser | null {
|
|||||||
return PARSERS[lang?.toLowerCase() ?? ""] ?? null
|
return PARSERS[lang?.toLowerCase() ?? ""] ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatFamilyFor(lang?: string): FormatFamily | null {
|
||||||
|
const parser = parserFor(lang)
|
||||||
|
if (parser === cppLanguage.parser) return "c"
|
||||||
|
if (parser === ojPythonLanguage.parser) return "python"
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
export function escapeHtml(text: string): string {
|
export function escapeHtml(text: string): string {
|
||||||
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
|
||||||
}
|
}
|
||||||
@@ -74,7 +82,19 @@ function walk(
|
|||||||
put(code, "")
|
put(code, "")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
highlightCode(code, parser.parse(code), codeHighlighter, put, () => put("\n", ""))
|
// 字符串里的 %d 这类占位符再拆出来,见 shared/formatPlaceholder.ts
|
||||||
|
const family = formatFamilyFor(lang)
|
||||||
|
const emit = (text: string, classes: string) => {
|
||||||
|
if (!family || !classes.includes("tok-string")) return put(text, classes)
|
||||||
|
let at = 0
|
||||||
|
for (const s of formatSpans(text, family)) {
|
||||||
|
if (s.from > at) put(text.slice(at, s.from), classes)
|
||||||
|
put(text.slice(s.from, s.to), `${classes} ${FORMAT_CLASS[family]}`)
|
||||||
|
at = s.to
|
||||||
|
}
|
||||||
|
if (at < text.length) put(text.slice(at), classes)
|
||||||
|
}
|
||||||
|
highlightCode(code, parser.parse(code), codeHighlighter, emit, () => put("\n", ""))
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 逐行高亮,返回每行的 HTML,每行的标签自己闭合 */
|
/** 逐行高亮,返回每行的 HTML,每行的标签自己闭合 */
|
||||||
|
|||||||
Reference in new issue
Block a user