@@ -0,0 +1,179 @@
|
||||
<script setup lang="ts">
|
||||
import { NSpin } from 'naive-ui'
|
||||
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import { extOf } from '@/components/objectstorage/codeHighlight'
|
||||
|
||||
/** 文本编辑器:CodeMirror 6,行号 / 语法高亮 / 搜索;
|
||||
* 核心与语言包全部动态 import 独立 chunk,不进编辑不下载。
|
||||
* 须放在定高容器内(内部自滚动)。 */
|
||||
const props = defineProps<{
|
||||
modelValue: string
|
||||
objectName: string
|
||||
disabled?: boolean
|
||||
}>()
|
||||
const emit = defineEmits<{ 'update:modelValue': [string] }>()
|
||||
|
||||
type CmExtension = import('@codemirror/state').Extension
|
||||
type CmEditorView = typeof import('@codemirror/view').EditorView
|
||||
|
||||
const host = ref<HTMLElement | null>(null)
|
||||
const booting = ref(true)
|
||||
const bootError = ref('')
|
||||
|
||||
let view: import('@codemirror/view').EditorView | null = null
|
||||
let EV: CmEditorView | null = null
|
||||
let editableComp: import('@codemirror/state').Compartment | null = null
|
||||
|
||||
onMounted(() => void boot())
|
||||
onBeforeUnmount(() => view?.destroy())
|
||||
|
||||
async function boot() {
|
||||
try {
|
||||
const [{ basicSetup }, { EditorView }, { Compartment }, lang, theme] = await Promise.all([
|
||||
import('codemirror'),
|
||||
import('@codemirror/view'),
|
||||
import('@codemirror/state'),
|
||||
loadLanguage(extOf(props.objectName)),
|
||||
themeExtensions(),
|
||||
])
|
||||
EV = EditorView
|
||||
editableComp = new Compartment()
|
||||
view = new EditorView({
|
||||
parent: host.value!,
|
||||
doc: props.modelValue,
|
||||
extensions: [
|
||||
basicSetup,
|
||||
EditorView.lineWrapping,
|
||||
...(lang ? [lang] : []),
|
||||
...theme,
|
||||
editableComp.of(EditorView.editable.of(!props.disabled)),
|
||||
EditorView.updateListener.of((u) => {
|
||||
if (u.docChanged) emit('update:modelValue', u.state.doc.toString())
|
||||
}),
|
||||
],
|
||||
})
|
||||
} catch (e) {
|
||||
bootError.value = e instanceof Error ? e.message : '编辑器加载失败'
|
||||
} finally {
|
||||
booting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 主题与语法配色引用设计 token(明暗自适应),语义与全局 .code-hl 一致 */
|
||||
async function themeExtensions(): Promise<CmExtension[]> {
|
||||
const [{ HighlightStyle, syntaxHighlighting }, { tags: t }, { EditorView }] = await Promise.all([
|
||||
import('@codemirror/language'),
|
||||
import('@lezer/highlight'),
|
||||
import('@codemirror/view'),
|
||||
])
|
||||
const hl = HighlightStyle.define([
|
||||
{ tag: [t.keyword, t.operatorKeyword, t.modifier], color: 'var(--color-warn)' },
|
||||
{ tag: [t.string, t.special(t.string), t.regexp], color: 'var(--color-ok)' },
|
||||
{ tag: [t.number, t.bool, t.null, t.atom], color: 'var(--color-accent)' },
|
||||
{ tag: [t.comment, t.meta], color: 'var(--color-ink-3)', fontStyle: 'italic' },
|
||||
{ tag: [t.propertyName, t.attributeName], color: 'var(--color-info)' },
|
||||
{ tag: [t.typeName, t.className, t.tagName], color: 'var(--color-accent)' },
|
||||
{ tag: t.heading, color: 'var(--color-ink)', fontWeight: '600' },
|
||||
{ tag: t.link, color: 'var(--color-accent)' },
|
||||
])
|
||||
const theme = EditorView.theme({
|
||||
'&': { height: '100%', fontSize: '12px', backgroundColor: 'transparent' },
|
||||
'.cm-scroller': {
|
||||
fontFamily: "ui-monospace, 'SF Mono', Menlo, Consolas, monospace",
|
||||
lineHeight: '1.65',
|
||||
overflow: 'auto',
|
||||
},
|
||||
'.cm-gutters': {
|
||||
backgroundColor: 'var(--color-wash)',
|
||||
color: 'var(--color-ink-3)',
|
||||
border: 'none',
|
||||
},
|
||||
'.cm-activeLineGutter': { backgroundColor: 'var(--color-wash)' },
|
||||
'.cm-activeLine': { backgroundColor: 'color-mix(in srgb, var(--color-wash) 55%, transparent)' },
|
||||
'&.cm-focused': { outline: 'none' },
|
||||
'.cm-selectionBackground, &.cm-focused .cm-selectionBackground': {
|
||||
backgroundColor: 'color-mix(in srgb, var(--color-accent) 18%, transparent)',
|
||||
},
|
||||
})
|
||||
// 不带 fallback:作为正式高亮器压过 basicSetup 内置的 defaultHighlightStyle(其为 fallback)
|
||||
return [theme, syntaxHighlighting(hl)]
|
||||
}
|
||||
|
||||
/** 扩展名 → CodeMirror 语言扩展;不在表内按纯文本编辑 */
|
||||
const CM_LANG: Record<string, () => Promise<CmExtension>> = {
|
||||
json: () => import('@codemirror/lang-json').then((m) => m.json()),
|
||||
yaml: () => import('@codemirror/lang-yaml').then((m) => m.yaml()),
|
||||
yml: () => import('@codemirror/lang-yaml').then((m) => m.yaml()),
|
||||
py: () => import('@codemirror/lang-python').then((m) => m.python()),
|
||||
js: () => import('@codemirror/lang-javascript').then((m) => m.javascript()),
|
||||
ts: () => import('@codemirror/lang-javascript').then((m) => m.javascript({ typescript: true })),
|
||||
xml: () => import('@codemirror/lang-xml').then((m) => m.xml()),
|
||||
svg: () => import('@codemirror/lang-xml').then((m) => m.xml()),
|
||||
html: () => import('@codemirror/lang-html').then((m) => m.html()),
|
||||
htm: () => import('@codemirror/lang-html').then((m) => m.html()),
|
||||
css: () => import('@codemirror/lang-css').then((m) => m.css()),
|
||||
md: () => import('@codemirror/lang-markdown').then((m) => m.markdown()),
|
||||
sql: () => import('@codemirror/lang-sql').then((m) => m.sql()),
|
||||
go: () => import('@codemirror/lang-go').then((m) => m.go()),
|
||||
sh: () => import('@codemirror/legacy-modes/mode/shell').then((m) => streamOf(m.shell)),
|
||||
ini: () => import('@codemirror/legacy-modes/mode/properties').then((m) => streamOf(m.properties)),
|
||||
conf: () =>
|
||||
import('@codemirror/legacy-modes/mode/properties').then((m) => streamOf(m.properties)),
|
||||
cfg: () => import('@codemirror/legacy-modes/mode/properties').then((m) => streamOf(m.properties)),
|
||||
env: () => import('@codemirror/legacy-modes/mode/properties').then((m) => streamOf(m.properties)),
|
||||
properties: () =>
|
||||
import('@codemirror/legacy-modes/mode/properties').then((m) => streamOf(m.properties)),
|
||||
toml: () => import('@codemirror/legacy-modes/mode/toml').then((m) => streamOf(m.toml)),
|
||||
}
|
||||
|
||||
async function streamOf<T>(def: import('@codemirror/language').StreamParser<T>) {
|
||||
const { StreamLanguage } = await import('@codemirror/language')
|
||||
return StreamLanguage.define(def)
|
||||
}
|
||||
|
||||
async function loadLanguage(ext: string): Promise<CmExtension | null> {
|
||||
const load = CM_LANG[ext]
|
||||
if (!load) return null
|
||||
try {
|
||||
return await load()
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(v) => {
|
||||
// 外部改写(如美化格式)同步进编辑器;自身输入产生的 emit 值相等,跳过
|
||||
if (view && v !== view.state.doc.toString())
|
||||
view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: v } })
|
||||
},
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.disabled,
|
||||
(d) => {
|
||||
if (view && editableComp && EV)
|
||||
view.dispatch({ effects: editableComp.reconfigure(EV.editable.of(!d)) })
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full min-h-0 flex-col">
|
||||
<div v-if="booting" class="flex flex-1 items-center justify-center">
|
||||
<NSpin size="small" />
|
||||
</div>
|
||||
<div v-else-if="bootError" class="flex flex-1 items-center justify-center text-xs text-err">
|
||||
{{ bootError }}
|
||||
</div>
|
||||
<div ref="host" class="cm-host min-h-0 flex-1" :class="booting || bootError ? 'hidden' : ''"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.cm-host :deep(.cm-editor) {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user