中职学生看不懂英文报错。原来提交前用浏览器里的 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:
1 parent
039992dc86
commit
2a4405d2ea
16 files changed
+733
-100
No files matched your search
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user