feat(题目): Python 语法错误改成中文说明,在编辑器里标出位置,中文标点可以一键换成英文
Deploy / deploy (push) Canceled after 0s

中职学生看不懂英文报错。原来提交前用浏览器里的 Skulpt 拦语法错误,只弹一句
「第 N 行存在语法错误」——Skulpt 九成报的是 bad input / bad token,没有列号。

- 提交前的语法检查挪到服务端:/code/format 先用 CPython 编译一遍(只编译不执行),
  api 镜像加 python3-minimal(3.13,与判题机同版本,+17MB),前端删掉 Skulpt
- 报错按规则表翻成中文(pythonError.ts),生产库 2695 条 Python 编译错误覆盖 99.7%;
  提交前拦下的和判题回来的编译错误共用同一张卡片
- 编辑器里标红出错行和 ^ 指的位置
- 中文标点(历史编译错误的 22%)一键换成英文,跳过字符串和注释、处理 f 字符串花括号;
  594 条里修好 593 条,经 tokenize 核对没有改到字符串内容;走编辑器事务,可 Ctrl+Z

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-09-27 21:47:01 +08:00
1 parent 039992dc86
commit 2a4405d2ea
16 files changed
+733 -100

No files matched your search

+123
View File
@@ -0,0 +1,123 @@
import { StateEffect, StateField, type EditorState } from "@codemirror/state"
import { Decoration, EditorView, ViewPlugin, type DecorationSet } from "@codemirror/view"
import type { PunctuationFix } from "./chinesePunctuation"
/**
* 在题目页的编辑器里标出编译错误的位置:整行淡红底,`^` 标的那一段加红波浪线。
*
* 中职学生读不懂英文报错,「看这里」比任何文字都直接。标记跟着编辑映射位置
* (在上面插一行,标记跟着往下走),下一次提交时清掉。
*/
interface Mark {
line: number
/** 行内列号,[from, to);没有就只标整行 */
from?: number
to?: number
}
const setMark = StateEffect.define<Mark | null>()
const lineDeco = Decoration.line({ class: "cm-error-line" })
const rangeDeco = Decoration.mark({ class: "cm-error-range" })
function build(state: EditorState, mark: Mark): DecorationSet {
if (mark.line < 1 || mark.line > state.doc.lines) return Decoration.none
const line = state.doc.line(mark.line)
const ranges = [lineDeco.range(line.from)]
if (mark.from !== undefined && mark.to !== undefined) {
const from = line.from + Math.min(mark.from, line.length)
const to = line.from + Math.min(mark.to, line.length)
if (to > from) ranges.push(rangeDeco.range(from, to))
}
return Decoration.set(ranges)
}
const markField = StateField.define<DecorationSet>({
create: () => Decoration.none,
update(deco, tr) {
deco = deco.map(tr.changes)
for (const effect of tr.effects) {
if (effect.is(setMark)) deco = effect.value ? build(tr.state, effect.value) : Decoration.none
}
return deco
},
provide: (field) => EditorView.decorations.from(field),
})
/**
* 题目页当前那个编辑器。结果面板和编辑器是兄弟组件,隔着好几层,
* 与其一路透传 ref,不如让编辑器挂载时自己登记。只挂在题目页主编辑器上
* (ProblemEditor 的扩展数组),自测、流程图那些编辑器不登记。
*/
let current: EditorView | null = null
const tracker = ViewPlugin.define((view) => {
current = view
return {
destroy() {
if (current === view) current = null
},
}
})
const theme = EditorView.baseTheme({
".cm-error-line": { backgroundColor: "rgba(208, 48, 80, 0.12)" },
".cm-error-range": {
textDecoration: "underline wavy #d03050",
textUnderlineOffset: "3px",
backgroundColor: "rgba(208, 48, 80, 0.18)",
},
})
export const errorMarkExtensions = [markField, tracker, theme]
/**
* 标出第 `line` 行并滚过去。`sourceLine` 是报错里打出来的那一行(去了行首空格),
* 编辑器里这一行对得上才标列范围 —— 学生看结果之前可能已经改过代码了,
* 对不上时列号没有意义,只标整行。
*/
export function showErrorMark(
line: number,
sourceLine: string | null,
caret: { from: number; to: number } | null,
) {
const view = current
if (!view || line < 1 || line > view.state.doc.lines) return
const text = view.state.doc.line(line).text
const mark: Mark = { line }
if (caret && sourceLine !== null) {
const stripped = text.replace(/^ +/, "")
if (stripped.trimEnd() === sourceLine.trimEnd()) {
const indent = text.length - stripped.length
mark.from = indent + caret.from
mark.to = indent + caret.to
}
}
const pos = view.state.doc.line(line).from
view.dispatch({
effects: [setMark.of(mark), EditorView.scrollIntoView(pos, { y: "center" })],
})
}
export function clearErrorMark() {
current?.dispatch({ effects: setMark.of(null) })
}
/** 当前编辑器里的代码;编辑器没挂载时为 null */
export function currentCode() {
return current?.state.doc.toString() ?? null
}
/**
* 在编辑器里批量替换。走编辑器事务而不是直接改 store 里的字符串:
* 这样一次 Ctrl+Z 就能撤回,协作中的对方也能同步到。
*
* 不打 userEvent:editTrace 按 `isUserEvent("input")` 数「自己敲的字」,
* 打成 `input.*` 就会把这次替换算成学生手打的。
*/
export function applyFixes(fixes: PunctuationFix[]) {
if (!current || !fixes.length) return false
current.dispatch({ changes: fixes, effects: setMark.of(null) })
return true
}