♻️ Refactor some code

This commit is contained in:
2025-11-19 20:54:58 +08:00
parent 991a89147e
commit 4471441d6f
13 changed files with 410 additions and 755 deletions

View File

@@ -14,6 +14,7 @@ import {getTabExtensions, updateTabConfig} from '@/views/editor/basic/tabExtensi
import {createFontExtensionFromBackend, updateFontConfig} from '@/views/editor/basic/fontExtension'; import {createFontExtensionFromBackend, updateFontConfig} from '@/views/editor/basic/fontExtension';
import {createStatsUpdateExtension} from '@/views/editor/basic/statsExtension'; import {createStatsUpdateExtension} from '@/views/editor/basic/statsExtension';
import {createContentChangePlugin} from '@/views/editor/basic/contentChangeExtension'; import {createContentChangePlugin} from '@/views/editor/basic/contentChangeExtension';
import {createWheelZoomExtension} from '@/views/editor/basic/wheelZoomExtension';
import {createDynamicKeymapExtension, updateKeymapExtension} from '@/views/editor/keymap'; import {createDynamicKeymapExtension, updateKeymapExtension} from '@/views/editor/keymap';
import { import {
createDynamicExtensions, createDynamicExtensions,
@@ -242,6 +243,11 @@ export const useEditorStore = defineStore('editor', () => {
fontWeight: configStore.config.editing.fontWeight fontWeight: configStore.config.editing.fontWeight
}); });
const wheelZoomExtension = createWheelZoomExtension(
() => configStore.increaseFontSize(),
() => configStore.decreaseFontSize()
);
// 统计扩展 // 统计扩展
const statsExtension = createStatsUpdateExtension(updateDocumentStats); const statsExtension = createStatsUpdateExtension(updateDocumentStats);
@@ -287,6 +293,7 @@ export const useEditorStore = defineStore('editor', () => {
themeExtension, themeExtension,
...tabExtensions, ...tabExtensions,
fontExtension, fontExtension,
wheelZoomExtension,
statsExtension, statsExtension,
contentChangeExtension, contentChangeExtension,
codeBlockExtension, codeBlockExtension,
@@ -707,12 +714,15 @@ export const useEditorStore = defineStore('editor', () => {
// 更新前端编辑器扩展 - 应用于所有实例 // 更新前端编辑器扩展 - 应用于所有实例
const manager = getExtensionManager(); const manager = getExtensionManager();
if (manager) { if (manager) {
// 使用立即更新模式,跳过防抖 // 直接更新前端扩展至所有视图
manager.updateExtensionImmediate(id, enabled, config || {}); manager.updateExtension(id, enabled, config);
} }
// 重新加载扩展配置 // 重新加载扩展配置
await extensionStore.loadExtensions(); await extensionStore.loadExtensions();
if (manager) {
manager.initExtensions(extensionStore.extensions);
}
await applyKeymapSettings(); await applyKeymapSettings();
}; };

View File

@@ -3,7 +3,6 @@ import {computed, onBeforeUnmount, onMounted, ref} from 'vue';
import {useEditorStore} from '@/stores/editorStore'; import {useEditorStore} from '@/stores/editorStore';
import {useDocumentStore} from '@/stores/documentStore'; import {useDocumentStore} from '@/stores/documentStore';
import {useConfigStore} from '@/stores/configStore'; import {useConfigStore} from '@/stores/configStore';
import {createWheelZoomHandler} from './basic/wheelZoomExtension';
import Toolbar from '@/components/toolbar/Toolbar.vue'; import Toolbar from '@/components/toolbar/Toolbar.vue';
import {useWindowStore} from "@/stores/windowStore"; import {useWindowStore} from "@/stores/windowStore";
import LoadingScreen from '@/components/loading/LoadingScreen.vue'; import LoadingScreen from '@/components/loading/LoadingScreen.vue';
@@ -19,12 +18,6 @@ const editorElement = ref<HTMLElement | null>(null);
const enableLoadingAnimation = computed(() => configStore.config.general.enableLoadingAnimation); const enableLoadingAnimation = computed(() => configStore.config.general.enableLoadingAnimation);
// 创建滚轮缩放处理器
const wheelHandler = createWheelZoomHandler(
configStore.increaseFontSize,
configStore.decreaseFontSize
);
onMounted(async () => { onMounted(async () => {
if (!editorElement.value) return; if (!editorElement.value) return;
@@ -38,16 +31,9 @@ onMounted(async () => {
editorStore.setEditorContainer(editorElement.value); editorStore.setEditorContainer(editorElement.value);
await tabStore.initializeTab(); await tabStore.initializeTab();
// 添加滚轮事件监听
editorElement.value.addEventListener('wheel', wheelHandler, {passive: false});
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
// 移除滚轮事件监听
if (editorElement.value) {
editorElement.value.removeEventListener('wheel', wheelHandler);
}
editorStore.clearAllEditors(); editorStore.clearAllEditors();
}); });
@@ -88,7 +74,6 @@ onBeforeUnmount(() => {
overflow: auto; overflow: auto;
} }
// 加载动画过渡效果
.loading-fade-enter-active, .loading-fade-enter-active,
.loading-fade-leave-active { .loading-fade-leave-active {
transition: opacity 0.3s ease; transition: opacity 0.3s ease;
@@ -99,3 +84,4 @@ onBeforeUnmount(() => {
opacity: 0; opacity: 0;
} }
</style> </style>

View File

@@ -1,22 +1,40 @@
// 处理滚轮缩放字体的事件处理函数 import {EditorView} from '@codemirror/view';
export const createWheelZoomHandler = ( import type {Extension} from '@codemirror/state';
increaseFontSize: () => void,
decreaseFontSize: () => void type FontAdjuster = () => Promise<void> | void;
) => {
return (event: WheelEvent) => { const runAdjuster = (adjuster: FontAdjuster) => {
// 检查是否按住了Ctrl键 try {
if (event.ctrlKey) { const result = adjuster();
// 阻止默认行为(防止页面缩放) if (result && typeof (result as Promise<void>).then === 'function') {
(result as Promise<void>).catch((error) => {
console.error('Failed to adjust font size:', error);
});
}
} 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(); event.preventDefault();
// 根据滚轮方向增大或减小字体
if (event.deltaY < 0) { if (event.deltaY < 0) {
// 向上滚动,增大字体 runAdjuster(increaseFontSize);
increaseFontSize(); } else if (event.deltaY > 0) {
} else { runAdjuster(decreaseFontSize);
// 向下滚动,减小字体
decreaseFontSize();
} }
return true;
} }
}; });
}; };

View File

@@ -1,7 +1,7 @@
import { Extension } from '@codemirror/state'; import { Extension } from '@codemirror/state';
import { useKeybindingStore } from '@/stores/keybindingStore'; import { useKeybindingStore } from '@/stores/keybindingStore';
import { useExtensionStore } from '@/stores/extensionStore'; import { useExtensionStore } from '@/stores/extensionStore';
import { KeymapManager } from './keymapManager'; import { Manager } from './manager';
/** /**
* 异步创建快捷键扩展 * 异步创建快捷键扩展
@@ -23,7 +23,7 @@ export const createDynamicKeymapExtension = async (): Promise<Extension> => {
// 获取启用的扩展ID列表 // 获取启用的扩展ID列表
const enabledExtensionIds = extensionStore.enabledExtensions.map(ext => ext.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列表 // 获取启用的扩展ID列表
const enabledExtensionIds = extensionStore.enabledExtensions.map(ext => ext.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 { commands, getCommandHandler, getCommandDescription, isCommandRegistered, getRegisteredCommands } from './commands';
export type { KeyBinding, CommandHandler, CommandDefinition, KeymapResult } from './types'; export type { KeyBinding, CommandHandler, CommandDefinition, KeymapResult } from './types';

View File

@@ -8,7 +8,7 @@ import {getCommandHandler, isCommandRegistered} from './commands';
* *
* CodeMirror快捷键扩展 * CodeMirror快捷键扩展
*/ */
export class KeymapManager { export class Manager {
private static compartment = new Compartment(); private static compartment = new Compartment();
/** /**

View File

@@ -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<ExtensionID, ExtensionState>();
// 编辑器视图管理
private viewsMap = new Map<number, EditorViewInfo>();
private activeViewId: number | null = null;
// 注册的扩展工厂
private extensionFactories = new Map<ExtensionID, ExtensionFactory>();
// 防抖处理
private debouncedUpdateFunctions = new Map<ExtensionID, {
debouncedFn: (enabled: boolean, config: any) => 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();
}
}

View File

@@ -1,301 +1,152 @@
import {ExtensionManager} from './extensionManager'; import {Manager} from './manager';
import {ExtensionID} from '@/../bindings/voidraft/internal/models/models'; import {ExtensionID} from '@/../bindings/voidraft/internal/models/models';
import i18n from '@/i18n'; import i18n from '@/i18n';
import {ExtensionFactory} from './types' import {ExtensionDefinition} from './types';
// 导入现有扩展的创建函数
import rainbowBracketsExtension from '../extensions/rainbowBracket/rainbowBracketsExtension'; import rainbowBracketsExtension from '../extensions/rainbowBracket/rainbowBracketsExtension';
import {createTextHighlighter} from '../extensions/textHighlight/textHighlightExtension'; import {createTextHighlighter} from '../extensions/textHighlight/textHighlightExtension';
import {color} from '../extensions/colorSelector'; import {color} from '../extensions/colorSelector';
import {hyperLink} from '../extensions/hyperlink'; import {hyperLink} from '../extensions/hyperlink';
import {minimap} from '../extensions/minimap'; import {minimap} from '../extensions/minimap';
import {vscodeSearch} from '../extensions/vscodeSearch'; import {vscodeSearch} from '../extensions/vscodeSearch';
import {createCheckboxExtension} from '../extensions/checkbox'; import {createCheckboxExtension} from '../extensions/checkbox';
import {createTranslatorExtension} from '../extensions/translator'; import {createTranslatorExtension} from '../extensions/translator';
import {foldingOnIndent} from '../extensions/fold/foldExtension'; import {foldingOnIndent} from '../extensions/fold/foldExtension';
/** type ExtensionEntry = {
* 彩虹括号扩展工厂 definition: ExtensionDefinition
*/ displayNameKey: string
export const rainbowBracketsFactory: ExtensionFactory = { descriptionKey: string
create(_config: any) {
return rainbowBracketsExtension();
},
getDefaultConfig() {
return {};
},
validateConfig(config: any) {
return typeof config === 'object';
}
}; };
/** type RegisteredExtensionID = Exclude<ExtensionID, ExtensionID.$zero | ExtensionID.ExtensionEditor>;
* 文本高亮扩展工厂
*/ const defineExtension = (create: (config: any) => any, defaultConfig: Record<string, any> = {}): ExtensionDefinition => ({
export const textHighlightFactory: ExtensionFactory = { create,
create(config: any) { defaultConfig
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));
}
};
/** const EXTENSION_REGISTRY: Record<RegisteredExtensionID, ExtensionEntry> = {
* 小地图扩展工厂
*/
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');
}
};
/**
* 超链接扩展工厂
*/
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 = {
// 编辑增强扩展
[ExtensionID.ExtensionRainbowBrackets]: { [ExtensionID.ExtensionRainbowBrackets]: {
factory: rainbowBracketsFactory, definition: defineExtension(() => rainbowBracketsExtension()),
displayNameKey: 'extensions.rainbowBrackets.name', displayNameKey: 'extensions.rainbowBrackets.name',
descriptionKey: 'extensions.rainbowBrackets.description' descriptionKey: 'extensions.rainbowBrackets.description'
}, },
[ExtensionID.ExtensionHyperlink]: { [ExtensionID.ExtensionHyperlink]: {
factory: hyperlinkFactory, definition: defineExtension(() => hyperLink),
displayNameKey: 'extensions.hyperlink.name', displayNameKey: 'extensions.hyperlink.name',
descriptionKey: 'extensions.hyperlink.description' descriptionKey: 'extensions.hyperlink.description'
}, },
[ExtensionID.ExtensionColorSelector]: { [ExtensionID.ExtensionColorSelector]: {
factory: colorSelectorFactory, definition: defineExtension(() => color),
displayNameKey: 'extensions.colorSelector.name', displayNameKey: 'extensions.colorSelector.name',
descriptionKey: 'extensions.colorSelector.description' descriptionKey: 'extensions.colorSelector.description'
}, },
[ExtensionID.ExtensionTranslator]: { [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', displayNameKey: 'extensions.translator.name',
descriptionKey: 'extensions.translator.description' descriptionKey: 'extensions.translator.description'
}, },
// UI增强扩展
[ExtensionID.ExtensionMinimap]: { [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', displayNameKey: 'extensions.minimap.name',
descriptionKey: 'extensions.minimap.description' descriptionKey: 'extensions.minimap.description'
}, },
// 工具扩展
[ExtensionID.ExtensionSearch]: { [ExtensionID.ExtensionSearch]: {
factory: searchFactory, definition: defineExtension(() => vscodeSearch),
displayNameKey: 'extensions.search.name', displayNameKey: 'extensions.search.name',
descriptionKey: 'extensions.search.description' descriptionKey: 'extensions.search.description'
}, },
[ExtensionID.ExtensionFold]: { [ExtensionID.ExtensionFold]: {
factory: foldFactory, definition: defineExtension(() => foldingOnIndent),
displayNameKey: 'extensions.fold.name', displayNameKey: 'extensions.fold.name',
descriptionKey: 'extensions.fold.description' descriptionKey: 'extensions.fold.description'
}, },
[ExtensionID.ExtensionTextHighlight]: { [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', displayNameKey: 'extensions.textHighlight.name',
descriptionKey: 'extensions.textHighlight.description' descriptionKey: 'extensions.textHighlight.description'
}, },
[ExtensionID.ExtensionCheckbox]: { [ExtensionID.ExtensionCheckbox]: {
factory: checkboxFactory, definition: defineExtension(() => createCheckboxExtension()),
displayNameKey: 'extensions.checkbox.name', displayNameKey: 'extensions.checkbox.name',
descriptionKey: 'extensions.checkbox.description' 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];
}; };
/** export function registerAllExtensions(manager: Manager): void {
* 注册所有扩展工厂到管理器 (Object.entries(EXTENSION_REGISTRY) as [RegisteredExtensionID, ExtensionEntry][]).forEach(([id, entry]) => {
* @param manager 扩展管理器实例 manager.registerExtension(id, entry.definition);
*/
export function registerAllExtensions(manager: ExtensionManager): void {
Object.entries(EXTENSION_CONFIGS).forEach(([id, config]) => {
manager.registerExtension(id as ExtensionID, config.factory);
}); });
} }
/**
* 获取扩展工厂的显示名称
* @param id 扩展ID
* @returns 显示名称
*/
export function getExtensionDisplayName(id: ExtensionID): string { export function getExtensionDisplayName(id: ExtensionID): string {
const config = EXTENSION_CONFIGS[id as ExtensionID]; const entry = getRegistryEntry(id);
return config?.displayNameKey ? i18n.global.t(config.displayNameKey) : id; return entry?.displayNameKey ? i18n.global.t(entry.displayNameKey) : id;
} }
/**
* 获取扩展工厂的描述
* @param id 扩展ID
* @returns 描述
*/
export function getExtensionDescription(id: ExtensionID): string { export function getExtensionDescription(id: ExtensionID): string {
const config = EXTENSION_CONFIGS[id as ExtensionID]; const entry = getRegistryEntry(id);
return config?.descriptionKey ? i18n.global.t(config.descriptionKey) : ''; return entry?.descriptionKey ? i18n.global.t(entry.descriptionKey) : '';
} }
/** function getExtensionDefinition(id: ExtensionID): ExtensionDefinition | undefined {
* 获取扩展工厂实例 return getRegistryEntry(id)?.definition;
* @param id 扩展ID
* @returns 扩展工厂实例
*/
export function getExtensionFactory(id: ExtensionID): ExtensionFactory | undefined {
return EXTENSION_CONFIGS[id as ExtensionID]?.factory;
} }
/**
* 获取扩展的默认配置
* @param id 扩展ID
* @returns 默认配置对象
*/
export function getExtensionDefaultConfig(id: ExtensionID): any { export function getExtensionDefaultConfig(id: ExtensionID): any {
const factory = getExtensionFactory(id); const definition = getExtensionDefinition(id);
return factory?.getDefaultConfig() || {}; if (!definition) return {};
return cloneConfig(definition.defaultConfig);
} }
/**
* 检查扩展是否有配置项
* @param id 扩展ID
* @returns 是否有配置项
*/
export function hasExtensionConfig(id: ExtensionID): boolean { export function hasExtensionConfig(id: ExtensionID): boolean {
const defaultConfig = getExtensionDefaultConfig(id); return Object.keys(getExtensionDefaultConfig(id)).length > 0;
return Object.keys(defaultConfig).length > 0;
} }
/**
* 获取所有可用扩展的ID列表
* @returns 扩展ID数组
*/
export function getAllExtensionIds(): ExtensionID[] { export function getAllExtensionIds(): ExtensionID[] {
return Object.keys(EXTENSION_CONFIGS) as ExtensionID[]; 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<string, any>);
}
return config;
};

