♻️ Refactor some code
This commit is contained in:
@@ -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();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
};
|
};
|
||||||
@@ -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';
|
||||||
@@ -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();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
|
|||||||
@@ -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';
|
||||||
135
frontend/src/views/editor/manager/manager.ts
Normal file
135
frontend/src/views/editor/manager/manager.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user