From 4471441d6f3a626f3933b2715f698fc2964d7e9e Mon Sep 17 00:00:00 2001 From: landaiqing Date: Wed, 19 Nov 2025 20:54:58 +0800 Subject: [PATCH] :recycle: Refactor some code --- frontend/src/i18n/locales/en-US.ts | 2 +- frontend/src/i18n/locales/zh-CN.ts | 2 +- frontend/src/stores/editorStore.ts | 16 +- frontend/src/views/editor/Editor.vue | 18 +- .../views/editor/basic/wheelZoomExtension.ts | 60 ++-- frontend/src/views/editor/keymap/index.ts | 8 +- .../keymap/{keymapManager.ts => manager.ts} | 2 +- .../views/editor/manager/extensionManager.ts | 299 ----------------- .../src/views/editor/manager/extensions.ts | 307 +++++------------- frontend/src/views/editor/manager/index.ts | 10 +- frontend/src/views/editor/manager/manager.ts | 135 ++++++++ frontend/src/views/editor/manager/types.ts | 50 +-- .../views/settings/pages/ExtensionsPage.vue | 256 +++++++-------- 13 files changed, 410 insertions(+), 755 deletions(-) rename frontend/src/views/editor/keymap/{keymapManager.ts => manager.ts} (99%) delete mode 100644 frontend/src/views/editor/manager/extensionManager.ts create mode 100644 frontend/src/views/editor/manager/manager.ts diff --git a/frontend/src/i18n/locales/en-US.ts b/frontend/src/i18n/locales/en-US.ts index 28831ad..e340d4b 100644 --- a/frontend/src/i18n/locales/en-US.ts +++ b/frontend/src/i18n/locales/en-US.ts @@ -341,4 +341,4 @@ export default { memory: 'Memory', clickToClean: 'Click to clean memory' } -}; \ No newline at end of file +}; diff --git a/frontend/src/i18n/locales/zh-CN.ts b/frontend/src/i18n/locales/zh-CN.ts index 3af8323..5577374 100644 --- a/frontend/src/i18n/locales/zh-CN.ts +++ b/frontend/src/i18n/locales/zh-CN.ts @@ -344,4 +344,4 @@ export default { memory: '内存', clickToClean: '点击清理内存' } -}; \ No newline at end of file +}; diff --git a/frontend/src/stores/editorStore.ts b/frontend/src/stores/editorStore.ts index b3fd40b..a7b8b93 100644 --- a/frontend/src/stores/editorStore.ts +++ b/frontend/src/stores/editorStore.ts @@ -14,6 +14,7 @@ import {getTabExtensions, updateTabConfig} from '@/views/editor/basic/tabExtensi import {createFontExtensionFromBackend, updateFontConfig} from '@/views/editor/basic/fontExtension'; import {createStatsUpdateExtension} from '@/views/editor/basic/statsExtension'; import {createContentChangePlugin} from '@/views/editor/basic/contentChangeExtension'; +import {createWheelZoomExtension} from '@/views/editor/basic/wheelZoomExtension'; import {createDynamicKeymapExtension, updateKeymapExtension} from '@/views/editor/keymap'; import { createDynamicExtensions, @@ -242,6 +243,11 @@ export const useEditorStore = defineStore('editor', () => { fontWeight: configStore.config.editing.fontWeight }); + const wheelZoomExtension = createWheelZoomExtension( + () => configStore.increaseFontSize(), + () => configStore.decreaseFontSize() + ); + // 统计扩展 const statsExtension = createStatsUpdateExtension(updateDocumentStats); @@ -287,6 +293,7 @@ export const useEditorStore = defineStore('editor', () => { themeExtension, ...tabExtensions, fontExtension, + wheelZoomExtension, statsExtension, contentChangeExtension, codeBlockExtension, @@ -707,12 +714,15 @@ export const useEditorStore = defineStore('editor', () => { // 更新前端编辑器扩展 - 应用于所有实例 const manager = getExtensionManager(); if (manager) { - // 使用立即更新模式,跳过防抖 - manager.updateExtensionImmediate(id, enabled, config || {}); + // 直接更新前端扩展至所有视图 + manager.updateExtension(id, enabled, config); } // 重新加载扩展配置 await extensionStore.loadExtensions(); + if (manager) { + manager.initExtensions(extensionStore.extensions); + } await applyKeymapSettings(); }; @@ -781,4 +791,4 @@ export const useEditorStore = defineStore('editor', () => { editorView: currentEditor, }; -}); \ No newline at end of file +}); diff --git a/frontend/src/views/editor/Editor.vue b/frontend/src/views/editor/Editor.vue index d8a5c81..0df2849 100644 --- a/frontend/src/views/editor/Editor.vue +++ b/frontend/src/views/editor/Editor.vue @@ -3,7 +3,6 @@ import {computed, onBeforeUnmount, onMounted, ref} from 'vue'; import {useEditorStore} from '@/stores/editorStore'; import {useDocumentStore} from '@/stores/documentStore'; import {useConfigStore} from '@/stores/configStore'; -import {createWheelZoomHandler} from './basic/wheelZoomExtension'; import Toolbar from '@/components/toolbar/Toolbar.vue'; import {useWindowStore} from "@/stores/windowStore"; import LoadingScreen from '@/components/loading/LoadingScreen.vue'; @@ -19,12 +18,6 @@ const editorElement = ref(null); const enableLoadingAnimation = computed(() => configStore.config.general.enableLoadingAnimation); -// 创建滚轮缩放处理器 -const wheelHandler = createWheelZoomHandler( - configStore.increaseFontSize, - configStore.decreaseFontSize -); - onMounted(async () => { if (!editorElement.value) return; @@ -38,16 +31,9 @@ onMounted(async () => { editorStore.setEditorContainer(editorElement.value); await tabStore.initializeTab(); - - // 添加滚轮事件监听 - editorElement.value.addEventListener('wheel', wheelHandler, {passive: false}); }); onBeforeUnmount(() => { - // 移除滚轮事件监听 - if (editorElement.value) { - editorElement.value.removeEventListener('wheel', wheelHandler); - } editorStore.clearAllEditors(); }); @@ -88,7 +74,6 @@ onBeforeUnmount(() => { overflow: auto; } -// 加载动画过渡效果 .loading-fade-enter-active, .loading-fade-leave-active { transition: opacity 0.3s ease; @@ -98,4 +83,5 @@ onBeforeUnmount(() => { .loading-fade-leave-to { opacity: 0; } - \ No newline at end of file + + diff --git a/frontend/src/views/editor/basic/wheelZoomExtension.ts b/frontend/src/views/editor/basic/wheelZoomExtension.ts index 2e3765f..b87a88a 100644 --- a/frontend/src/views/editor/basic/wheelZoomExtension.ts +++ b/frontend/src/views/editor/basic/wheelZoomExtension.ts @@ -1,22 +1,40 @@ -// 处理滚轮缩放字体的事件处理函数 -export const createWheelZoomHandler = ( - increaseFontSize: () => void, - decreaseFontSize: () => void -) => { - return (event: WheelEvent) => { - // 检查是否按住了Ctrl键 - if (event.ctrlKey) { - // 阻止默认行为(防止页面缩放) - event.preventDefault(); - - // 根据滚轮方向增大或减小字体 - if (event.deltaY < 0) { - // 向上滚动,增大字体 - increaseFontSize(); - } else { - // 向下滚动,减小字体 - decreaseFontSize(); - } +import {EditorView} from '@codemirror/view'; +import type {Extension} from '@codemirror/state'; + +type FontAdjuster = () => Promise | void; + +const runAdjuster = (adjuster: FontAdjuster) => { + try { + const result = adjuster(); + if (result && typeof (result as Promise).then === 'function') { + (result as Promise).catch((error) => { + console.error('Failed to adjust font size:', error); + }); } - }; -}; \ No newline at end of file + } catch (error) { + console.error('Failed to adjust font size:', error); + } +}; + +export const createWheelZoomExtension = ( + increaseFontSize: FontAdjuster, + decreaseFontSize: FontAdjuster +): Extension => { + return EditorView.domEventHandlers({ + wheel(event) { + if (!event.ctrlKey) { + return false; + } + + event.preventDefault(); + + if (event.deltaY < 0) { + runAdjuster(increaseFontSize); + } else if (event.deltaY > 0) { + runAdjuster(decreaseFontSize); + } + + return true; + } + }); +}; diff --git a/frontend/src/views/editor/keymap/index.ts b/frontend/src/views/editor/keymap/index.ts index 836639c..cea0d88 100644 --- a/frontend/src/views/editor/keymap/index.ts +++ b/frontend/src/views/editor/keymap/index.ts @@ -1,7 +1,7 @@ import { Extension } from '@codemirror/state'; import { useKeybindingStore } from '@/stores/keybindingStore'; import { useExtensionStore } from '@/stores/extensionStore'; -import { KeymapManager } from './keymapManager'; +import { Manager } from './manager'; /** * 异步创建快捷键扩展 @@ -23,7 +23,7 @@ export const createDynamicKeymapExtension = async (): Promise => { // 获取启用的扩展ID列表 const enabledExtensionIds = extensionStore.enabledExtensions.map(ext => ext.id); - return KeymapManager.createKeymapExtension(keybindingStore.keyBindings, enabledExtensionIds); + return Manager.createKeymapExtension(keybindingStore.keyBindings, enabledExtensionIds); }; /** @@ -37,10 +37,10 @@ export const updateKeymapExtension = (view: any): void => { // 获取启用的扩展ID列表 const enabledExtensionIds = extensionStore.enabledExtensions.map(ext => ext.id); - KeymapManager.updateKeymap(view, keybindingStore.keyBindings, enabledExtensionIds); + Manager.updateKeymap(view, keybindingStore.keyBindings, enabledExtensionIds); }; // 导出相关模块 -export { KeymapManager } from './keymapManager'; +export { Manager } from './manager'; export { commands, getCommandHandler, getCommandDescription, isCommandRegistered, getRegisteredCommands } from './commands'; export type { KeyBinding, CommandHandler, CommandDefinition, KeymapResult } from './types'; \ No newline at end of file diff --git a/frontend/src/views/editor/keymap/keymapManager.ts b/frontend/src/views/editor/keymap/manager.ts similarity index 99% rename from frontend/src/views/editor/keymap/keymapManager.ts rename to frontend/src/views/editor/keymap/manager.ts index ec1f450..a57f677 100644 --- a/frontend/src/views/editor/keymap/keymapManager.ts +++ b/frontend/src/views/editor/keymap/manager.ts @@ -8,7 +8,7 @@ import {getCommandHandler, isCommandRegistered} from './commands'; * 快捷键管理器 * 负责将后端配置转换为CodeMirror快捷键扩展 */ -export class KeymapManager { +export class Manager { private static compartment = new Compartment(); /** diff --git a/frontend/src/views/editor/manager/extensionManager.ts b/frontend/src/views/editor/manager/extensionManager.ts deleted file mode 100644 index d135abb..0000000 --- a/frontend/src/views/editor/manager/extensionManager.ts +++ /dev/null @@ -1,299 +0,0 @@ -import {Compartment, Extension} from '@codemirror/state'; -import {EditorView} from '@codemirror/view'; -import {Extension as ExtensionConfig, ExtensionID} from '@/../bindings/voidraft/internal/models/models'; -import {ExtensionState, EditorViewInfo, ExtensionFactory} from './types' -import {createDebounce} from '@/common/utils/debounce'; - - - -/** - * 扩展管理器 - * 负责管理所有动态扩展的注册、启用、禁用和配置更新 - * 采用统一配置,多视图同步的设计模式 - */ -export class ExtensionManager { - // 统一的扩展状态存储 - private extensionStates = new Map(); - - // 编辑器视图管理 - private viewsMap = new Map(); - private activeViewId: number | null = null; - - // 注册的扩展工厂 - private extensionFactories = new Map(); - - // 防抖处理 - private debouncedUpdateFunctions = new Map void; - cancel: () => void; - flush: () => void; - }>(); - - /** - * 注册扩展工厂 - * @param id 扩展ID - * @param factory 扩展工厂 - */ - registerExtension(id: ExtensionID, factory: ExtensionFactory): void { - this.extensionFactories.set(id, factory); - - // 创建初始状态 - if (!this.extensionStates.has(id)) { - const compartment = new Compartment(); - const defaultConfig = factory.getDefaultConfig(); - - this.extensionStates.set(id, { - id, - factory, - config: defaultConfig, - enabled: false, - compartment, - extension: [] // 默认为空扩展(禁用状态) - }); - } - - // 为每个扩展创建防抖函数 - if (!this.debouncedUpdateFunctions.has(id)) { - const { debouncedFn, cancel, flush } = createDebounce( - (enabled: boolean, config: any) => { - this.updateExtensionImmediate(id, enabled, config); - }, - { delay: 300 } - ); - - this.debouncedUpdateFunctions.set(id, { - debouncedFn, - cancel, - flush - }); - } - } - - /** - * 获取所有注册的扩展ID列表 - */ - getRegisteredExtensions(): ExtensionID[] { - return Array.from(this.extensionFactories.keys()); - } - - /** - * 检查扩展是否已注册 - * @param id 扩展ID - */ - isExtensionRegistered(id: ExtensionID): boolean { - return this.extensionFactories.has(id); - } - - /** - * 从后端配置初始化扩展状态 - * @param extensionConfigs 后端扩展配置列表 - */ - initializeExtensionsFromConfig(extensionConfigs: ExtensionConfig[]): void { - for (const config of extensionConfigs) { - const factory = this.extensionFactories.get(config.id); - if (!factory) continue; - - // 验证配置 - if (factory.validateConfig && !factory.validateConfig(config.config)) { - continue; - } - - try { - // 创建扩展实例 - const extension = config.enabled ? factory.create(config.config) : []; - - // 如果状态已存在则更新,否则创建新状态 - if (this.extensionStates.has(config.id)) { - const state = this.extensionStates.get(config.id)!; - state.config = config.config; - state.enabled = config.enabled; - state.extension = extension; - } else { - const compartment = new Compartment(); - this.extensionStates.set(config.id, { - id: config.id, - factory, - config: config.config, - enabled: config.enabled, - compartment, - extension - }); - } - } catch (error) { - console.error(`Failed to initialize extension ${config.id}:`, error); - } - } - } - - /** - * 获取初始扩展配置数组(用于创建编辑器) - * @returns CodeMirror扩展数组 - */ - getInitialExtensions(): Extension[] { - const extensions: Extension[] = []; - - // 为每个注册的扩展添加compartment - for (const state of this.extensionStates.values()) { - extensions.push(state.compartment.of(state.extension)); - } - - return extensions; - } - - /** - * 设置编辑器视图 - * @param view 编辑器视图实例 - * @param documentId 文档ID - */ - setView(view: EditorView, documentId: number): void { - // 保存视图信息 - this.viewsMap.set(documentId, { - view, - documentId, - registered: true - }); - - // 设置当前活动视图 - this.activeViewId = documentId; - } - - /** - * 获取当前活动视图 - */ - private getActiveView(): EditorView | null { - if (this.activeViewId === null) return null; - const viewInfo = this.viewsMap.get(this.activeViewId); - return viewInfo ? viewInfo.view : null; - } - - /** - * 更新单个扩展配置并应用到所有视图(带防抖功能) - * @param id 扩展ID - * @param enabled 是否启用 - * @param config 扩展配置 - */ - updateExtension(id: ExtensionID, enabled: boolean, config: any = {}): void { - const debouncedUpdate = this.debouncedUpdateFunctions.get(id); - if (debouncedUpdate) { - debouncedUpdate.debouncedFn(enabled, config); - } else { - // 如果没有防抖函数,直接执行 - this.updateExtensionImmediate(id, enabled, config); - } - } - - /** - * 立即更新扩展(无防抖) - * @param id 扩展ID - * @param enabled 是否启用 - * @param config 扩展配置 - */ - updateExtensionImmediate(id: ExtensionID, enabled: boolean, config: any = {}): void { - // 获取扩展状态 - const state = this.extensionStates.get(id); - if (!state) return; - - // 获取工厂 - const factory = state.factory; - - // 验证配置 - if (factory.validateConfig && !factory.validateConfig(config)) { - return; - } - - try { - // 创建新的扩展实例 - const extension = enabled ? factory.create(config) : []; - - // 更新内部状态 - state.config = config; - state.enabled = enabled; - state.extension = extension; - - // 应用到所有视图 - this.applyExtensionToAllViews(id); - } catch (error) { - console.error(`Failed to update extension ${id}:`, error); - } - } - - /** - * 将指定扩展的当前状态应用到所有视图 - * @param id 扩展ID - */ - private applyExtensionToAllViews(id: ExtensionID): void { - const state = this.extensionStates.get(id); - if (!state) return; - - // 遍历所有视图并应用更改 - for (const viewInfo of this.viewsMap.values()) { - try { - if (!viewInfo.registered) continue; - - viewInfo.view.dispatch({ - effects: state.compartment.reconfigure(state.extension) - }); - } catch (error) { - console.error(`Failed to apply extension ${id} to document ${viewInfo.documentId}:`, error); - } - } - } - - - /** - * 获取扩展当前状态 - * @param id 扩展ID - */ - getExtensionState(id: ExtensionID): { - enabled: boolean - config: any - } | null { - const state = this.extensionStates.get(id); - if (!state) return null; - - return { - enabled: state.enabled, - config: state.config - }; - } - - /** - * 重置扩展到默认配置 - * @param id 扩展ID - */ - resetExtensionToDefault(id: ExtensionID): void { - const state = this.extensionStates.get(id); - if (!state) return; - - const defaultConfig = state.factory.getDefaultConfig(); - this.updateExtension(id, true, defaultConfig); - } - - /** - * 从管理器中移除视图 - * @param documentId 文档ID - */ - removeView(documentId: number): void { - if (this.activeViewId === documentId) { - this.activeViewId = null; - } - - this.viewsMap.delete(documentId); - } - - /** - * 销毁管理器 - */ - destroy(): void { - // 清除所有防抖函数 - for (const { cancel } of this.debouncedUpdateFunctions.values()) { - cancel(); - } - this.debouncedUpdateFunctions.clear(); - - this.viewsMap.clear(); - this.activeViewId = null; - this.extensionFactories.clear(); - this.extensionStates.clear(); - } -} \ No newline at end of file diff --git a/frontend/src/views/editor/manager/extensions.ts b/frontend/src/views/editor/manager/extensions.ts index ebfebd3..02b27b7 100644 --- a/frontend/src/views/editor/manager/extensions.ts +++ b/frontend/src/views/editor/manager/extensions.ts @@ -1,301 +1,152 @@ -import {ExtensionManager} from './extensionManager'; +import {Manager} from './manager'; import {ExtensionID} from '@/../bindings/voidraft/internal/models/models'; import i18n from '@/i18n'; -import {ExtensionFactory} from './types' +import {ExtensionDefinition} from './types'; -// 导入现有扩展的创建函数 import rainbowBracketsExtension from '../extensions/rainbowBracket/rainbowBracketsExtension'; import {createTextHighlighter} from '../extensions/textHighlight/textHighlightExtension'; - import {color} from '../extensions/colorSelector'; import {hyperLink} from '../extensions/hyperlink'; import {minimap} from '../extensions/minimap'; import {vscodeSearch} from '../extensions/vscodeSearch'; import {createCheckboxExtension} from '../extensions/checkbox'; import {createTranslatorExtension} from '../extensions/translator'; - import {foldingOnIndent} from '../extensions/fold/foldExtension'; -/** - * 彩虹括号扩展工厂 - */ -export const rainbowBracketsFactory: ExtensionFactory = { - create(_config: any) { - return rainbowBracketsExtension(); - }, - getDefaultConfig() { - return {}; - }, - validateConfig(config: any) { - return typeof config === 'object'; - } +type ExtensionEntry = { + definition: ExtensionDefinition + displayNameKey: string + descriptionKey: string }; -/** - * 文本高亮扩展工厂 - */ -export const textHighlightFactory: ExtensionFactory = { - create(config: any) { - return createTextHighlighter({ - backgroundColor: config.backgroundColor || '#FFD700', - opacity: config.opacity || 0.3 - }); - }, - getDefaultConfig() { - return { - backgroundColor: '#FFD700', // 金黄色 - opacity: 0.3 // 透明度 - }; - }, - validateConfig(config: any) { - return typeof config === 'object' && - (!config.backgroundColor || typeof config.backgroundColor === 'string') && - (!config.opacity || (typeof config.opacity === 'number' && config.opacity >= 0 && config.opacity <= 1)); - } -}; +type RegisteredExtensionID = Exclude; -/** - * 小地图扩展工厂 - */ -export const minimapFactory: ExtensionFactory = { - create(config: any) { - const options = { - displayText: config.displayText || 'characters', - showOverlay: config.showOverlay || 'always', - autohide: config.autohide || false - }; - return minimap(options); - }, - getDefaultConfig() { - return { - displayText: 'characters', - showOverlay: 'always', - autohide: false - }; - }, - validateConfig(config: any) { - return typeof config === 'object' && - (!config.displayText || typeof config.displayText === 'string') && - (!config.showOverlay || typeof config.showOverlay === 'string') && - (!config.autohide || typeof config.autohide === 'boolean'); - } -}; +const defineExtension = (create: (config: any) => any, defaultConfig: Record = {}): ExtensionDefinition => ({ + create, + defaultConfig +}); -/** - * 超链接扩展工厂 - */ -export const hyperlinkFactory: ExtensionFactory = { - create(_config: any) { - return hyperLink; - }, - getDefaultConfig() { - return {}; - }, - validateConfig(config: any) { - return typeof config === 'object'; - } -}; - -/** - * 颜色选择器扩展工厂 - */ -export const colorSelectorFactory: ExtensionFactory = { - create(_config: any) { - return color; - }, - getDefaultConfig() { - return {}; - }, - validateConfig(config: any) { - return typeof config === 'object'; - } -}; - -/** - * 搜索扩展工厂 - */ -export const searchFactory: ExtensionFactory = { - create(_config: any) { - return vscodeSearch; - }, - getDefaultConfig() { - return {}; - }, - validateConfig(config: any) { - return typeof config === 'object'; - } -}; - -export const foldFactory: ExtensionFactory = { - create(_config: any) { - return foldingOnIndent; - }, - getDefaultConfig(): any { - return {}; - }, - validateConfig(config: any): boolean { - return typeof config === 'object'; - } -}; - -/** - * 选择框扩展工厂 - */ -export const checkboxFactory: ExtensionFactory = { - create(_config: any) { - return createCheckboxExtension(); - }, - getDefaultConfig() { - return {}; - }, - validateConfig(config: any) { - return typeof config === 'object'; - } -}; - -/** - * 翻译扩展工厂 - */ -export const translatorFactory: ExtensionFactory = { - create(config: any) { - return createTranslatorExtension({ - minSelectionLength: config.minSelectionLength || 2, - maxTranslationLength: config.maxTranslationLength || 5000, - }); - }, - getDefaultConfig() { - return { - minSelectionLength: 2, - maxTranslationLength: 5000, - }; - }, - validateConfig(config: any) { - return typeof config === 'object'; - } -}; - -/** - * 所有扩展的统一配置 - * 排除$zero值以避免TypeScript类型错误 - */ -const EXTENSION_CONFIGS = { - - // 编辑增强扩展 +const EXTENSION_REGISTRY: Record = { [ExtensionID.ExtensionRainbowBrackets]: { - factory: rainbowBracketsFactory, + definition: defineExtension(() => rainbowBracketsExtension()), displayNameKey: 'extensions.rainbowBrackets.name', descriptionKey: 'extensions.rainbowBrackets.description' }, [ExtensionID.ExtensionHyperlink]: { - factory: hyperlinkFactory, + definition: defineExtension(() => hyperLink), displayNameKey: 'extensions.hyperlink.name', descriptionKey: 'extensions.hyperlink.description' }, [ExtensionID.ExtensionColorSelector]: { - factory: colorSelectorFactory, + definition: defineExtension(() => color), displayNameKey: 'extensions.colorSelector.name', descriptionKey: 'extensions.colorSelector.description' }, [ExtensionID.ExtensionTranslator]: { - factory: translatorFactory, + definition: defineExtension((config: any) => createTranslatorExtension({ + minSelectionLength: config?.minSelectionLength ?? 2, + maxTranslationLength: config?.maxTranslationLength ?? 5000 + }), { + minSelectionLength: 2, + maxTranslationLength: 5000 + }), displayNameKey: 'extensions.translator.name', descriptionKey: 'extensions.translator.description' }, - - // UI增强扩展 [ExtensionID.ExtensionMinimap]: { - factory: minimapFactory, + definition: defineExtension((config: any) => minimap({ + displayText: config?.displayText ?? 'characters', + showOverlay: config?.showOverlay ?? 'always', + autohide: config?.autohide ?? false + }), { + displayText: 'characters', + showOverlay: 'always', + autohide: false + }), displayNameKey: 'extensions.minimap.name', descriptionKey: 'extensions.minimap.description' }, - - // 工具扩展 [ExtensionID.ExtensionSearch]: { - factory: searchFactory, + definition: defineExtension(() => vscodeSearch), displayNameKey: 'extensions.search.name', descriptionKey: 'extensions.search.description' }, - [ExtensionID.ExtensionFold]: { - factory: foldFactory, + definition: defineExtension(() => foldingOnIndent), displayNameKey: 'extensions.fold.name', descriptionKey: 'extensions.fold.description' }, [ExtensionID.ExtensionTextHighlight]: { - factory: textHighlightFactory, + definition: defineExtension((config: any) => createTextHighlighter({ + backgroundColor: config?.backgroundColor ?? '#FFD700', + opacity: config?.opacity ?? 0.3 + }), { + backgroundColor: '#FFD700', + opacity: 0.3 + }), displayNameKey: 'extensions.textHighlight.name', descriptionKey: 'extensions.textHighlight.description' }, [ExtensionID.ExtensionCheckbox]: { - factory: checkboxFactory, + definition: defineExtension(() => createCheckboxExtension()), displayNameKey: 'extensions.checkbox.name', descriptionKey: 'extensions.checkbox.description' } +} as const; + +const isRegisteredExtension = (id: ExtensionID): id is RegisteredExtensionID => + Object.prototype.hasOwnProperty.call(EXTENSION_REGISTRY, id); + +const getRegistryEntry = (id: ExtensionID): ExtensionEntry | undefined => { + if (!isRegisteredExtension(id)) { + return undefined; + } + return EXTENSION_REGISTRY[id]; }; -/** - * 注册所有扩展工厂到管理器 - * @param manager 扩展管理器实例 - */ -export function registerAllExtensions(manager: ExtensionManager): void { - Object.entries(EXTENSION_CONFIGS).forEach(([id, config]) => { - manager.registerExtension(id as ExtensionID, config.factory); +export function registerAllExtensions(manager: Manager): void { + (Object.entries(EXTENSION_REGISTRY) as [RegisteredExtensionID, ExtensionEntry][]).forEach(([id, entry]) => { + manager.registerExtension(id, entry.definition); }); } -/** - * 获取扩展工厂的显示名称 - * @param id 扩展ID - * @returns 显示名称 - */ export function getExtensionDisplayName(id: ExtensionID): string { - const config = EXTENSION_CONFIGS[id as ExtensionID]; - return config?.displayNameKey ? i18n.global.t(config.displayNameKey) : id; + const entry = getRegistryEntry(id); + return entry?.displayNameKey ? i18n.global.t(entry.displayNameKey) : id; } -/** - * 获取扩展工厂的描述 - * @param id 扩展ID - * @returns 描述 - */ export function getExtensionDescription(id: ExtensionID): string { - const config = EXTENSION_CONFIGS[id as ExtensionID]; - return config?.descriptionKey ? i18n.global.t(config.descriptionKey) : ''; + const entry = getRegistryEntry(id); + return entry?.descriptionKey ? i18n.global.t(entry.descriptionKey) : ''; } -/** - * 获取扩展工厂实例 - * @param id 扩展ID - * @returns 扩展工厂实例 - */ -export function getExtensionFactory(id: ExtensionID): ExtensionFactory | undefined { - return EXTENSION_CONFIGS[id as ExtensionID]?.factory; +function getExtensionDefinition(id: ExtensionID): ExtensionDefinition | undefined { + return getRegistryEntry(id)?.definition; } -/** - * 获取扩展的默认配置 - * @param id 扩展ID - * @returns 默认配置对象 - */ export function getExtensionDefaultConfig(id: ExtensionID): any { - const factory = getExtensionFactory(id); - return factory?.getDefaultConfig() || {}; + const definition = getExtensionDefinition(id); + if (!definition) return {}; + return cloneConfig(definition.defaultConfig); } -/** - * 检查扩展是否有配置项 - * @param id 扩展ID - * @returns 是否有配置项 - */ export function hasExtensionConfig(id: ExtensionID): boolean { - const defaultConfig = getExtensionDefaultConfig(id); - return Object.keys(defaultConfig).length > 0; + return Object.keys(getExtensionDefaultConfig(id)).length > 0; } -/** - * 获取所有可用扩展的ID列表 - * @returns 扩展ID数组 - */ export function getAllExtensionIds(): ExtensionID[] { - return Object.keys(EXTENSION_CONFIGS) as ExtensionID[]; -} \ No newline at end of file + return Object.keys(EXTENSION_REGISTRY) as RegisteredExtensionID[]; +} + +const cloneConfig = (config: any) => { + if (Array.isArray(config)) { + return config.map(cloneConfig); + } + if (config && typeof config === 'object') { + return Object.keys(config).reduce((acc, key) => { + acc[key] = cloneConfig(config[key]); + return acc; + }, {} as Record); + } + return config; +}; diff --git a/frontend/src/views/editor/manager/index.ts b/frontend/src/views/editor/manager/index.ts index 7f2bb1e..de845f5 100644 --- a/frontend/src/views/editor/manager/index.ts +++ b/frontend/src/views/editor/manager/index.ts @@ -1,13 +1,13 @@ import {Extension} from '@codemirror/state'; import {EditorView} from '@codemirror/view'; import {useExtensionStore} from '@/stores/extensionStore'; -import {ExtensionManager} from './extensionManager'; +import {Manager} from './manager'; import {registerAllExtensions} from './extensions'; /** * 全局扩展管理器实例 */ -const extensionManager = new ExtensionManager(); +const extensionManager = new Manager(); /** * 异步创建动态扩展 @@ -26,7 +26,7 @@ export const createDynamicExtensions = async (_documentId?: number): Promise { +export const getExtensionManager = (): Manager => { return extensionManager; }; @@ -58,5 +58,5 @@ export const removeExtensionManagerView = (documentId: number): void => { }; // 导出相关模块 -export {ExtensionManager} from './extensionManager'; +export {Manager} from './manager'; export {registerAllExtensions, getExtensionDisplayName, getExtensionDescription} from './extensions'; \ No newline at end of file diff --git a/frontend/src/views/editor/manager/manager.ts b/frontend/src/views/editor/manager/manager.ts new file mode 100644 index 0000000..a9628cf --- /dev/null +++ b/frontend/src/views/editor/manager/manager.ts @@ -0,0 +1,135 @@ +import {Compartment, Extension} from '@codemirror/state'; +import {EditorView} from '@codemirror/view'; +import {Extension as ExtensionConfig, ExtensionID} from '@/../bindings/voidraft/internal/models/models'; +import {ExtensionDefinition, ExtensionState} from './types'; + +/** + * 扩展管理器 + * 负责注册、初始化与同步所有动态扩展 + */ +export class Manager { + private extensionStates = new Map(); + private views = new Map(); + + registerExtension(id: ExtensionID, definition: ExtensionDefinition): void { + const existingState = this.extensionStates.get(id); + if (existingState) { + existingState.definition = definition; + if (existingState.config === undefined) { + existingState.config = this.cloneConfig(definition.defaultConfig ?? {}); + } + } else { + const compartment = new Compartment(); + const defaultConfig = this.cloneConfig(definition.defaultConfig ?? {}); + this.extensionStates.set(id, { + id, + definition, + config: defaultConfig, + enabled: false, + compartment, + extension: [] + }); + } + } + + initExtensions(extensionConfigs: ExtensionConfig[]): void { + for (const config of extensionConfigs) { + const state = this.extensionStates.get(config.id); + if (!state) continue; + const resolvedConfig = this.cloneConfig(config.config ?? state.definition.defaultConfig ?? {}); + this.commitExtensionState(state, config.enabled, resolvedConfig); + } + } + + getInitialExtensions(): Extension[] { + const extensions: Extension[] = []; + for (const state of this.extensionStates.values()) { + extensions.push(state.compartment.of(state.extension)); + } + return extensions; + } + + setView(view: EditorView, documentId: number): void { + this.views.set(documentId, view); + this.applyAllExtensionsToView(view); + } + + updateExtension(id: ExtensionID, enabled: boolean, config?: any): void { + const state = this.extensionStates.get(id); + if (!state) return; + + const resolvedConfig = this.resolveConfig(state, config); + this.commitExtensionState(state, enabled, resolvedConfig); + } + + removeView(documentId: number): void { + this.views.delete(documentId); + } + + destroy(): void { + this.views.clear(); + this.extensionStates.clear(); + } + + private resolveConfig(state: ExtensionState, config?: any): any { + if (config !== undefined) { + return this.cloneConfig(config); + } + if (state.config !== undefined) { + return this.cloneConfig(state.config); + } + return this.cloneConfig(state.definition.defaultConfig ?? {}); + } + + private commitExtensionState(state: ExtensionState, enabled: boolean, config: any): void { + try { + const runtimeExtension = enabled ? state.definition.create(config) : []; + state.enabled = enabled; + state.config = config; + state.extension = runtimeExtension; + this.applyExtensionToAllViews(state.id); + } catch (error) { + console.error(`Failed to update extension ${state.id}:`, error); + } + } + + private applyExtensionToAllViews(id: ExtensionID): void { + const state = this.extensionStates.get(id); + if (!state) return; + + for (const [documentId, view] of this.views.entries()) { + try { + view.dispatch({effects: state.compartment.reconfigure(state.extension)}); + } catch (error) { + console.error(`Failed to apply extension ${id} to document ${documentId}:`, error); + } + } + } + + private applyAllExtensionsToView(view: EditorView): void { + const effects: any[] = []; + for (const state of this.extensionStates.values()) { + effects.push(state.compartment.reconfigure(state.extension)); + } + if (effects.length === 0) return; + + try { + view.dispatch({effects}); + } catch (error) { + console.error('Failed to register extensions on view:', error); + } + } + + private cloneConfig(config: T): T { + if (Array.isArray(config)) { + return config.map(item => this.cloneConfig(item)) as unknown as T; + } + if (config && typeof config === 'object') { + return Object.keys(config as Record).reduce((acc, key) => { + (acc as any)[key] = this.cloneConfig((config as Record)[key]); + return acc; + }, {} as Record) as T; + } + return config; + } +} diff --git a/frontend/src/views/editor/manager/types.ts b/frontend/src/views/editor/manager/types.ts index 7b70b94..431194c 100644 --- a/frontend/src/views/editor/manager/types.ts +++ b/frontend/src/views/editor/manager/types.ts @@ -1,49 +1 @@ -import {Compartment, Extension} from '@codemirror/state'; -import {EditorView} from '@codemirror/view'; -import {ExtensionID} from '@/../bindings/voidraft/internal/models/models'; -/** - * 扩展工厂接口 - * 每个扩展需要实现此接口来创建和配置扩展 - */ -export interface ExtensionFactory { - /** - * 创建扩展实例 - * @param config 扩展配置 - * @returns CodeMirror扩展 - */ - create(config: any): Extension - - /** - * 获取默认配置 - * @returns 默认配置对象 - */ - getDefaultConfig(): any - - /** - * 验证配置 - * @param config 配置对象 - * @returns 是否有效 - */ - validateConfig?(config: any): boolean -} - -/** - * 扩展状态 - */ -export interface ExtensionState { - id: ExtensionID - factory: ExtensionFactory - config: any - enabled: boolean - compartment: Compartment - extension: Extension -} - -/** - * 视图信息 - */ -export interface EditorViewInfo { - view: EditorView - documentId: number - registered: boolean -} +import {Compartment, Extension} from '@codemirror/state'; import {ExtensionID} from '@/../bindings/voidraft/internal/models/models'; /** * 扩展定义 * 标准化 create 方法和默认配置 */ export interface ExtensionDefinition { create(config: any): Extension defaultConfig: Record } /** * 扩展运行时状态 */ export interface ExtensionState { id: ExtensionID definition: ExtensionDefinition config: any enabled: boolean compartment: Compartment extension: Extension } \ No newline at end of file diff --git a/frontend/src/views/settings/pages/ExtensionsPage.vue b/frontend/src/views/settings/pages/ExtensionsPage.vue index e86e626..2eebb6f 100644 --- a/frontend/src/views/settings/pages/ExtensionsPage.vue +++ b/frontend/src/views/settings/pages/ExtensionsPage.vue @@ -66,10 +66,12 @@ const updateExtensionConfig = async (extensionId: ExtensionID, configKey: string if (!extension) return; // 更新配置 - const updatedConfig = {...extension.config, [configKey]: value}; - - console.log(`[ExtensionsPage] 更新扩展 ${extensionId} 配置, ${configKey}=${value}`); - + const updatedConfig = {...extension.config}; + if (value === undefined) { + delete updatedConfig[configKey]; + } else { + updatedConfig[configKey] = value; + } // 使用editorStore的updateExtension方法更新,确保应用到所有编辑器实例 await editorStore.updateExtension(extensionId, extension.enabled, updatedConfig); @@ -81,7 +83,7 @@ const updateExtensionConfig = async (extensionId: ExtensionID, configKey: string // 重置扩展到默认配置 const resetExtension = async (extensionId: ExtensionID) => { try { - // 重置到默认配置(后端) + // 重置到默认配置 await ExtensionService.ResetExtensionToDefault(extensionId); // 重新加载扩展状态以获取最新配置 @@ -92,63 +94,65 @@ const resetExtension = async (extensionId: ExtensionID) => { if (extension) { // 通过editorStore更新,确保所有视图都能同步 await editorStore.updateExtension(extensionId, extension.enabled, extension.config); - console.log(`[ExtensionsPage] 重置扩展 ${extensionId} 配置,同步应用到所有编辑器实例`); } } catch (error) { console.error('Failed to reset extension:', error); } }; -// 配置项类型定义 -type ConfigItemType = 'toggle' | 'number' | 'text' | 'select' - -interface SelectOption { - value: any - label: string -} - -interface ConfigItemMeta { - type: ConfigItemType - options?: SelectOption[] -} - -// 只保留 select 类型的配置项元数据 -const extensionConfigMeta: Partial>> = { - [ExtensionID.ExtensionMinimap]: { - displayText: { - type: 'select', - options: [ - {value: 'characters', label: 'Characters'}, - {value: 'blocks', label: 'Blocks'} - ] - }, - showOverlay: { - type: 'select', - options: [ - {value: 'always', label: 'Always'}, - {value: 'mouse-over', label: 'Mouse Over'} - ] - } +const getConfigValue = ( + config: Record | undefined, + configKey: string, + defaultValue: any +) => { + if (config && Object.prototype.hasOwnProperty.call(config, configKey)) { + return config[configKey]; } + return defaultValue; + }; -// 获取配置项类型 -const getConfigItemType = (extensionId: ExtensionID, configKey: string, defaultValue: any): string => { - const meta = extensionConfigMeta[extensionId]?.[configKey]; - if (meta?.type) { - return meta.type; + +const formatConfigValue = (value: any): string => { + if (value === undefined) return ''; + try { + const serialized = JSON.stringify(value); + return serialized ?? ''; + } catch (error) { + console.warn('Failed to stringify config value', error); + return ''; } - // 根据默认值类型自动推断 - if (typeof defaultValue === 'boolean') return 'toggle'; - if (typeof defaultValue === 'number') return 'number'; - return 'text'; }; -// 获取选择框的选项列表 -const getSelectOptions = (extensionId: ExtensionID, configKey: string): SelectOption[] => { - return extensionConfigMeta[extensionId]?.[configKey]?.options || []; + +const handleConfigInput = async ( + extensionId: ExtensionID, + configKey: string, + defaultValue: any, + event: Event +) => { + const target = event.target as HTMLInputElement | null; + if (!target) return; + const rawValue = target.value; + const trimmedValue = rawValue.trim(); + if (!trimmedValue.length) { + await updateExtensionConfig(extensionId, configKey, undefined); + return; + } + + try { + const parsedValue = JSON.parse(trimmedValue); + await updateExtensionConfig(extensionId, configKey, parsedValue); + } catch (_error) { + const extension = extensionStore.extensions.find(ext => ext.id === extensionId); + const fallbackValue = getConfigValue(extension?.config, configKey, defaultValue); + target.value = formatConfigValue(fallbackValue); + + } + }; +