View File

@@ -1,13 +1,13 @@
import {Extension} from '@codemirror/state'; import {Extension} from '@codemirror/state';
import {EditorView} from '@codemirror/view'; import {EditorView} from '@codemirror/view';
import {useExtensionStore} from '@/stores/extensionStore'; import {useExtensionStore} from '@/stores/extensionStore';
import {ExtensionManager} from './extensionManager'; import {Manager} from './manager';
import {registerAllExtensions} from './extensions'; import {registerAllExtensions} from './extensions';
/** /**
* 全局扩展管理器实例 * 全局扩展管理器实例
*/ */
const extensionManager = new ExtensionManager(); const extensionManager = new Manager();
/** /**
* 异步创建动态扩展 * 异步创建动态扩展
@@ -26,7 +26,7 @@ export const createDynamicExtensions = async (_documentId?: number): Promise<Ext
} }
// 初始化扩展管理器配置 // 初始化扩展管理器配置
extensionManager.initializeExtensionsFromConfig(extensionStore.extensions); extensionManager.initExtensions(extensionStore.extensions);
// 获取初始扩展配置 // 获取初始扩展配置
return extensionManager.getInitialExtensions(); return extensionManager.getInitialExtensions();
@@ -36,7 +36,7 @@ export const createDynamicExtensions = async (_documentId?: number): Promise<Ext
* 获取扩展管理器实例 * 获取扩展管理器实例
* @returns 扩展管理器 * @returns 扩展管理器
*/ */
export const getExtensionManager = (): ExtensionManager => { export const getExtensionManager = (): Manager => {
return extensionManager; 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'; export {registerAllExtensions, getExtensionDisplayName, getExtensionDescription} from './extensions';

View File

@@ -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<ExtensionID, ExtensionState>();
private views = new Map<number, EditorView>();
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<T>(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<string, any>).reduce((acc, key) => {
(acc as any)[key] = this.cloneConfig((config as Record<string, any>)[key]);
return acc;
}, {} as Record<string, any>) as T;
}
return config;
}
}

