Private
Public Access
1
0

重构:CodeMirror 架构优化

核心优化:
- 新增统一导出避免多实例问题
- 语言加载器从动态改为静态导入
- 使用 Compartment 实现主题/语言动态切换

依赖清理:
- 移除废弃的 @codemirror/highlight
- 移除不再使用的 @codemirror/legacy-modes

组件优化:
- CodeEditor 添加内容更新防抖
- 改进亮色主题样式
- 移除不必要的编辑器重建逻辑

构建配置:
- 简化 Vite manualChunks 配置
- 优化依赖预加载列表

文档清理:
- 删除过期的代码审查文档
- 更新版本号 0.3.0 → 0.3.2
This commit is contained in:
2026-02-06 11:32:27 +08:00
parent 9eb39fbb8f
commit 0229cab550
30 changed files with 592 additions and 3971 deletions

View File

@@ -82,6 +82,7 @@ import SettingsPanel from './components/SettingsPanel.vue'
import UpdateNotification from './components/UpdateNotification.vue'
import { useUpdateStore } from './stores/update'
import { useConfigStore } from './stores/config'
import { preloadCommonLanguages } from './utils/codeMirrorLoader'
// 存储键
const ACTIVE_TAB_STORAGE_KEY = 'app-active-tab'
@@ -138,6 +139,9 @@ const getComponent = (key) => {
onMounted(() => {
loadConfig()
// 预加载常用编辑器语言包
preloadCommonLanguages()
// 设置更新事件监听
updateStore.setupEventListeners()

View File

@@ -4,14 +4,30 @@
<script setup>
import { ref, onMounted, watch, onBeforeUnmount, computed, nextTick } from 'vue'
import { EditorView, lineNumbers, highlightActiveLineGutter, keymap } from '@codemirror/view'
import { EditorState } from '@codemirror/state'
import { defaultKeymap, history } from '@codemirror/commands'
import { bracketMatching } from '@codemirror/language'
import { oneDark } from '@codemirror/theme-one-dark'
import {
EditorView, lineNumbers, highlightActiveLineGutter, keymap,
EditorState, Compartment,
defaultKeymap, history,
bracketMatching, defaultHighlightStyle, syntaxHighlighting,
oneDark
} from '@/utils/codemirrorExports'
import { useThemeStore } from '@/stores/theme'
import { loadLanguageExtension, getLanguageFromExtension } from '@/utils/codeMirrorLoader'
// ==================== 主题定义 ====================
// 亮色主题的基础样式
const lightTheme = EditorView.theme({
'&': { backgroundColor: '#ffffff' },
'.cm-gutters': { backgroundColor: '#f7f7f7', color: '#999', border: 'none' },
'.cm-activeLineGutter': { backgroundColor: '#e8e8e8', color: '#333' },
'.cm-line': { caretColor: '#000' },
'.cm-selection': { backgroundColor: '#d9d9d9' },
'.cm-cursor': { borderLeftColor: '#000' }
})
// ==================== Props & Emits ====================
const props = defineProps({
modelValue: { type: String, required: true },
fileExtension: { type: String, default: '' }
@@ -19,69 +35,144 @@ const props = defineProps({
const emit = defineEmits(['update:modelValue'])
// ==================== 状态管理 ====================
const themeStore = useThemeStore()
const editorContainer = ref(null)
let view = null
const createExtensions = async () => {
// 使用 Compartment 实现动态切换,避免重建编辑器
const themeCompartment = new Compartment()
const languageCompartment = new Compartment()
// ==================== 防抖处理 ====================
let emitTimeout = null
const debouncedEmit = (value) => {
if (emitTimeout) {
clearTimeout(emitTimeout)
}
emitTimeout = setTimeout(() => {
emit('update:modelValue', value)
}, 150)
}
// 获取当前主题扩展
const getThemeExtension = () => {
if (themeStore.isDark) {
return [oneDark]
} else {
// 亮色主题:使用默认语法高亮样式
return [
EditorView.theme({
'&': { backgroundColor: '#ffffff' },
'.cm-gutters': { backgroundColor: '#f7f7f7', color: '#999', border: 'none' },
'.cm-activeLineGutter': { backgroundColor: '#e8e8e8', color: '#333' },
'.cm-line': { caretColor: '#000' },
'.cm-selection': { backgroundColor: '#d9d9d9' },
'.cm-cursor': { borderLeftColor: '#000' }
}),
syntaxHighlighting(defaultHighlightStyle)
]
}
}
// ==================== 扩展配置 ====================
const createExtensions = () => {
const extensions = [
lineNumbers(),
highlightActiveLineGutter(),
history(),
keymap.of(defaultKeymap),
bracketMatching(),
// 内容更新监听(带防抖)
EditorView.updateListener.of((update) => {
if (update.docChanged) {
emit('update:modelValue', update.state.doc.toString())
debouncedEmit(update.state.doc.toString())
}
}),
// 基础样式
EditorView.theme({
'&': { height: '100%', fontSize: '13px' },
'.cm-scroller': { overflow: 'auto', fontFamily: 'Consolas, Monaco, Courier New, monospace' },
'.cm-content': { padding: '8px', minHeight: '100%' },
'.cm-line': { padding: '0 0' },
'&.cm-focused': { outline: 'none' }
})
}),
// 使用 Compartment 支持动态切换主题
themeCompartment.of(getThemeExtension()),
// 使用 Compartment 支持动态切换语言
languageCompartment.of([])
]
if (themeStore.isDark) {
extensions.push(oneDark)
}
const language = getLanguageFromExtension(props.fileExtension)
if (language !== 'text') {
const langExtension = await loadLanguageExtension(language)
if (langExtension) {
extensions.push(langExtension)
}
}
return extensions
}
const createEditor = async (docContent = '') => {
// ==================== 语言管理 ====================
const initLanguage = async () => {
const language = getLanguageFromExtension(props.fileExtension)
if (language === 'text') return
try {
const langExtension = await loadLanguageExtension(language)
if (langExtension && view) {
view.dispatch({
effects: languageCompartment.reconfigure(langExtension)
})
}
} catch (error) {
console.warn(`[CodeEditor] 加载语言包失败: ${language}`, error)
}
}
// ==================== 编辑器创建 ====================
const createEditor = (docContent = '') => {
if (!editorContainer.value) return
const extensions = await createExtensions()
const state = EditorState.create({ doc: docContent, extensions })
const state = EditorState.create({
doc: docContent,
extensions: createExtensions()
})
view = new EditorView({ state, parent: editorContainer.value })
// 初始化语言
initLanguage()
}
const recreateEditor = async () => {
if (!view) return
const currentDoc = view.state.doc.toString()
view.destroy()
await createEditor(currentDoc)
}
// ==================== 生命周期 ====================
onMounted(async () => {
await createEditor(props.modelValue || '')
onMounted(() => {
createEditor(props.modelValue || '')
// 确保主题正确应用(在下一 tick
nextTick(() => {
if (view) {
view.dispatch({
effects: themeCompartment.reconfigure(getThemeExtension())
})
}
})
})
onBeforeUnmount(() => {
if (emitTimeout) {
clearTimeout(emitTimeout)
}
view?.destroy()
view = null
})
// ==================== 监听器 ====================
// 监听外部内容变化
watch(() => props.modelValue, (newValue) => {
if (view && newValue !== view.state.doc.toString()) {
view.dispatch({
@@ -90,24 +181,39 @@ watch(() => props.modelValue, (newValue) => {
}
})
const isDark = computed(() => themeStore.isDark)
watch([isDark, () => props.fileExtension], async () => {
await nextTick()
await recreateEditor()
// 监听主题变化(使用 Compartment 重建,不丢失状态)
watch(() => themeStore.isDark, () => {
if (view) {
view.dispatch({
effects: themeCompartment.reconfigure(getThemeExtension())
})
}
})
// 监听文件扩展名变化(重新加载语言)
watch(() => props.fileExtension, () => {
initLanguage()
})
</script>
<style scoped>
.codemirror-editor {
height: 100%;
width: 100%;
overflow: hidden;
}
.codemirror-editor :deep(.cm-editor) {
height: 100%;
width: 100%;
}
.codemirror-editor :deep(.cm-scroller) {
overflow: auto;
height: 100%;
}
.codemirror-editor :deep(.cm-content) {
height: 100%;
}
</style>

View File

@@ -162,11 +162,14 @@ const onSubmenuLeave = () => {
leaveTimer.value = scheduleClose(100)
}
const onClick = () => {
const onClick = (event: MouseEvent) => {
if (leaveTimer.value) clearTimeout(leaveTimer.value)
const event = props.item.isDir ? 'navigate' : 'openFile'
emit(event, props.item.path)
// 阻止事件冒泡,避免触发父级 breadcrumb-segment 的点击
event.stopPropagation()
const eventType = props.item.isDir ? 'navigate' : 'openFile'
emit(eventType, props.item.path)
}
const emitNavigate = (path: string) => emit('navigate', path)

View File

@@ -1,98 +0,0 @@
<template>
<div class="code-editor">
<!-- 代码编辑器 -->
<CodeMirror
v-if="!isEditMode"
:model-value="content"
:extensions="extensions"
:style="{ height: `${height}px` }"
@update:model-value="handleContentUpdate"
readonly
/>
<!-- 编辑模式 -->
<CodeMirror
v-else
v-model="editableContent"
:extensions="extensions"
:style="{ height: `${height}px` }"
/>
</div>
</template>
<script setup lang="ts">
import { ref, watch, computed } from 'vue'
import CodeMirror from 'vue-codemirror6'
import { javascript } from '@codemirror/lang-javascript'
import { oneDark } from '@codemirror/theme-one-dark'
import { keymap } from '@codemirror/view'
import { EditorView } from '@codemirror/view'
import { EditorState } from '@codemirror/state'
import { basicSetup } from 'codemirror'
import { markdown } from '@codemirror/lang-markdown'
// Props
interface Props {
content: string
height: number
isEditMode: boolean
currentFileExtension: string
}
const props = withDefaults(defineProps<Props>(), {
height: 400
})
// Emits
interface Emits {
(e: 'update:content', content: string): void
(e: 'save'): void
}
const emit = defineEmits<Emits>()
// 可编辑内容
const editableContent = ref(props.content)
// 监听 content 变化
watch(() => props.content, (newContent) => {
editableContent.value = newContent
})
// 内容更新
const handleContentUpdate = (value: string) => {
emit('update:content', value)
}
// 根据文件扩展名获取语言
const getLanguage = (ext: string) => {
const languageMap: Record<string, any> = {
js: javascript(),
jsx: javascript(),
ts: javascript(),
tsx: javascript(),
md: markdown()
}
return languageMap[ext] || []
}
// CodeMirror 扩展
const extensions = computed(() => {
const ext = props.currentFileExtension
return [
basicSetup,
keymap.of(/* 添加快捷键 */),
EditorView.theme({ '&': { height: '100%' }, '.cm-scroller': { overflow: 'auto' } }),
oneDark,
...getLanguage(ext)
]
})
</script>
<style scoped>
.code-editor {
width: 100%;
height: 100%;
}
</style>

View File

@@ -1,190 +0,0 @@
<template>
<div class="file-editor-panel" :style="{ width: width + '%' }">
<!-- 面板标题 -->
<div class="panel-header">
<span class="panel-title">{{ title }}</span>
<div class="panel-actions">
<a-button v-if="canSave" type="primary" size="small" @click="handleSave">
保存
</a-button>
<a-button v-if="canReset" size="small" type="outline" @click="handleReset">
重置
</a-button>
<a-button
v-if="isEditableWithPreview"
size="small"
type="text"
@click="handleToggleEditMode"
>
{{ isEditMode ? '预览' : '编辑' }}
</a-button>
</div>
</div>
<!-- 编辑器内容 -->
<div class="editor-content">
<!-- 代码/文本编辑器 -->
<CodeEditor
v-if="!isMediaFile && !isPdfFile && !isBinary"
:content="fileContent"
:height="height"
:isEditMode="isEditMode"
:currentFileExtension="currentFileExtension"
@update:content="handleContentUpdate"
/>
<!-- 媒体预览 -->
<MediaPreview
v-else-if="isMediaFile"
:url="previewUrl"
:type="mediaType"
@load="handleMediaLoad"
@error="handleMediaError"
/>
<!-- PDF预览 -->
<iframe
v-else-if="isPdfFile"
:src="previewUrl"
class="preview-pdf"
></iframe>
<!-- 二进制文件信息 -->
<BinaryInfo v-else :content="fileContent" />
</div>
<!-- 底部调整条 -->
<div v-if="!isBinary && !isMediaFile" class="resizer" @mousedown="handleStartResize"></div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import CodeEditor from './FileEditor/CodeEditor.vue'
import MediaPreview from './FileEditor/MediaPreview.vue'
import BinaryInfo from './FileEditor/BinaryInfo.vue'
// Props
interface Props {
config: any
width: number
currentDirectory: string
}
const props = defineProps<Props>()
// Emits
interface Emits {
(e: 'save'): void
(e: 'reset'): void
(e: 'toggleEditMode'): void
(e: 'startResize', event: MouseEvent): void
(e: 'contentUpdate', content: string): void
(e: 'imageLoad', dimensions: string): void
(e: 'imageError'): void
}
const emit = defineEmits<Emits>()
// 计算属性
const title = computed(() => {
if (props.config.isImageView) return '🖼️ 图片预览'
if (props.config.isVideoView) return '🎬 视频预览'
if (props.config.isAudioView) return '🎵 音频预览'
if (props.config.isPdfFile) return '📕 PDF 预览'
if (props.config.isHtmlFile) return '🌐 HTML'
if (props.config.isMarkdownFile) return '📝 Markdown'
if (props.config.isBinaryFile) return ' 二进制文件'
return '📝 文件内容'
})
const fileContent = computed(() => props.config.fileContent || '')
const isEditMode = computed(() => props.config.isEditMode || false)
const height = computed(() => props.config.fileContentHeight || 400)
const previewUrl = computed(() => props.config.previewUrl || '')
const currentFileExtension = computed(() => props.config.currentFileExtension || '')
const canSave = computed(() => props.config.canSaveFile || false)
const canReset = computed(() => props.config.canResetContent || false)
const isEditableWithPreview = computed(() => {
const ext = currentFileExtension.value
return ['html', 'htm', 'md', 'markdown'].includes(ext)
})
const isMediaFile = computed(() =>
props.config.isImageView ||
props.config.isVideoView ||
props.config.isAudioView
)
const isPdfFile = computed(() => props.config.isPdfFile)
const isBinary = computed(() => props.config.isBinaryFile)
const mediaFileType = computed(() => {
if (props.config.isImageView) return 'image'
if (props.config.isVideoView) return 'video'
if (props.config.isAudioView) return 'audio'
return 'image'
})
// 事件处理
const handleSave = () => emit('save')
const handleReset = () => emit('reset')
const handleToggleEditMode = () => emit('toggleEditMode')
const handleStartResize = (event: MouseEvent) => emit('startResize', event)
const handleContentUpdate = (content: string) => emit('contentUpdate', content)
const handleMediaLoad = (dimensions: string) => emit('imageLoad', dimensions)
const handleMediaError = () => emit('imageError')
</script>
<style scoped>
.file-editor-panel {
display: flex;
flex-direction: column;
height: 100%;
background: var(--color-bg-1);
border-left: 1px solid var(--color-border);
}
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
border-bottom: 1px solid var(--color-border);
background: var(--color-bg-2);
}
.panel-title {
font-size: 13px;
font-weight: 600;
color: var(--color-text-1);
}
.panel-actions {
display: flex;
gap: 8px;
}
.editor-content {
flex: 1;
overflow: hidden;
position: relative;
}
.preview-pdf {
width: 100%;
height: 100%;
border: none;
}
.resizer {
height: 4px;
background: var(--color-border);
cursor: row-resize;
transition: background 0.2s;
}
.resizer:hover {
background: rgb(var(--primary-6));
}
</style>

View File

@@ -101,24 +101,20 @@ interface PathSegment {
const segments = computed<PathSegment[]>(() => {
if (!props.path) return []
const normalizedPath = props.path.replace(/\\/g, '/')
const path = props.path.replace(/\\/g, '/')
if (/^[A-Za-z]:\/?$/.test(normalizedPath)) {
const driveLetter = normalizedPath.charAt(0) + ':'
return [{ name: driveLetter, path: driveLetter + '/' }]
// 根目录
if (/^[A-Za-z]:\/?$/.test(path)) {
const drive = path[0] + ':'
return [{ name: drive, path: drive + '/' }]
}
const parts = normalizedPath.split('/').filter(p => p)
let currentPath = ''
return parts.map((part, index) => {
if (index === 0 && part.endsWith(':')) {
currentPath = part + '/'
} else {
currentPath += '/' + part
}
return { name: part, path: currentPath }
})
return path.split('/').filter(Boolean).reduce<PathSegment[]>((acc, part, i) => {
const prev = acc[i - 1]?.path || ''
const current = part.endsWith(':') ? part + '/' : prev + (prev.endsWith('/') ? '' : '/') + part
acc.push({ name: part, path: current })
return acc
}, [])
})
const activeIndex = ref<number | null>(null)

View File

@@ -23,6 +23,9 @@ export function useFileEdit(options: UseFileEditOptions = {}) {
const fileContent = ref('')
const originalContent = ref('')
// 当前文件路径(用于验证更新是否来自当前文件)
const currentFilePathRef = ref('')
// 编辑状态
const isEditMode = ref(false)
const fileContentHeight = ref(400)
@@ -34,6 +37,12 @@ export function useFileEdit(options: UseFileEditOptions = {}) {
// 保存状态
const isSaving = ref(false)
// 文件版本跟踪(用于防止切换文件后的过期更新)
const fileVersion = ref(0)
// 最后一次文件加载的时间戳,用于过滤过期更新
const lastLoadTime = ref(0)
// 使用文件操作 composable
const { readFile, writeFile } = useFileOperations({
onSuccess: (operation, data) => {
@@ -198,6 +207,15 @@ export function useFileEdit(options: UseFileEditOptions = {}) {
try {
isBinaryFile.value = false
// 记录当前加载的文件路径,用于后续验证更新
currentFilePathRef.value = path
// 增加文件版本号,使之前的过期更新失效
fileVersion.value++
// 记录加载时间戳,用于过滤过期更新
lastLoadTime.value = Date.now()
// 先清空内容,避免显示之前文件的内容
fileContent.value = ''
originalContent.value = ''
@@ -486,8 +504,32 @@ ${ext ? `文件类型: ${fileTypeDesc}\n` : ''}
/**
* 更新文件内容
* 注意:需要确保更新后 fileContent 和 originalContent 保持正确的同步关系
*/
const updateContent = (content: string) => {
const updateContent = (content: string, expectedVersion?: number) => {
// 如果提供了期望的版本号,检查是否匹配
// 这用于防止快速切换文件时,旧文件的防抖更新覆盖新文件的内容
if (expectedVersion !== undefined && expectedVersion !== fileVersion.value) {
// 版本不匹配,这是一个过期的更新,忽略它
console.debug('[useFileEdit] 忽略过期更新(版本不匹配):', {
expected: expectedVersion,
current: fileVersion.value,
content: content.substring(0, 50)
})
return
}
// 额外检查:如果更新是在文件加载后的短时间内,可能是过期更新
// 防抖时间是 150ms我们使用 300ms 的安全边际
const timeSinceLoad = Date.now() - lastLoadTime.value
if (timeSinceLoad < 300) {
console.debug('[useFileEdit] 忽略过期更新(时间窗口内):', {
timeSinceLoad,
content: content.substring(0, 50)
})
return
}
// 确保只有在内容真正改变时才更新
if (fileContent.value !== content) {
fileContent.value = content
@@ -538,6 +580,7 @@ ${ext ? `文件类型: ${fileTypeDesc}\n` : ''}
isSaving,
isBinaryFile,
draftKey,
fileVersion,
// 计算属性
contentChanged,

View File

@@ -240,7 +240,7 @@ const { previewUrl, updatePreviewUrl, imageLoading, currentImageDimensions, dete
})
// 文件编辑
const { fileContent, originalContent, isEditMode, fileContentHeight, contentChanged, canSaveFile, canResetContent, loadFile, saveFile, resetContent, clearContent, toggleEditMode, updateContent, setEditorHeight, isBinaryFile: isBinaryFileRef } =
const { fileContent, originalContent, isEditMode, fileContentHeight, contentChanged, canSaveFile, canResetContent, loadFile, saveFile, resetContent, clearContent, toggleEditMode, updateContent, setEditorHeight, isBinaryFile: isBinaryFileRef, fileVersion } =
useFileEdit({
currentFilePath: selectedFileItem,
currentDirectory: filePath
@@ -927,7 +927,8 @@ const handleStartResize = (event: MouseEvent) => {
}
const handleContentUpdate = (content: string) => {
updateContent(content)
// useFileEdit 内部会检查版本号和时间,防止过期更新
updateContent(content, fileVersion.value)
}
const handleImageLoad = (dimensions: string) => {

View File

@@ -1,88 +1,81 @@
/**
* CodeMirror 语言包动态加载器
* 按需加载语言支持,减少初始包体积和构建时间
* CodeMirror 语言包加载器
* 使用统一导出避免多实例问题
*/
import {
javascript, json, yaml, html, css,
cpp, rust, go, python, php, sql, markdown, java
} from './codemirrorExports'
const languageCache = new Map()
/**
* 动态加载 CodeMirror 语言扩展
* 获取语言扩展
* @param {string} language - 语言名称
* @returns {Promise<Extension|null>} CodeMirror 语言扩展
* @returns {Extension|null} CodeMirror 语言扩展
*/
export async function loadLanguageExtension(language) {
export function loadLanguageExtension(language) {
// 检查缓存
if (languageCache.has(language)) {
return languageCache.get(language)
}
try {
let extension
let extension = null
// 现代语言包(直接返回扩展)
const modernLangs = {
javascript: ['@codemirror/lang-javascript', 'javascript', { jsx: true }],
typescript: ['@codemirror/lang-javascript', 'javascript', { jsx: true }],
json: ['@codemirror/lang-json', 'json'],
yaml: ['@codemirror/lang-yaml', 'yaml'],
html: ['@codemirror/lang-html', 'html'],
css: ['@codemirror/lang-css', 'css'],
cpp: ['@codemirror/lang-cpp', 'cpp'],
c: ['@codemirror/lang-cpp', 'cpp'],
rust: ['@codemirror/lang-rust', 'rust'],
go: ['@codemirror/lang-go', 'go'],
python: ['@codemirror/lang-python', 'python'],
php: ['@codemirror/lang-php', 'php'],
sql: ['@codemirror/lang-sql', 'sql'],
markdown: ['@codemirror/lang-markdown', 'markdown'],
java: ['@codemirror/lang-java', 'java']
}
if (modernLangs[language]) {
const [path, method, ...args] = modernLangs[language]
const mod = await import(path)
extension = mod[method](...args)
} else {
// Legacy 语言包(需要 StreamLanguage 包装)
const legacyLangs = {
ruby: ['@codemirror/legacy-modes/mode/ruby', 'ruby'],
shell: ['@codemirror/legacy-modes/mode/shell', 'shell'],
bash: ['@codemirror/legacy-modes/mode/shell', 'shell'],
kotlin: ['@codemirror/legacy-modes/mode/clike', 'kotlin'],
csharp: ['@codemirror/legacy-modes/mode/clike', 'csharp'],
swift: ['@codemirror/legacy-modes/mode/swift', 'swift'],
r: ['@codemirror/legacy-modes/mode/r', 'r'],
perl: ['@codemirror/legacy-modes/mode/perl', 'perl'],
latex: ['@codemirror/legacy-modes/mode/stex', 'stex'],
tex: ['@codemirror/legacy-modes/mode/stex', 'stex'],
xml: ['@codemirror/legacy-modes/mode/xml', 'xml'],
svg: ['@codemirror/legacy-modes/mode/xml', 'xml'],
properties: ['@codemirror/legacy-modes/mode/properties', 'properties'],
ini: ['@codemirror/legacy-modes/mode/properties', 'properties'],
cfg: ['@codemirror/legacy-modes/mode/properties', 'properties'],
conf: ['@codemirror/legacy-modes/mode/properties', 'properties'],
dockerfile: ['@codemirror/legacy-modes/mode/dockerfile', 'dockerFile'],
matlab: ['@codemirror/legacy-modes/mode/octave', 'octave'],
octave: ['@codemirror/legacy-modes/mode/octave', 'octave']
}
if (legacyLangs[language]) {
const [path, method] = legacyLangs[language]
const [modeMod, { StreamLanguage }] = await Promise.all([
import(path),
import('@codemirror/language')
])
extension = StreamLanguage.define(modeMod[method])
}
}
if (extension) {
languageCache.set(language, extension)
}
return extension
} catch (error) {
console.error(`[CodeMirror] 加载语言包失败: ${language}`, error)
return null
// 使用静态导入的语言包
switch (language) {
case 'javascript':
extension = javascript({ jsx: true })
break
case 'typescript':
extension = javascript({ typescript: true, jsx: true })
break
case 'json':
extension = json()
break
case 'yaml':
extension = yaml()
break
case 'html':
extension = html()
break
case 'css':
extension = css()
break
case 'cpp':
case 'c':
extension = cpp()
break
case 'rust':
extension = rust()
break
case 'go':
extension = go()
break
case 'python':
extension = python()
break
case 'php':
extension = php()
break
case 'sql':
extension = sql()
break
case 'markdown':
extension = markdown()
break
case 'java':
extension = java()
break
default:
return null
}
if (extension) {
languageCache.set(language, extension)
}
return extension
}
/**
@@ -98,7 +91,6 @@ export function getLanguageFromExtension(extension) {
ts: 'typescript', tsx: 'typescript',
json: 'json',
yaml: 'yaml', yml: 'yaml',
xml: 'xml', xhtml: 'xml', svg: 'svg',
html: 'html', htm: 'html',
css: 'css', scss: 'css', sass: 'css', less: 'css',
cpp: 'cpp', c: 'c', cc: 'cpp', cxx: 'cpp', h: 'cpp', hpp: 'cpp', hxx: 'cpp',
@@ -106,24 +98,9 @@ export function getLanguageFromExtension(extension) {
go: 'go',
python: 'python', py: 'python', pyw: 'python',
php: 'php',
ruby: 'ruby', rb: 'ruby',
perl: 'perl', pl: 'perl', pm: 'perl',
shell: 'shell', sh: 'shell', bash: 'shell', zsh: 'shell',
bat: 'shell', cmd: 'shell', ps1: 'shell',
sql: 'sql',
java: 'java',
kotlin: 'kotlin', kt: 'kotlin', kts: 'kotlin',
csharp: 'csharp', cs: 'csharp', csx: 'csharp',
swift: 'swift',
markdown: 'markdown', md: 'markdown',
r: 'r',
matlab: 'matlab', m: 'matlab',
latex: 'latex', tex: 'latex',
dockerfile: 'dockerfile',
makefile: 'makefile', mk: 'makefile', gnumakefile: 'makefile',
ini: 'ini', cfg: 'ini', conf: 'ini', properties: 'properties',
gitignore: 'gitignore',
txt: 'text', text: 'text', log: 'text', csv: 'text'
java: 'java'
}
return langMap[ext] || 'text'
@@ -133,6 +110,7 @@ export function getLanguageFromExtension(extension) {
* 预加载常用语言包
* 用于在应用启动时预热缓存
*/
export async function preloadCommonLanguages() {
await Promise.all(['javascript', 'json', 'markdown', 'python', 'sql'].map(loadLanguageExtension))
export function preloadCommonLanguages() {
// 现在是同步的,不需要 Promise.all
;['javascript', 'json', 'markdown', 'python', 'sql'].forEach(loadLanguageExtension)
}

View File

@@ -0,0 +1,26 @@
/**
* CodeMirror 统一导出
* 确保所有模块使用同一个 CodeMirror 实例,避免多实例问题
*/
// Core
export { EditorView, lineNumbers, highlightActiveLineGutter, keymap, drawSelection, dropCursor } from '@codemirror/view'
export { EditorState, Compartment, Facet, StateEffect, StateField } from '@codemirror/state'
export { defaultKeymap, history, historyKeymap } from '@codemirror/commands'
export { bracketMatching, defaultHighlightStyle, syntaxHighlighting, StreamLanguage } from '@codemirror/language'
export { oneDark } from '@codemirror/theme-one-dark'
// Language packages
export { javascript } from '@codemirror/lang-javascript'
export { json } from '@codemirror/lang-json'
export { yaml } from '@codemirror/lang-yaml'
export { html } from '@codemirror/lang-html'
export { css } from '@codemirror/lang-css'
export { cpp } from '@codemirror/lang-cpp'
export { rust } from '@codemirror/lang-rust'
export { go } from '@codemirror/lang-go'
export { python } from '@codemirror/lang-python'
export { php } from '@codemirror/lang-php'
export { sql } from '@codemirror/lang-sql'
export { markdown } from '@codemirror/lang-markdown'
export { java } from '@codemirror/lang-java'

View File

@@ -43,12 +43,13 @@
import {nextTick, onBeforeUnmount, onMounted, ref, watch} from 'vue'
import {Message} from '@arco-design/web-vue'
import {IconPlayArrow, IconStorage, IconCode} from '@arco-design/web-vue/es/icon'
import {EditorView, keymap, lineNumbers} from '@codemirror/view'
import {EditorState} from '@codemirror/state'
import {sql} from '@codemirror/lang-sql'
import {javascript} from '@codemirror/lang-javascript'
import {defaultKeymap, history, historyKeymap} from '@codemirror/commands'
import {defaultHighlightStyle, syntaxHighlighting} from '@codemirror/language'
import {
EditorView, keymap, lineNumbers,
EditorState,
sql, javascript,
defaultKeymap, history, historyKeymap,
defaultHighlightStyle, syntaxHighlighting
} from '@/utils/codemirrorExports'
import {useTabPersistence} from '../composables/useTabPersistence'
// ==================== Props & Events ====================

View File

@@ -17,10 +17,12 @@
import { ref, onMounted, onBeforeUnmount, nextTick, watch } from 'vue'
import { Message } from '@arco-design/web-vue'
import { IconCopy } from '@arco-design/web-vue/es/icon'
import { EditorView, lineNumbers } from '@codemirror/view'
import { EditorState } from '@codemirror/state'
import { sql } from '@codemirror/lang-sql'
import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language'
import {
EditorView, lineNumbers,
EditorState,
sql,
defaultHighlightStyle, syntaxHighlighting
} from '@/utils/codemirrorExports'
interface Props {
statements: string[]

View File

@@ -1,6 +1,5 @@
import { ref, nextTick } from 'vue'
import { EditorView } from '@codemirror/view'
import { EditorState } from '@codemirror/state'
import { EditorView, EditorState } from '@/utils/codemirrorExports'
export interface TabEditorTab {
id?: number