feat(自学): C 教程里没写 main 的片段也能运行——上下套一层灰色的 main,灰的几行改不了;其余没 main 的片段不再给一按就报错的「运行」按钮
Deploy / deploy (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
教程正文里写 ```c main 的片段才套。套的行就在编辑器里,报错行号和看到的一致,复制出去是完整程序。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
dae10d0813
commit
27210eac1b
4 files changed
+92
-17
No files matched your search
@@ -9,6 +9,7 @@ import { explainPythonCompileError } from "oj/problem/utils/pythonError"
|
|||||||
import { explainRuntimeError } from "oj/problem/utils/runtimeError"
|
import { explainRuntimeError } from "oj/problem/utils/runtimeError"
|
||||||
import { useAuthModalStore } from "shared/store/authModal"
|
import { useAuthModalStore } from "shared/store/authModal"
|
||||||
import { useUserStore } from "shared/store/user"
|
import { useUserStore } from "shared/store/user"
|
||||||
|
import { mainWrapExtensions, wrapMain } from "../wrapMain"
|
||||||
|
|
||||||
const CodeEditor = defineAsyncComponent(() => import("shared/components/CodeEditor.vue"))
|
const CodeEditor = defineAsyncComponent(() => import("shared/components/CodeEditor.vue"))
|
||||||
|
|
||||||
@@ -18,11 +19,14 @@ const CodeEditor = defineAsyncComponent(() => import("shared/components/CodeEdit
|
|||||||
* 运行走题目页同一台判题机(POST /trial-runs,带 tutorialId),不算提交。
|
* 运行走题目页同一台判题机(POST /trial-runs,带 tutorialId),不算提交。
|
||||||
* 报错用题目页那套中文解释(pythonError / cError / runtimeError),原文收在下面。
|
* 报错用题目页那套中文解释(pythonError / cError / runtimeError),原文收在下面。
|
||||||
* 代码里读输入(input() / scanf)才出输入框。
|
* 代码里读输入(input() / scanf)才出输入框。
|
||||||
|
* C 的片段(原文没有 main,只是讲一两行写法)跑不起来,不给「运行」按钮;
|
||||||
|
* 教程里标了 ```c main 的片段套上灰色的 main 再给(见 wrapMain.ts)。
|
||||||
*/
|
*/
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
code: string
|
code: string
|
||||||
lang: "python" | "c"
|
lang: "python" | "c"
|
||||||
tutorialId: number
|
tutorialId: number
|
||||||
|
wrapMain?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
@@ -30,7 +34,9 @@ const authModal = useAuthModalStore()
|
|||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
const { copy } = useClipboard({ legacy: true })
|
const { copy } = useClipboard({ legacy: true })
|
||||||
|
|
||||||
const value = ref(props.code)
|
const original = computed(() => (props.wrapMain ? wrapMain(props.code) : props.code))
|
||||||
|
const extensions = computed(() => (props.wrapMain ? mainWrapExtensions : []))
|
||||||
|
const value = ref(original.value)
|
||||||
const input = ref("")
|
const input = ref("")
|
||||||
const running = ref(false)
|
const running = ref(false)
|
||||||
|
|
||||||
@@ -41,16 +47,15 @@ type Outcome =
|
|||||||
const outcome = ref<Outcome | null>(null)
|
const outcome = ref<Outcome | null>(null)
|
||||||
|
|
||||||
// 换课时组件实例可能被复用(v-for 的 key 是段落序号),跟着原文重置
|
// 换课时组件实例可能被复用(v-for 的 key 是段落序号),跟着原文重置
|
||||||
watch(
|
watch(original, (code) => {
|
||||||
() => props.code,
|
value.value = code
|
||||||
(code) => {
|
input.value = ""
|
||||||
value.value = code
|
outcome.value = null
|
||||||
input.value = ""
|
})
|
||||||
outcome.value = null
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
const edited = computed(() => value.value !== props.code)
|
const edited = computed(() => value.value !== original.value)
|
||||||
|
// 看原文不看改过的:学生把 main 删了,按钮还在,点了才知道错在哪
|
||||||
|
const runnable = computed(() => props.lang === "python" || /\bmain\s*\(/.test(original.value))
|
||||||
const needsInput = computed(() =>
|
const needsInput = computed(() =>
|
||||||
props.lang === "python"
|
props.lang === "python"
|
||||||
? /\binput\s*\(/.test(value.value)
|
? /\binput\s*\(/.test(value.value)
|
||||||
@@ -173,7 +178,7 @@ async function run() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function restore() {
|
function restore() {
|
||||||
value.value = props.code
|
value.value = original.value
|
||||||
outcome.value = null
|
outcome.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,7 +199,14 @@ async function copyCode() {
|
|||||||
<Icon icon="ph:arrow-counter-clockwise" :width="14" />还原
|
<Icon icon="ph:arrow-counter-clockwise" :width="14" />还原
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
<button type="button" class="run" :class="{ busy: running }" :disabled="running" @click="run">
|
<button
|
||||||
|
v-if="runnable"
|
||||||
|
type="button"
|
||||||
|
class="run"
|
||||||
|
:class="{ busy: running }"
|
||||||
|
:disabled="running"
|
||||||
|
@click="run"
|
||||||
|
>
|
||||||
<template v-if="running">运行中…</template>
|
<template v-if="running">运行中…</template>
|
||||||
<template v-else><Icon icon="ph:play-fill" :width="12" />运行</template>
|
<template v-else><Icon icon="ph:play-fill" :width="12" />运行</template>
|
||||||
</button>
|
</button>
|
||||||
@@ -203,10 +215,16 @@ async function copyCode() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="editor" :style="{ minHeight: `${lineCount * 22 + 16}px` }">
|
<div class="editor" :style="{ minHeight: `${lineCount * 22 + 16}px` }">
|
||||||
<CodeEditor v-model:value="value" :language="language" :font-size="14" height="auto" />
|
<CodeEditor
|
||||||
|
v-model:value="value"
|
||||||
|
:language="language"
|
||||||
|
:font-size="14"
|
||||||
|
height="auto"
|
||||||
|
:extra-extensions="extensions"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="needsInput"
|
v-if="runnable && needsInput"
|
||||||
class="pane input"
|
class="pane input"
|
||||||
:class="{ ask: outcome?.kind === 'error' && outcome.needInput }"
|
:class="{ ask: outcome?.kind === 'error' && outcome.needInput }"
|
||||||
>
|
>
|
||||||
@@ -315,6 +333,9 @@ async function copyCode() {
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
border-right: 0;
|
border-right: 0;
|
||||||
}
|
}
|
||||||
|
.editor :deep(.cm-main-wrap) {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
.editor :deep(.cm-scroller) {
|
.editor :deep(.cm-scroller) {
|
||||||
font-family: var(--learn-code-font);
|
font-family: var(--learn-code-font);
|
||||||
line-height: 22px;
|
line-height: 22px;
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ function inline(text: string) {
|
|||||||
v-else-if="seg.type === 'code'"
|
v-else-if="seg.type === 'code'"
|
||||||
:code="seg.code"
|
:code="seg.code"
|
||||||
:lang="seg.lang"
|
:lang="seg.lang"
|
||||||
|
:wrap-main="seg.wrapMain"
|
||||||
:tutorial-id="tutorialId"
|
:tutorial-id="tutorialId"
|
||||||
/>
|
/>
|
||||||
<ExerciseWidget
|
<ExerciseWidget
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type { Exercise } from "utils/types"
|
|||||||
* - `goals`:「## 学习目标」那一节画成「学完这课你能」的方框;
|
* - `goals`:「## 学习目标」那一节画成「学完这课你能」的方框;
|
||||||
* - `code`:带语言的代码块(```python / ```c)换成能改、能运行的代码块;
|
* - `code`:带语言的代码块(```python / ```c)换成能改、能运行的代码块;
|
||||||
* 不带语言的(「输出结果」那种)留在 Markdown 里照常显示;
|
* 不带语言的(「输出结果」那种)留在 Markdown 里照常显示;
|
||||||
|
* ```c main 是没写 main 的片段,由代码块套上灰色的 main 再运行(见 CodeRunBlock);
|
||||||
* - `exercise`:`[[exercise:36]]` 占位符换成练一练;
|
* - `exercise`:`[[exercise:36]]` 占位符换成练一练;
|
||||||
* - `md`:其余的交给 Markdown.vue。
|
* - `md`:其余的交给 Markdown.vue。
|
||||||
*
|
*
|
||||||
@@ -17,7 +18,7 @@ export type LessonSegment =
|
|||||||
| { type: "md"; content: string }
|
| { type: "md"; content: string }
|
||||||
| { type: "heading"; id: string; text: string }
|
| { type: "heading"; id: string; text: string }
|
||||||
| { type: "goals"; id: string; items: string[] }
|
| { type: "goals"; id: string; items: string[] }
|
||||||
| { type: "code"; lang: "python" | "c"; code: string; index: number }
|
| { type: "code"; lang: "python" | "c"; code: string; index: number; wrapMain: boolean }
|
||||||
| { type: "exercise"; exercise: Exercise; index: number }
|
| { type: "exercise"; exercise: Exercise; index: number }
|
||||||
|
|
||||||
/** 大纲里的一项:一个二级标题,挂着它这一节里的练一练 */
|
/** 大纲里的一项:一个二级标题,挂着它这一节里的练一练 */
|
||||||
@@ -75,7 +76,7 @@ export function parseLesson(content: string, exercises: Exercise[]) {
|
|||||||
let i = 0
|
let i = 0
|
||||||
while (i < lines.length) {
|
while (i < lines.length) {
|
||||||
const line = lines[i]!
|
const line = lines[i]!
|
||||||
const fence = line.match(/^(\s*)(```+|~~~+)\s*([\w+#-]*)\s*$/)
|
const fence = line.match(/^(\s*)(```+|~~~+)\s*([\w+#-]*)(\s+main)?\s*$/)
|
||||||
if (fence) {
|
if (fence) {
|
||||||
const marker = fence[2]!
|
const marker = fence[2]!
|
||||||
const lang = CODE_LANGS[fence[3]!.toLowerCase()]
|
const lang = CODE_LANGS[fence[3]!.toLowerCase()]
|
||||||
@@ -84,7 +85,13 @@ export function parseLesson(content: string, exercises: Exercise[]) {
|
|||||||
while (j < lines.length && !lines[j]!.trim().startsWith(marker)) body.push(lines[j++]!)
|
while (j < lines.length && !lines[j]!.trim().startsWith(marker)) body.push(lines[j++]!)
|
||||||
if (lang) {
|
if (lang) {
|
||||||
flush()
|
flush()
|
||||||
segments.push({ type: "code", lang, code: body.join("\n"), index: codeIndex++ })
|
segments.push({
|
||||||
|
type: "code",
|
||||||
|
lang,
|
||||||
|
code: body.join("\n"),
|
||||||
|
index: codeIndex++,
|
||||||
|
wrapMain: lang === "c" && !!fence[4],
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
md.push(line, ...body, lines[j] ?? marker)
|
md.push(line, ...body, lines[j] ?? marker)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { EditorState, RangeSetBuilder, Transaction, type Extension } from "@codemirror/state"
|
||||||
|
import { Decoration, EditorView } from "@codemirror/view"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* C 教程里没写 main 的片段(```c main):运行时套上一层 main,套的这几行灰着、改不了。
|
||||||
|
*
|
||||||
|
* 套的行就放在编辑器里,不是另外拼在提交的代码上 —— 这样报错的行号和学生看到的一样,
|
||||||
|
* 复制出去的也是一份完整能跑的程序。
|
||||||
|
*/
|
||||||
|
const HEAD = ["#include <stdio.h>", "", "int main(void) {"]
|
||||||
|
const FOOT = [" return 0;", "}"]
|
||||||
|
|
||||||
|
export function wrapMain(code: string) {
|
||||||
|
const body = code.split("\n").map((line) => (line ? ` ${line}` : line))
|
||||||
|
return [...HEAD, ...body, ...FOOT].join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 头尾两段的字符范围:头到片段第一行行首,尾从片段最后一行行尾起(换行符算在套的那边) */
|
||||||
|
function lockedRanges(state: EditorState) {
|
||||||
|
const doc = state.doc
|
||||||
|
const headEnd = doc.line(HEAD.length + 1).from
|
||||||
|
const footStart = doc.line(doc.lines - FOOT.length + 1).from - 1
|
||||||
|
return { headEnd, footStart }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 边界上插入是放行的(实测 changeFilter 只拦范围内部),所以片段首行行首、末行行尾照常能打字。
|
||||||
|
// 只拦学生的编辑:「还原」、换课时 CodeMirror.vue 整篇替换文档,那一下不带 userEvent
|
||||||
|
const lock = EditorState.changeFilter.of((tr) => {
|
||||||
|
if (!tr.annotation(Transaction.userEvent)) return true
|
||||||
|
const { headEnd, footStart } = lockedRanges(tr.startState)
|
||||||
|
return [0, headEnd, footStart, tr.startState.doc.length]
|
||||||
|
})
|
||||||
|
|
||||||
|
const dim = Decoration.line({ class: "cm-main-wrap" })
|
||||||
|
|
||||||
|
const dimLines = EditorView.decorations.compute(["doc"], (state) => {
|
||||||
|
const builder = new RangeSetBuilder<Decoration>()
|
||||||
|
const doc = state.doc
|
||||||
|
for (let n = 1; n <= doc.lines; n++) {
|
||||||
|
if (n <= HEAD.length || n > doc.lines - FOOT.length)
|
||||||
|
builder.add(doc.line(n).from, doc.line(n).from, dim)
|
||||||
|
}
|
||||||
|
return builder.finish()
|
||||||
|
})
|
||||||
|
|
||||||
|
export const mainWrapExtensions: Extension[] = [lock, dimLines]
|
||||||
Reference in new issue
Block a user