View File

@@ -1,49 +1 @@
import {Compartment, Extension} from '@codemirror/state'; 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
}

View File

@@ -66,10 +66,12 @@ const updateExtensionConfig = async (extensionId: ExtensionID, configKey: string
if (!extension) return; if (!extension) return;
// 更新配置 // 更新配置
const updatedConfig = {...extension.config, [configKey]: value}; const updatedConfig = {...extension.config};
if (value === undefined) {
console.log(`[ExtensionsPage] 更新扩展 ${extensionId} 配置, ${configKey}=${value}`); delete updatedConfig[configKey];
} else {
updatedConfig[configKey] = value;
}
// 使用editorStore的updateExtension方法更新确保应用到所有编辑器实例 // 使用editorStore的updateExtension方法更新确保应用到所有编辑器实例
await editorStore.updateExtension(extensionId, extension.enabled, updatedConfig); await editorStore.updateExtension(extensionId, extension.enabled, updatedConfig);
@@ -81,7 +83,7 @@ const updateExtensionConfig = async (extensionId: ExtensionID, configKey: string
// 重置扩展到默认配置 // 重置扩展到默认配置
const resetExtension = async (extensionId: ExtensionID) => { const resetExtension = async (extensionId: ExtensionID) => {
try { try {
// 重置到默认配置(后端) // 重置到默认配置
await ExtensionService.ResetExtensionToDefault(extensionId); await ExtensionService.ResetExtensionToDefault(extensionId);
// 重新加载扩展状态以获取最新配置 // 重新加载扩展状态以获取最新配置
@@ -92,63 +94,65 @@ const resetExtension = async (extensionId: ExtensionID) => {
if (extension) { if (extension) {
// 通过editorStore更新确保所有视图都能同步 // 通过editorStore更新确保所有视图都能同步
await editorStore.updateExtension(extensionId, extension.enabled, extension.config); await editorStore.updateExtension(extensionId, extension.enabled, extension.config);
console.log(`[ExtensionsPage] 重置扩展 ${extensionId} 配置,同步应用到所有编辑器实例`);
} }
} catch (error) { } catch (error) {
console.error('Failed to reset extension:', error); console.error('Failed to reset extension:', error);
} }
}; };
// 配置项类型定义 const getConfigValue = (
type ConfigItemType = 'toggle' | 'number' | 'text' | 'select' config: Record<string, any> | undefined,
configKey: string,
defaultValue: any
) => {
if (config && Object.prototype.hasOwnProperty.call(config, configKey)) {
return config[configKey];
}
return defaultValue;
interface SelectOption {
value: any
label: string
}
interface ConfigItemMeta {
type: ConfigItemType
options?: SelectOption[]
}
// 只保留 select 类型的配置项元数据
const extensionConfigMeta: Partial<Record<ExtensionID, Record<string, ConfigItemMeta>>> = {
[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 getConfigItemType = (extensionId: ExtensionID, configKey: string, defaultValue: any): string => { const formatConfigValue = (value: any): string => {
const meta = extensionConfigMeta[extensionId]?.[configKey]; if (value === undefined) return '';
if (meta?.type) { try {
return meta.type; 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[] => { const handleConfigInput = async (
return extensionConfigMeta[extensionId]?.[configKey]?.options || []; 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);
}
}; };
</script> </script>
<template> <template>
@@ -204,58 +208,28 @@ const getSelectOptions = (extensionId: ExtensionID, configKey: string): SelectOp
</button> </button>
</div> </div>
<div <div class="config-table-wrapper">
<table class="config-table">
<tbody>
<tr
v-for="[configKey, configValue] in Object.entries(extension.defaultConfig)" v-for="[configKey, configValue] in Object.entries(extension.defaultConfig)"
:key="configKey" :key="configKey"
class="config-item"
> >
<SettingItem <th scope="row" class="config-table-key">
:title="configKey" {{ configKey }}
> </th>
<!-- 布尔值切换开关 --> <td class="config-table-value">
<ToggleSwitch
v-if="getConfigItemType(extension.id, configKey, configValue) === 'toggle'"
:model-value="extension.config[configKey] ?? configValue"
@update:model-value="updateExtensionConfig(extension.id, configKey, $event)"
/>
<!-- 数字输入框 -->
<input <input
v-else-if="getConfigItemType(extension.id, configKey, configValue) === 'number'" class="config-value-input"
type="number"
class="config-input"
:value="extension.config[configKey] ?? configValue"
:min="configKey === 'opacity' ? 0 : undefined"
:max="configKey === 'opacity' ? 1 : undefined"
:step="configKey === 'opacity' ? 0.1 : 1"
@input="updateExtensionConfig(extension.id, configKey, parseFloat(($event.target as HTMLInputElement).value))"
/>
<!-- 选择框 -->
<select
v-else-if="getConfigItemType(extension.id, configKey, configValue) === 'select'"
class="config-select"
:value="extension.config[configKey] ?? configValue"
@change="updateExtensionConfig(extension.id, configKey, ($event.target as HTMLSelectElement).value)"
>
<option
v-for="option in getSelectOptions(extension.id, configKey)"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</option>
</select>
<!-- 文本输入框 -->
<input
v-else
type="text" type="text"
class="config-input" :value="formatConfigValue(getConfigValue(extension.config, configKey, configValue))"
:value="extension.config[configKey] ?? configValue" @change="handleConfigInput(extension.id, configKey, configValue, $event)"
@input="updateExtensionConfig(extension.id, configKey, ($event.target as HTMLInputElement).value)" @keyup.enter.prevent="handleConfigInput(extension.id, configKey, configValue, $event)"
/> />
</SettingItem> </td>
</tr>
</tbody>
</table>
</div> </div>
</div> </div>
</div> </div>
@@ -361,37 +335,65 @@ const getSelectOptions = (extensionId: ExtensionID, configKey: string): SelectOp
} }
} }
.config-item { .config-table-wrapper {
&:not(:last-child) { border: 1px solid var(--settings-input-border);
margin-bottom: 12px; border-radius: 6px;
margin-top: 8px;
overflow: hidden;
background-color: var(--settings-panel, var(--settings-input-bg));
} }
/* 配置项标题和描述字体大小 */ .config-table {
:deep(.setting-item-title) { width: 100%;
border-collapse: collapse;
font-size: 12px; font-size: 12px;
} }
:deep(.setting-item-description) { .config-table tr + tr {
font-size: 11px; border-top: 1px solid var(--settings-input-border);
}
} }
.config-input, .config-select { .config-table th,
min-width: 120px; .config-table td {
padding: 4px 8px; padding: 10px 12px;
border: 1px solid var(--settings-input-border); vertical-align: middle;
border-radius: 3px; }
.config-table-key {
width: 36%;
text-align: left;
font-weight: 600;
color: var(--settings-text-secondary);
border-right: 1px solid var(--settings-input-border);
background-color: var(--settings-input-bg); background-color: var(--settings-input-bg);
color: var(--settings-text); }
font-size: 11px;
&:focus { .config-table-value {
padding: 6px;
}
.config-value-input {
width: 100%;
padding: 8px 10px;
border: 1px solid transparent;
border-radius: 4px;
background: transparent;
color: var(--settings-text);
font-size: 12px;
line-height: 1.4;
box-sizing: border-box;
transition: border-color 0.2s ease, background-color 0.2s ease;
}
.config-value-input:hover {
border-color: var(--settings-hover-border, var(--settings-input-border));
background-color: var(--settings-hover);
}
.config-value-input:focus {
outline: none; outline: none;
border-color: var(--settings-accent); border-color: var(--settings-accent);
} background-color: var(--settings-input-bg);
}
.config-select {
cursor: pointer;
} }
</style> </style>