重构:CodeMirror 架构优化
核心优化: - 新增统一导出避免多实例问题 - 语言加载器从动态改为静态导入 - 使用 Compartment 实现主题/语言动态切换 依赖清理: - 移除废弃的 @codemirror/highlight - 移除不再使用的 @codemirror/legacy-modes 组件优化: - CodeEditor 添加内容更新防抖 - 改进亮色主题样式 - 移除不必要的编辑器重建逻辑 构建配置: - 简化 Vite manualChunks 配置 - 优化依赖预加载列表 文档清理: - 删除过期的代码审查文档 - 更新版本号 0.3.0 → 0.3.2
This commit is contained in:
102
web/package-lock.json
generated
102
web/package-lock.json
generated
@@ -10,7 +10,6 @@
|
||||
"dependencies": {
|
||||
"@arco-design/web-vue": "^2.54.0",
|
||||
"@codemirror/commands": "^6.10.1",
|
||||
"@codemirror/highlight": "^0.19.8",
|
||||
"@codemirror/lang-cpp": "^6.0.3",
|
||||
"@codemirror/lang-css": "^6.3.1",
|
||||
"@codemirror/lang-go": "^6.0.1",
|
||||
@@ -25,7 +24,6 @@
|
||||
"@codemirror/lang-sql": "^6.10.0",
|
||||
"@codemirror/lang-yaml": "^6.1.2",
|
||||
"@codemirror/language": "^6.12.1",
|
||||
"@codemirror/legacy-modes": "^6.5.2",
|
||||
"@codemirror/state": "^6.5.3",
|
||||
"@codemirror/theme-one-dark": "^6.1.3",
|
||||
"@codemirror/view": "^6.39.8",
|
||||
@@ -211,71 +209,6 @@
|
||||
"@lezer/common": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/highlight": {
|
||||
"version": "0.19.8",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/highlight/-/highlight-0.19.8.tgz",
|
||||
"integrity": "sha512-v/lzuHjrYR8MN2mEJcUD6fHSTXXli9C1XGYpr+ElV6fLBIUhMTNKR3qThp611xuWfXfwDxeL7ppcbkM/MzPV3A==",
|
||||
"deprecated": "As of 0.20.0, this package has been split between @lezer/highlight and @codemirror/language",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^0.19.0",
|
||||
"@codemirror/rangeset": "^0.19.0",
|
||||
"@codemirror/state": "^0.19.3",
|
||||
"@codemirror/view": "^0.19.39",
|
||||
"@lezer/common": "^0.15.0",
|
||||
"style-mod": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/highlight/node_modules/@codemirror/language": {
|
||||
"version": "0.19.10",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/language/-/language-0.19.10.tgz",
|
||||
"integrity": "sha512-yA0DZ3RYn2CqAAGW62VrU8c4YxscMQn45y/I9sjBlqB1e2OTQLg4CCkMBuMSLXk4xaqjlsgazeOQWaJQOKfV8Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^0.19.0",
|
||||
"@codemirror/text": "^0.19.0",
|
||||
"@codemirror/view": "^0.19.0",
|
||||
"@lezer/common": "^0.15.5",
|
||||
"@lezer/lr": "^0.15.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/highlight/node_modules/@codemirror/state": {
|
||||
"version": "0.19.9",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/state/-/state-0.19.9.tgz",
|
||||
"integrity": "sha512-psOzDolKTZkx4CgUqhBQ8T8gBc0xN5z4gzed109aF6x7D7umpDRoimacI/O6d9UGuyl4eYuDCZmDFr2Rq7aGOw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/text": "^0.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/highlight/node_modules/@codemirror/view": {
|
||||
"version": "0.19.48",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/view/-/view-0.19.48.tgz",
|
||||
"integrity": "sha512-0eg7D2Nz4S8/caetCTz61rK0tkHI17V/d15Jy0kLOT8dTLGGNJUponDnW28h2B6bERmPlVHKh8MJIr5OCp1nGw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/rangeset": "^0.19.5",
|
||||
"@codemirror/state": "^0.19.3",
|
||||
"@codemirror/text": "^0.19.0",
|
||||
"style-mod": "^4.0.0",
|
||||
"w3c-keyname": "^2.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/highlight/node_modules/@lezer/common": {
|
||||
"version": "0.15.12",
|
||||
"resolved": "https://registry.npmmirror.com/@lezer/common/-/common-0.15.12.tgz",
|
||||
"integrity": "sha512-edfwCxNLnzq5pBA/yaIhwJ3U3Kz8VAUOTRg0hhxaizaI1N+qxV7EXDv/kLCkLeq2RzSFvxexlaj5Mzfn2kY0Ig==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@codemirror/highlight/node_modules/@lezer/lr": {
|
||||
"version": "0.15.8",
|
||||
"resolved": "https://registry.npmmirror.com/@lezer/lr/-/lr-0.15.8.tgz",
|
||||
"integrity": "sha512-bM6oE6VQZ6hIFxDNKk8bKPa14hqFrV07J/vHGOeiAbJReIaQXmkVb6xQu4MR+JBTLa5arGRyAAjJe1qaQt3Uvg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^0.15.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-cpp": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/lang-cpp/-/lang-cpp-6.0.3.tgz",
|
||||
@@ -458,15 +391,6 @@
|
||||
"style-mod": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/legacy-modes": {
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/legacy-modes/-/legacy-modes-6.5.2.tgz",
|
||||
"integrity": "sha512-/jJbwSTazlQEDOQw2FJ8LEEKVS72pU0lx6oM54kGpL8t/NJ2Jda3CZ4pcltiKTdqYSRk3ug1B3pil1gsjA6+8Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lint": {
|
||||
"version": "6.9.2",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/lint/-/lint-6.9.2.tgz",
|
||||
@@ -478,25 +402,6 @@
|
||||
"crelt": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/rangeset": {
|
||||
"version": "0.19.9",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/rangeset/-/rangeset-0.19.9.tgz",
|
||||
"integrity": "sha512-V8YUuOvK+ew87Xem+71nKcqu1SXd5QROMRLMS/ljT5/3MCxtgrRie1Cvild0G/Z2f1fpWxzX78V0U4jjXBorBQ==",
|
||||
"deprecated": "As of 0.20.0, this package has been merged into @codemirror/state",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^0.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/rangeset/node_modules/@codemirror/state": {
|
||||
"version": "0.19.9",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/state/-/state-0.19.9.tgz",
|
||||
"integrity": "sha512-psOzDolKTZkx4CgUqhBQ8T8gBc0xN5z4gzed109aF6x7D7umpDRoimacI/O6d9UGuyl4eYuDCZmDFr2Rq7aGOw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/text": "^0.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/state": {
|
||||
"version": "6.5.3",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/state/-/state-6.5.3.tgz",
|
||||
@@ -506,13 +411,6 @@
|
||||
"@marijn/find-cluster-break": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/text": {
|
||||
"version": "0.19.6",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/text/-/text-0.19.6.tgz",
|
||||
"integrity": "sha512-T9jnREMIygx+TPC1bOuepz18maGq/92q2a+n4qTqObKwvNMg+8cMTslb8yxeEDEq7S3kpgGWxgO1UWbQRij0dA==",
|
||||
"deprecated": "As of 0.20.0, this package has been merged into @codemirror/state",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@codemirror/theme-one-dark": {
|
||||
"version": "6.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz",
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
"dependencies": {
|
||||
"@arco-design/web-vue": "^2.54.0",
|
||||
"@codemirror/commands": "^6.10.1",
|
||||
"@codemirror/highlight": "^0.19.8",
|
||||
"@codemirror/lang-cpp": "^6.0.3",
|
||||
"@codemirror/lang-css": "^6.3.1",
|
||||
"@codemirror/lang-go": "^6.0.1",
|
||||
@@ -25,7 +24,6 @@
|
||||
"@codemirror/lang-sql": "^6.10.0",
|
||||
"@codemirror/lang-yaml": "^6.1.2",
|
||||
"@codemirror/language": "^6.12.1",
|
||||
"@codemirror/legacy-modes": "^6.5.2",
|
||||
"@codemirror/state": "^6.5.3",
|
||||
"@codemirror/theme-one-dark": "^6.1.3",
|
||||
"@codemirror/view": "^6.39.8",
|
||||
|
||||
@@ -1 +1 @@
|
||||
db157c3d15eff27c46a5fa33f3b95e47
|
||||
74b8a7937d28d6e8fb6d93e63e81abf7
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
26
web/src/utils/codemirrorExports.js
Normal file
26
web/src/utils/codemirrorExports.js
Normal 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'
|
||||
@@ -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 ====================
|
||||
|
||||
@@ -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[]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -18,7 +18,9 @@ export default defineConfig({
|
||||
})
|
||||
],
|
||||
resolve: {
|
||||
alias: { '@': resolve(__dirname, 'src') }
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src')
|
||||
}
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
@@ -27,29 +29,9 @@ export default defineConfig({
|
||||
minify: 'esbuild',
|
||||
cssCodeSplit: true,
|
||||
chunkSizeWarningLimit: 1000,
|
||||
esbuild: {
|
||||
target: 'es2020',
|
||||
drop: ['console', 'debugger']
|
||||
},
|
||||
target: 'es2020',
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: (id) => {
|
||||
if (!id.includes('node_modules')) return
|
||||
|
||||
if (id.includes('@codemirror')) {
|
||||
if (id.includes('lang-') || id.includes('legacy-modes')) {
|
||||
return 'vendor-codemirror-langs'
|
||||
}
|
||||
return 'vendor-codemirror-core'
|
||||
}
|
||||
|
||||
if (id.includes('@arco-design')) return 'vendor-arco'
|
||||
if (id.includes('mermaid')) return 'vendor-mermaid'
|
||||
if (id.includes('marked') || id.includes('highlight.js')) return 'vendor-markdown'
|
||||
if (id.includes('vue') || id.includes('pinia')) return 'vendor-vue'
|
||||
|
||||
return 'vendor'
|
||||
},
|
||||
chunkFileNames: 'assets/js/[name]-[hash].js',
|
||||
entryFileNames: 'assets/js/[name]-[hash].js',
|
||||
assetFileNames: 'assets/[ext]/[name]-[hash].[ext]'
|
||||
@@ -57,18 +39,6 @@ export default defineConfig({
|
||||
}
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: [
|
||||
'vue', 'pinia', '@arco-design/web-vue', 'marked', 'highlight.js',
|
||||
'@codemirror/view', '@codemirror/state', '@codemirror/language', '@codemirror/commands',
|
||||
'@codemirror/lang-javascript', '@codemirror/lang-json', '@codemirror/lang-yaml',
|
||||
'@codemirror/lang-html', '@codemirror/lang-css', '@codemirror/lang-markdown',
|
||||
'@codemirror/lang-sql', '@codemirror/lang-java', '@codemirror/lang-python',
|
||||
'@codemirror/lang-php', '@codemirror/lang-rust', '@codemirror/lang-go', '@codemirror/lang-cpp',
|
||||
'@codemirror/legacy-modes/mode/clike', '@codemirror/legacy-modes/mode/ruby',
|
||||
'@codemirror/legacy-modes/mode/shell', '@codemirror/legacy-modes/mode/xml'
|
||||
]
|
||||
},
|
||||
cacheDir: 'node_modules/.vite'
|
||||
include: ['vue', 'pinia', '@arco-design/web-vue', 'marked', 'highlight.js']
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user