refactor(编辑器): 去掉 vue-codemirror,代码编辑器、协作编辑器、教程编辑器改用自己的薄外壳
三个编辑器只用到它的 v-model、extensions、只读、缩进、占位文字、尺寸和 ready, shared/components/CodeMirror.vue 照它的做法写了这几样,功能和手感不变: - 它默认带的 basicSetup(行号、撤销、折叠、搜索、括号……)照样带上 - 各项配置的先后顺序和它一致;外部改代码仍是一次不带 userEvent 的 changes, 提交时的编辑过程统计不受影响 - @codemirror/commands 现在直接 import,显式声明进依赖 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6216e1cf22
commit
d2b8a1ce4a
7 files changed
+133
-24
No files matched your search
@@ -12,6 +12,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/autocomplete": "^6.20.3",
|
"@codemirror/autocomplete": "^6.20.3",
|
||||||
|
"@codemirror/commands": "^6.10.4",
|
||||||
"@codemirror/lang-cpp": "^6.0.3",
|
"@codemirror/lang-cpp": "^6.0.3",
|
||||||
"@codemirror/lang-markdown": "^6.5.2",
|
"@codemirror/lang-markdown": "^6.5.2",
|
||||||
"@codemirror/lang-python": "^6.2.1",
|
"@codemirror/lang-python": "^6.2.1",
|
||||||
@@ -47,7 +48,6 @@
|
|||||||
"pinyin-pro": "^3.29.5",
|
"pinyin-pro": "^3.29.5",
|
||||||
"vue": "^3.5.43",
|
"vue": "^3.5.43",
|
||||||
"vue-chartjs": "^5.3.4",
|
"vue-chartjs": "^5.3.4",
|
||||||
"vue-codemirror": "^6.1.1",
|
|
||||||
"vue-router": "^5.4.0",
|
"vue-router": "^5.4.0",
|
||||||
"y-codemirror.next": "^0.3.6",
|
"y-codemirror.next": "^0.3.6",
|
||||||
"y-protocols": "1.0.7",
|
"y-protocols": "1.0.7",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import type { SubmissionTrace } from "@oj2/contract"
|
|||||||
* 直接失效。计数本身也不需要响应式。
|
* 直接失效。计数本身也不需要响应式。
|
||||||
*
|
*
|
||||||
* **只数带 userEvent 的事务。** 下面这些都不带,所以天然不会被算进去:
|
* **只数带 userEvent 的事务。** 下面这些都不带,所以天然不会被算进去:
|
||||||
* - `codeStore.setCode()` —— vue-codemirror 的 setDoc 只 dispatch 一个 changes:
|
* - `codeStore.setCode()` —— shared/components/CodeMirror.vue 同步 v-model 时只 dispatch 一个 changes:
|
||||||
* 提交前的自动格式化、载入草稿 / 模板、切语言都走这条;
|
* 提交前的自动格式化、载入草稿 / 模板、切语言都走这条;
|
||||||
* - 课堂协作里对方的改动 —— y-codemirror.next 应用远程更新时只挂 ySyncAnnotation。
|
* - 课堂协作里对方的改动 —— y-codemirror.next 应用远程更新时只挂 ySyncAnnotation。
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { cpp } from "@codemirror/lang-cpp"
|
|||||||
import { python } from "@codemirror/lang-python"
|
import { python } from "@codemirror/lang-python"
|
||||||
import { sql, SQLite } from "@codemirror/lang-sql"
|
import { sql, SQLite } from "@codemirror/lang-sql"
|
||||||
import { bracketMatching } from "@codemirror/language"
|
import { bracketMatching } from "@codemirror/language"
|
||||||
import { Codemirror } from "vue-codemirror"
|
import CodeMirror from "./CodeMirror.vue"
|
||||||
import type { Extension } from "@codemirror/state"
|
import type { Extension } from "@codemirror/state"
|
||||||
import { autocompletion, closeBrackets, completeAnyWord } from "@codemirror/autocomplete"
|
import { autocompletion, closeBrackets, completeAnyWord } from "@codemirror/autocomplete"
|
||||||
import type { LANGUAGE } from "utils/types"
|
import type { LANGUAGE } from "utils/types"
|
||||||
@@ -53,13 +53,13 @@ const extensions = computed(() => [
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Codemirror
|
<CodeMirror
|
||||||
v-model="code"
|
v-model="code"
|
||||||
indentWithTab
|
|
||||||
:extensions="extensions"
|
:extensions="extensions"
|
||||||
:disabled="readonly"
|
:disabled="readonly"
|
||||||
:tab-size="4"
|
:tab-size="4"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:style="{ height, fontSize: `${fontSize}px` }"
|
:height="height"
|
||||||
|
:font-size="`${fontSize}px`"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* CodeMirror 6 的 Vue 外壳,代码编辑器、协作编辑器、教程编辑器共用。
|
||||||
|
*
|
||||||
|
* 原来是 vue-codemirror,三个编辑器只用到它的 v-model、extensions、只读、缩进、
|
||||||
|
* 占位文字、尺寸和 ready 这几样,这里照它的做法写了这几样:
|
||||||
|
* - **basicSetup 是它默认带的**(行号、撤销、折叠、搜索、括号……),这里也带上,
|
||||||
|
* 漏了编辑器会少一大截功能;
|
||||||
|
* - 各项配置的先后顺序和它一致(同优先级下排前面的赢),换掉之后行为不变。
|
||||||
|
*/
|
||||||
|
import { basicSetup } from "codemirror"
|
||||||
|
import { indentWithTab } from "@codemirror/commands"
|
||||||
|
import { indentUnit } from "@codemirror/language"
|
||||||
|
import { Compartment, EditorState, type Extension } from "@codemirror/state"
|
||||||
|
import { EditorView, keymap, placeholder as placeholderExt } from "@codemirror/view"
|
||||||
|
|
||||||
|
const {
|
||||||
|
extensions = [],
|
||||||
|
disabled = false,
|
||||||
|
tabSize = 4,
|
||||||
|
placeholder = "",
|
||||||
|
height = "100%",
|
||||||
|
fontSize = "",
|
||||||
|
} = defineProps<{
|
||||||
|
/** 传稳定的数组实例:每换一个新数组,编辑器就重配一次 */
|
||||||
|
extensions?: Extension[]
|
||||||
|
disabled?: boolean
|
||||||
|
tabSize?: number
|
||||||
|
placeholder?: string
|
||||||
|
height?: string
|
||||||
|
fontSize?: string
|
||||||
|
}>()
|
||||||
|
const code = defineModel<string>({ default: "" })
|
||||||
|
const emit = defineEmits<{ ready: [view: EditorView] }>()
|
||||||
|
|
||||||
|
const container = useTemplateRef("container")
|
||||||
|
let view: EditorView | null = null
|
||||||
|
|
||||||
|
const compartments = {
|
||||||
|
extensions: new Compartment(),
|
||||||
|
disabled: new Compartment(),
|
||||||
|
tabSize: new Compartment(),
|
||||||
|
placeholder: new Compartment(),
|
||||||
|
size: new Compartment(),
|
||||||
|
}
|
||||||
|
|
||||||
|
const disabledExt = (value: boolean) =>
|
||||||
|
value ? [EditorView.editable.of(false), EditorState.readOnly.of(true)] : []
|
||||||
|
const tabSizeExt = (size: number) => [EditorState.tabSize.of(size), indentUnit.of(" ".repeat(size))]
|
||||||
|
const placeholderOf = (text: string) => (text ? placeholderExt(text) : [])
|
||||||
|
const sizeExt = (h: string, fs: string) =>
|
||||||
|
EditorView.theme({ "&": fs ? { height: h, fontSize: fs } : { height: h } })
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
view = new EditorView({
|
||||||
|
parent: container.value!,
|
||||||
|
state: EditorState.create({
|
||||||
|
doc: code.value,
|
||||||
|
extensions: [
|
||||||
|
basicSetup,
|
||||||
|
EditorView.updateListener.of((update) => {
|
||||||
|
if (!update.docChanged) return
|
||||||
|
const doc = update.state.doc.toString()
|
||||||
|
if (doc !== code.value) code.value = doc
|
||||||
|
}),
|
||||||
|
compartments.extensions.of(extensions),
|
||||||
|
compartments.disabled.of(disabledExt(disabled)),
|
||||||
|
keymap.of([indentWithTab]),
|
||||||
|
compartments.tabSize.of(tabSizeExt(tabSize)),
|
||||||
|
compartments.placeholder.of(placeholderOf(placeholder)),
|
||||||
|
compartments.size.of(sizeExt(height, fontSize)),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
emit("ready", view)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
view?.destroy()
|
||||||
|
view = null
|
||||||
|
})
|
||||||
|
|
||||||
|
function reconfigure(compartment: Compartment, extension: Extension) {
|
||||||
|
view?.dispatch({ effects: compartment.reconfigure(extension) })
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(code, (value) => {
|
||||||
|
if (!view) return
|
||||||
|
const current = view.state.doc.toString()
|
||||||
|
if (value === current) return
|
||||||
|
view.dispatch({ changes: { from: 0, to: current.length, insert: value } })
|
||||||
|
})
|
||||||
|
watch(
|
||||||
|
() => extensions,
|
||||||
|
(value) => reconfigure(compartments.extensions, value),
|
||||||
|
)
|
||||||
|
watch(
|
||||||
|
() => disabled,
|
||||||
|
(value) => reconfigure(compartments.disabled, disabledExt(value)),
|
||||||
|
)
|
||||||
|
watch(
|
||||||
|
() => tabSize,
|
||||||
|
(value) => reconfigure(compartments.tabSize, tabSizeExt(value)),
|
||||||
|
)
|
||||||
|
watch(
|
||||||
|
() => placeholder,
|
||||||
|
(value) => reconfigure(compartments.placeholder, placeholderOf(value)),
|
||||||
|
)
|
||||||
|
watch([() => height, () => fontSize], ([h, fs]) => reconfigure(compartments.size, sizeExt(h, fs)))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- display: contents,和原来 vue-codemirror 的外层一样不占布局 -->
|
||||||
|
<div ref="container" style="display: contents" />
|
||||||
|
</template>
|
||||||
@@ -3,7 +3,7 @@ import { markdown } from "@codemirror/lang-markdown"
|
|||||||
import { EditorSelection } from "@codemirror/state"
|
import { EditorSelection } from "@codemirror/state"
|
||||||
import { EditorView, keymap } from "@codemirror/view"
|
import { EditorView, keymap } from "@codemirror/view"
|
||||||
import { Icon } from "@iconify/vue"
|
import { Icon } from "@iconify/vue"
|
||||||
import { Codemirror } from "vue-codemirror"
|
import CodeMirror from "./CodeMirror.vue"
|
||||||
import { uploadImage } from "../../admin/api"
|
import { uploadImage } from "../../admin/api"
|
||||||
import { styleTheme } from "../extensions/baseTheme"
|
import { styleTheme } from "../extensions/baseTheme"
|
||||||
import { oneDark } from "../themes/oneDark"
|
import { oneDark } from "../themes/oneDark"
|
||||||
@@ -196,12 +196,12 @@ const tools = computed(() => [
|
|||||||
<input ref="fileInput" type="file" accept="image/*" multiple hidden @change="onPickImages" />
|
<input ref="fileInput" type="file" accept="image/*" multiple hidden @change="onPickImages" />
|
||||||
</div>
|
</div>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<Codemirror
|
<CodeMirror
|
||||||
v-model="modelValue"
|
v-model="modelValue"
|
||||||
:extensions="extensions"
|
:extensions="extensions"
|
||||||
:tab-size="4"
|
:tab-size="4"
|
||||||
:style="{ height: '100%', fontSize: '14px' }"
|
font-size="14px"
|
||||||
@ready="view = $event.view"
|
@ready="view = $event"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { bracketMatching } from "@codemirror/language"
|
import { bracketMatching } from "@codemirror/language"
|
||||||
import { Codemirror } from "vue-codemirror"
|
import CodeMirror from "./CodeMirror.vue"
|
||||||
import { autocompletion, closeBrackets, completeAnyWord } from "@codemirror/autocomplete"
|
import { autocompletion, closeBrackets, completeAnyWord } from "@codemirror/autocomplete"
|
||||||
import type { EditorView } from "@codemirror/view"
|
import type { EditorView } from "@codemirror/view"
|
||||||
import type { Extension } from "@codemirror/state"
|
import type { Extension } from "@codemirror/state"
|
||||||
@@ -68,10 +68,6 @@ const extensions = computed(() => [
|
|||||||
...extraExtensions,
|
...extraExtensions,
|
||||||
])
|
])
|
||||||
|
|
||||||
interface EditorReadyPayload {
|
|
||||||
view: EditorView
|
|
||||||
}
|
|
||||||
|
|
||||||
// shallowRef,不是 ref:CodeMirror 的 EditorView 是带 getter 的类实例,
|
// shallowRef,不是 ref:CodeMirror 的 EditorView 是带 getter 的类实例,
|
||||||
// Vue 的 UnwrapRef 深度展开会把它结构化成一个丢了原型方法的假类型,
|
// Vue 的 UnwrapRef 深度展开会把它结构化成一个丢了原型方法的假类型,
|
||||||
// vue-tsc 会报 "missing dispatchTransactions/_root/..." 这类莫名其妙的错。
|
// vue-tsc 会报 "missing dispatchTransactions/_root/..." 这类莫名其妙的错。
|
||||||
@@ -123,11 +119,11 @@ const detach = () => {
|
|||||||
if (wasBound && collabStore.room) collabStore.leave("left")
|
if (wasBound && collabStore.room) collabStore.leave("left")
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleEditorReady = (payload: EditorReadyPayload) => {
|
const handleEditorReady = (view: EditorView) => {
|
||||||
editorView.value = payload.view
|
editorView.value = view
|
||||||
// 也从这里起:学生排队时切去看提交记录、老师在这期间接了单,
|
// 也从这里起:学生排队时切去看提交记录、老师在这期间接了单,
|
||||||
// 等他切回来时 room 早就非空了,只靠下面的 watch 是等不到的
|
// 等他切回来时 room 早就非空了,只靠下面的 watch 是等不到的
|
||||||
bind(payload.view)
|
bind(view)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -206,14 +202,14 @@ onUnmounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Codemirror
|
<CodeMirror
|
||||||
v-model="code"
|
v-model="code"
|
||||||
indentWithTab
|
|
||||||
:extensions="extensions"
|
:extensions="extensions"
|
||||||
:disabled="readonly"
|
:disabled="readonly"
|
||||||
:tab-size="4"
|
:tab-size="4"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:style="{ height, fontSize: `${fontSize}px` }"
|
:height="height"
|
||||||
|
:font-size="`${fontSize}px`"
|
||||||
@ready="handleEditorReady"
|
@ready="handleEditorReady"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@@ -41,6 +41,7 @@
|
|||||||
"version": "1.9.0",
|
"version": "1.9.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/autocomplete": "^6.20.3",
|
"@codemirror/autocomplete": "^6.20.3",
|
||||||
|
"@codemirror/commands": "^6.10.4",
|
||||||
"@codemirror/lang-cpp": "^6.0.3",
|
"@codemirror/lang-cpp": "^6.0.3",
|
||||||
"@codemirror/lang-markdown": "^6.5.2",
|
"@codemirror/lang-markdown": "^6.5.2",
|
||||||
"@codemirror/lang-python": "^6.2.1",
|
"@codemirror/lang-python": "^6.2.1",
|
||||||
@@ -76,7 +77,6 @@
|
|||||||
"pinyin-pro": "^3.29.5",
|
"pinyin-pro": "^3.29.5",
|
||||||
"vue": "^3.5.43",
|
"vue": "^3.5.43",
|
||||||
"vue-chartjs": "^5.3.4",
|
"vue-chartjs": "^5.3.4",
|
||||||
"vue-codemirror": "^6.1.1",
|
|
||||||
"vue-router": "^5.4.0",
|
"vue-router": "^5.4.0",
|
||||||
"y-codemirror.next": "^0.3.6",
|
"y-codemirror.next": "^0.3.6",
|
||||||
"y-protocols": "1.0.7",
|
"y-protocols": "1.0.7",
|
||||||
@@ -1565,8 +1565,6 @@
|
|||||||
|
|
||||||
"vue-chartjs": ["vue-chartjs@5.3.4", "https://registry.npmjs.com/vue-chartjs/-/vue-chartjs-5.3.4.tgz", { "peerDependencies": { "chart.js": "^4.1.1", "vue": "^3.0.0-0 || ^2.7.0" } }, "sha512-x3Fqob8RQvrTdssfi9ecsCzEkFOd8JPmNwSkSQzdfKj/uBsRJs/Y88cZcZIEcPsTVfMGwMo4MOoihoDG2DoE/g=="],
|
"vue-chartjs": ["vue-chartjs@5.3.4", "https://registry.npmjs.com/vue-chartjs/-/vue-chartjs-5.3.4.tgz", { "peerDependencies": { "chart.js": "^4.1.1", "vue": "^3.0.0-0 || ^2.7.0" } }, "sha512-x3Fqob8RQvrTdssfi9ecsCzEkFOd8JPmNwSkSQzdfKj/uBsRJs/Y88cZcZIEcPsTVfMGwMo4MOoihoDG2DoE/g=="],
|
||||||
|
|
||||||
"vue-codemirror": ["vue-codemirror@6.1.1", "https://registry.npmjs.com/vue-codemirror/-/vue-codemirror-6.1.1.tgz", { "dependencies": { "@codemirror/commands": "6.x", "@codemirror/language": "6.x", "@codemirror/state": "6.x", "@codemirror/view": "6.x" }, "peerDependencies": { "codemirror": "6.x", "vue": "3.x" } }, "sha512-rTAYo44owd282yVxKtJtnOi7ERAcXTeviwoPXjIc6K/IQYUsoDkzPvw/JDFtSP6T7Cz/2g3EHaEyeyaQCKoDMg=="],
|
|
||||||
|
|
||||||
"vue-demi": ["vue-demi@0.14.10", "https://registry.npmjs.com/vue-demi/-/vue-demi-0.14.10.tgz", { "peerDependencies": { "@vue/composition-api": "^1.0.0-rc.1", "vue": "^3.0.0-0 || ^2.6.0" }, "optionalPeers": ["@vue/composition-api"], "bin": { "vue-demi-fix": "bin/vue-demi-fix.js", "vue-demi-switch": "bin/vue-demi-switch.js" } }, "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg=="],
|
"vue-demi": ["vue-demi@0.14.10", "https://registry.npmjs.com/vue-demi/-/vue-demi-0.14.10.tgz", { "peerDependencies": { "@vue/composition-api": "^1.0.0-rc.1", "vue": "^3.0.0-0 || ^2.6.0" }, "optionalPeers": ["@vue/composition-api"], "bin": { "vue-demi-fix": "bin/vue-demi-fix.js", "vue-demi-switch": "bin/vue-demi-switch.js" } }, "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg=="],
|
||||||
|
|
||||||
"vue-router": ["vue-router@5.4.0", "https://registry.npmjs.com/vue-router/-/vue-router-5.4.0.tgz", { "dependencies": { "@vue-macros/common": "^3.1.3", "@vue/devtools-api": "^8.1.5", "ast-walker-scope": "^0.10.0", "chokidar": "^5.0.0", "confbox": "^0.3.1", "local-pkg": "^1.2.1", "magic-string": "^1.4.3", "mlly": "^1.8.2", "muggle-string": "^0.4.1", "nostics": "^1.3.0", "pathe": "^2.0.3", "picomatch": "^4.0.7", "scule": "^1.3.0", "tinyglobby": "^0.2.17", "unplugin": "^3.4.0", "unplugin-utils": "^0.3.2" }, "peerDependencies": { "@pinia/colada": ">=0.21.2", "@vue/compiler-sfc": "^3.5.34 || ^4.0.0", "pinia": "^3.0.4 || ^4.0.2", "vite": "^7.3.0 || ^8.0.0", "vue": "^3.5.34 || ^4.0.0" }, "optionalPeers": ["@pinia/colada", "@vue/compiler-sfc", "pinia", "vite"] }, "sha512-pFvcYcxinDHjZUlf7AzFBrT8r8Xqvv4uMHoJnjc+EqPawXQvHTkF8S4tfSmQLpriVHUGfkaKlI7Et0KRjTMTdw=="],
|
"vue-router": ["vue-router@5.4.0", "https://registry.npmjs.com/vue-router/-/vue-router-5.4.0.tgz", { "dependencies": { "@vue-macros/common": "^3.1.3", "@vue/devtools-api": "^8.1.5", "ast-walker-scope": "^0.10.0", "chokidar": "^5.0.0", "confbox": "^0.3.1", "local-pkg": "^1.2.1", "magic-string": "^1.4.3", "mlly": "^1.8.2", "muggle-string": "^0.4.1", "nostics": "^1.3.0", "pathe": "^2.0.3", "picomatch": "^4.0.7", "scule": "^1.3.0", "tinyglobby": "^0.2.17", "unplugin": "^3.4.0", "unplugin-utils": "^0.3.2" }, "peerDependencies": { "@pinia/colada": ">=0.21.2", "@vue/compiler-sfc": "^3.5.34 || ^4.0.0", "pinia": "^3.0.4 || ^4.0.2", "vite": "^7.3.0 || ^8.0.0", "vue": "^3.5.34 || ^4.0.0" }, "optionalPeers": ["@pinia/colada", "@vue/compiler-sfc", "pinia", "vite"] }, "sha512-pFvcYcxinDHjZUlf7AzFBrT8r8Xqvv4uMHoJnjc+EqPawXQvHTkF8S4tfSmQLpriVHUGfkaKlI7Et0KRjTMTdw=="],
|
||||||
|
|||||||
Reference in new issue
Block a user