From f3bcb87828f223f5375a0b6cd0499305aadfd927 Mon Sep 17 00:00:00 2001 From: landaiqing Date: Tue, 24 Jun 2025 14:16:53 +0800 Subject: [PATCH] :sparkles: Add extension management service --- .../voidraft/internal/models/models.ts | 245 ++++++++++++++- .../internal/services/extensionservice.ts | 93 ++++++ .../voidraft/internal/services/index.ts | 2 + frontend/src/stores/editorStore.ts | 51 ++- frontend/src/stores/extensionStore.ts | 76 +++++ frontend/src/stores/systemStore.ts | 140 ++++----- frontend/src/views/editor/Editor.vue | 2 +- .../autoSaveExtension.ts | 2 +- .../{extensions => basic}/basicSetup.ts | 34 -- .../{extensions => basic}/fontExtension.ts | 8 - .../{extensions => basic}/statsExtension.ts | 0 .../{extensions => basic}/tabExtension.ts | 0 .../{extensions => basic}/themeExtension.ts | 0 .../wheelZoomExtension.ts | 0 .../extensions/{ => fold}/foldExtension.ts | 0 frontend/src/views/editor/extensions/index.ts | 10 - .../rainbowBracketsExtension.ts} | 2 +- .../textHighlightExtension.ts | 0 .../keymap/commandRegistry.ts | 43 ++- .../editor/{extensions => }/keymap/index.ts | 0 .../{extensions => }/keymap/keymapManager.ts | 0 .../editor/{extensions => }/keymap/types.ts | 0 .../views/editor/manager/ExtensionManager.ts | 264 ++++++++++++++++ .../src/views/editor/manager/factories.ts | 296 ++++++++++++++++++ frontend/src/views/editor/manager/index.ts | 51 +++ .../views/settings/pages/KeyBindingsPage.vue | 2 +- internal/models/extensions.go | 220 +++++++++++++ internal/services/config_migration_service.go | 9 + internal/services/extension_service.go | 278 ++++++++++++++++ internal/services/path_manager.go | 19 +- internal/services/service_manager.go | 11 + 31 files changed, 1682 insertions(+), 176 deletions(-) create mode 100644 frontend/bindings/voidraft/internal/services/extensionservice.ts create mode 100644 frontend/src/stores/extensionStore.ts rename frontend/src/views/editor/{extensions => basic}/autoSaveExtension.ts (97%) rename frontend/src/views/editor/{extensions => basic}/basicSetup.ts (58%) rename frontend/src/views/editor/{extensions => basic}/fontExtension.ts (93%) rename frontend/src/views/editor/{extensions => basic}/statsExtension.ts (100%) rename frontend/src/views/editor/{extensions => basic}/tabExtension.ts (100%) rename frontend/src/views/editor/{extensions => basic}/themeExtension.ts (100%) rename frontend/src/views/editor/{extensions => basic}/wheelZoomExtension.ts (100%) rename frontend/src/views/editor/extensions/{ => fold}/foldExtension.ts (100%) delete mode 100644 frontend/src/views/editor/extensions/index.ts rename frontend/src/views/editor/extensions/{rainbowBrackets.ts => rainbowBracket/rainbowBracketsExtension.ts} (98%) rename frontend/src/views/editor/extensions/{ => textHighlight}/textHighlightExtension.ts (100%) rename frontend/src/views/editor/{extensions => }/keymap/commandRegistry.ts (90%) rename frontend/src/views/editor/{extensions => }/keymap/index.ts (100%) rename frontend/src/views/editor/{extensions => }/keymap/keymapManager.ts (100%) rename frontend/src/views/editor/{extensions => }/keymap/types.ts (100%) create mode 100644 frontend/src/views/editor/manager/ExtensionManager.ts create mode 100644 frontend/src/views/editor/manager/factories.ts create mode 100644 frontend/src/views/editor/manager/index.ts create mode 100644 internal/models/extensions.go create mode 100644 internal/services/extension_service.go diff --git a/frontend/bindings/voidraft/internal/models/models.ts b/frontend/bindings/voidraft/internal/models/models.ts index 64ff228..97f03c6 100644 --- a/frontend/bindings/voidraft/internal/models/models.ts +++ b/frontend/bindings/voidraft/internal/models/models.ts @@ -334,6 +334,227 @@ export class EditingConfig { } } +/** + * Extension 单个扩展配置 + */ +export class Extension { + /** + * 扩展唯一标识 + */ + "id": ExtensionID; + + /** + * 扩展分类 + */ + "category": ExtensionCategory; + + /** + * 是否启用 + */ + "enabled": boolean; + + /** + * 是否为默认扩展 + */ + "isDefault": boolean; + + /** + * 扩展配置项 + */ + "config": ExtensionConfig; + + /** Creates a new Extension instance. */ + constructor($$source: Partial = {}) { + if (!("id" in $$source)) { + this["id"] = ("" as ExtensionID); + } + if (!("category" in $$source)) { + this["category"] = ("" as ExtensionCategory); + } + if (!("enabled" in $$source)) { + this["enabled"] = false; + } + if (!("isDefault" in $$source)) { + this["isDefault"] = false; + } + if (!("config" in $$source)) { + this["config"] = ({} as ExtensionConfig); + } + + Object.assign(this, $$source); + } + + /** + * Creates a new Extension instance from a string or object. + */ + static createFrom($$source: any = {}): Extension { + const $$createField4_0 = $$createType6; + let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; + if ("config" in $$parsedSource) { + $$parsedSource["config"] = $$createField4_0($$parsedSource["config"]); + } + return new Extension($$parsedSource as Partial); + } +} + +/** + * ExtensionCategory 扩展分类 + */ +export enum ExtensionCategory { + /** + * The Go zero value for the underlying type of the enum. + */ + $zero = "", + + /** + * 编辑增强 + */ + CategoryEditing = "editing", + + /** + * 界面增强 + */ + CategoryUI = "ui", + + /** + * 工具类 + */ + CategoryTools = "tools", +}; + +/** + * ExtensionConfig 扩展配置项(动态配置) + */ +export type ExtensionConfig = { [_: string]: any }; + +/** + * ExtensionID 扩展标识符 + */ +export enum ExtensionID { + /** + * The Go zero value for the underlying type of the enum. + */ + $zero = "", + + /** + * 编辑增强扩展 + * 彩虹括号 + */ + ExtensionRainbowBrackets = "rainbowBrackets", + + /** + * 超链接 + */ + ExtensionHyperlink = "hyperlink", + + /** + * 颜色选择器 + */ + ExtensionColorSelector = "colorSelector", + ExtensionFold = "fold", + ExtensionTextHighlight = "textHighlight", + + /** + * UI增强扩展 + * 小地图 + */ + ExtensionMinimap = "minimap", + + /** + * 代码爆炸效果 + */ + ExtensionCodeBlast = "codeBlast", + + /** + * 工具扩展 + * 搜索功能 + */ + ExtensionSearch = "search", + + /** + * 代码块 + */ + ExtensionCodeBlock = "codeBlock", +}; + +/** + * ExtensionMetadata 扩展配置元数据 + */ +export class ExtensionMetadata { + /** + * 配置版本 + */ + "version": string; + + /** + * 最后更新时间 + */ + "lastUpdated": string; + + /** Creates a new ExtensionMetadata instance. */ + constructor($$source: Partial = {}) { + if (!("version" in $$source)) { + this["version"] = ""; + } + if (!("lastUpdated" in $$source)) { + this["lastUpdated"] = ""; + } + + Object.assign(this, $$source); + } + + /** + * Creates a new ExtensionMetadata instance from a string or object. + */ + static createFrom($$source: any = {}): ExtensionMetadata { + let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; + return new ExtensionMetadata($$parsedSource as Partial); + } +} + +/** + * ExtensionSettings 扩展设置配置 + */ +export class ExtensionSettings { + /** + * 扩展列表 + */ + "extensions": Extension[]; + + /** + * 配置元数据 + */ + "metadata": ExtensionMetadata; + + /** Creates a new ExtensionSettings instance. */ + constructor($$source: Partial = {}) { + if (!("extensions" in $$source)) { + this["extensions"] = []; + } + if (!("metadata" in $$source)) { + this["metadata"] = (new ExtensionMetadata()); + } + + Object.assign(this, $$source); + } + + /** + * Creates a new ExtensionSettings instance from a string or object. + */ + static createFrom($$source: any = {}): ExtensionSettings { + const $$createField0_0 = $$createType9; + const $$createField1_0 = $$createType10; + let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; + if ("extensions" in $$parsedSource) { + $$parsedSource["extensions"] = $$createField0_0($$parsedSource["extensions"]); + } + if ("metadata" in $$parsedSource) { + $$parsedSource["metadata"] = $$createField1_0($$parsedSource["metadata"]); + } + return new ExtensionSettings($$parsedSource as Partial); + } +} + /** * GeneralConfig 通用设置配置 */ @@ -397,7 +618,7 @@ export class GeneralConfig { * Creates a new GeneralConfig instance from a string or object. */ static createFrom($$source: any = {}): GeneralConfig { - const $$createField5_0 = $$createType6; + const $$createField5_0 = $$createType11; let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; if ("globalHotkey" in $$parsedSource) { $$parsedSource["globalHotkey"] = $$createField5_0($$parsedSource["globalHotkey"]); @@ -874,8 +1095,8 @@ export class KeyBindingConfig { * Creates a new KeyBindingConfig instance from a string or object. */ static createFrom($$source: any = {}): KeyBindingConfig { - const $$createField0_0 = $$createType8; - const $$createField1_0 = $$createType9; + const $$createField0_0 = $$createType13; + const $$createField1_0 = $$createType14; let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; if ("keyBindings" in $$parsedSource) { $$parsedSource["keyBindings"] = $$createField0_0($$parsedSource["keyBindings"]); @@ -1029,7 +1250,17 @@ const $$createType2 = AppearanceConfig.createFrom; const $$createType3 = UpdatesConfig.createFrom; const $$createType4 = ConfigMetadata.createFrom; const $$createType5 = DocumentMeta.createFrom; -const $$createType6 = HotkeyCombo.createFrom; -const $$createType7 = KeyBinding.createFrom; -const $$createType8 = $Create.Array($$createType7); -const $$createType9 = KeyBindingMetadata.createFrom; +var $$createType6 = (function $$initCreateType6(...args): any { + if ($$createType6 === $$initCreateType6) { + $$createType6 = $$createType7; + } + return $$createType6(...args); +}); +const $$createType7 = $Create.Map($Create.Any, $Create.Any); +const $$createType8 = Extension.createFrom; +const $$createType9 = $Create.Array($$createType8); +const $$createType10 = ExtensionMetadata.createFrom; +const $$createType11 = HotkeyCombo.createFrom; +const $$createType12 = KeyBinding.createFrom; +const $$createType13 = $Create.Array($$createType12); +const $$createType14 = KeyBindingMetadata.createFrom; diff --git a/frontend/bindings/voidraft/internal/services/extensionservice.ts b/frontend/bindings/voidraft/internal/services/extensionservice.ts new file mode 100644 index 0000000..93ebc0b --- /dev/null +++ b/frontend/bindings/voidraft/internal/services/extensionservice.ts @@ -0,0 +1,93 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +/** + * ExtensionService 扩展管理服务 + * @module + */ + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import {Call as $Call, Create as $Create} from "@wailsio/runtime"; + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import * as models$0 from "../models/models.js"; + +/** + * DisableExtension 禁用扩展 + */ +export function DisableExtension(id: models$0.ExtensionID): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(2040844784, id) as any; + return $resultPromise; +} + +/** + * EnableExtension 启用扩展 + */ +export function EnableExtension(id: models$0.ExtensionID): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(2926319443, id) as any; + return $resultPromise; +} + +/** + * GetAllExtensions 获取所有扩展配置 + */ +export function GetAllExtensions(): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(3094292124) as any; + let $typingPromise = $resultPromise.then(($result: any) => { + return $$createType1($result); + }) as any; + $typingPromise.cancel = $resultPromise.cancel.bind($resultPromise); + return $typingPromise; +} + +/** + * GetExtensionSettings 获取完整扩展配置 + */ +export function GetExtensionSettings(): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(2127854337) as any; + let $typingPromise = $resultPromise.then(($result: any) => { + return $$createType3($result); + }) as any; + $typingPromise.cancel = $resultPromise.cancel.bind($resultPromise); + return $typingPromise; +} + +/** + * ResetAllExtensionsToDefault 重置所有扩展到默认状态 + */ +export function ResetAllExtensionsToDefault(): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(270611949) as any; + return $resultPromise; +} + +/** + * ResetExtensionToDefault 重置扩展到默认状态 + */ +export function ResetExtensionToDefault(id: models$0.ExtensionID): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(868308101, id) as any; + return $resultPromise; +} + +/** + * ServiceShutdown 关闭服务 + */ +export function ServiceShutdown(): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(4127635746) as any; + return $resultPromise; +} + +/** + * UpdateExtensionState 更新扩展状态 + */ +export function UpdateExtensionState(id: models$0.ExtensionID, enabled: boolean, config: models$0.ExtensionConfig): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(2917946454, id, enabled, config) as any; + return $resultPromise; +} + +// Private type creation functions +const $$createType0 = models$0.Extension.createFrom; +const $$createType1 = $Create.Array($$createType0); +const $$createType2 = models$0.ExtensionSettings.createFrom; +const $$createType3 = $Create.Nullable($$createType2); diff --git a/frontend/bindings/voidraft/internal/services/index.ts b/frontend/bindings/voidraft/internal/services/index.ts index 980bd62..24fafc8 100644 --- a/frontend/bindings/voidraft/internal/services/index.ts +++ b/frontend/bindings/voidraft/internal/services/index.ts @@ -4,6 +4,7 @@ import * as ConfigService from "./configservice.js"; import * as DialogService from "./dialogservice.js"; import * as DocumentService from "./documentservice.js"; +import * as ExtensionService from "./extensionservice.js"; import * as HotkeyService from "./hotkeyservice.js"; import * as KeyBindingService from "./keybindingservice.js"; import * as MigrationService from "./migrationservice.js"; @@ -15,6 +16,7 @@ export { ConfigService, DialogService, DocumentService, + ExtensionService, HotkeyService, KeyBindingService, MigrationService, diff --git a/frontend/src/stores/editorStore.ts b/frontend/src/stores/editorStore.ts index 71166e8..352b3f3 100644 --- a/frontend/src/stores/editorStore.ts +++ b/frontend/src/stores/editorStore.ts @@ -8,13 +8,16 @@ import {useThemeStore} from './themeStore'; import {SystemThemeType} from '@/../bindings/voidraft/internal/models/models'; import {DocumentService} from '@/../bindings/voidraft/internal/services'; import {ensureSyntaxTree} from "@codemirror/language" -import {createBasicSetup} from '@/views/editor/extensions/basicSetup'; -import {createThemeExtension, updateEditorTheme} from '@/views/editor/extensions/themeExtension'; -import {getTabExtensions, updateTabConfig} from '@/views/editor/extensions/tabExtension'; -import {createFontExtensionFromBackend, updateFontConfig} from '@/views/editor/extensions/fontExtension'; -import {createStatsUpdateExtension} from '@/views/editor/extensions/statsExtension'; -import {createAutoSavePlugin, createSaveShortcutPlugin} from '@/views/editor/extensions/autoSaveExtension'; -import {createDynamicKeymapExtension} from '@/views/editor/extensions/keymap'; +import {createBasicSetup} from '@/views/editor/basic/basicSetup'; +import {createThemeExtension, updateEditorTheme} from '@/views/editor/basic/themeExtension'; +import {getTabExtensions, updateTabConfig} from '@/views/editor/basic/tabExtension'; +import {createFontExtensionFromBackend, updateFontConfig} from '@/views/editor/basic/fontExtension'; +import {createStatsUpdateExtension} from '@/views/editor/basic/statsExtension'; +import {createAutoSavePlugin, createSaveShortcutPlugin} from '@/views/editor/basic/autoSaveExtension'; +import {createDynamicKeymapExtension} from '@/views/editor/keymap'; +import { createDynamicExtensions, setExtensionManagerView, getExtensionManager } from '@/views/editor/manager'; +import { useExtensionStore } from './extensionStore'; +import { ExtensionService } from '@/../bindings/voidraft/internal/services'; export interface DocumentStats { lines: number; @@ -27,6 +30,7 @@ export const useEditorStore = defineStore('editor', () => { const configStore = useConfigStore(); const documentStore = useDocumentStore(); const themeStore = useThemeStore(); + const extensionStore = useExtensionStore(); // 状态 const documentStats = ref({ @@ -130,14 +134,14 @@ export const useEditorStore = defineStore('editor', () => { ); // 创建保存快捷键插件 - const saveShortcutPlugin = createSaveShortcutPlugin(() => { + const saveShortcutExtension = createSaveShortcutPlugin(() => { if (editorView.value) { handleManualSave(); } }); // 创建自动保存插件 - const autoSavePlugin = createAutoSavePlugin({ + const autoSaveExtension = createAutoSavePlugin({ debounceDelay: 300, // 300毫秒的输入防抖 onSave: (success) => { if (success) { @@ -148,6 +152,9 @@ export const useEditorStore = defineStore('editor', () => { // 创建动态快捷键扩展 const keymapExtension = await createDynamicKeymapExtension(); + + // 创建动态扩展 + const dynamicExtensions = await createDynamicExtensions(); // 组合所有扩展 const extensions: Extension[] = [ @@ -157,8 +164,9 @@ export const useEditorStore = defineStore('editor', () => { ...tabExtensions, fontExtension, statsExtension, - saveShortcutPlugin, - autoSavePlugin + saveShortcutExtension, + autoSaveExtension, + ...dynamicExtensions ]; // 创建编辑器状态 @@ -175,6 +183,9 @@ export const useEditorStore = defineStore('editor', () => { // 将编辑器实例保存到store setEditorView(view); + + // 设置编辑器视图到扩展管理器 + setExtensionManagerView(view); isEditorInitialized.value = true; @@ -257,6 +268,23 @@ export const useEditorStore = defineStore('editor', () => { } }); + // 扩展管理方法 + const updateExtension = async (id: any, enabled: boolean, config?: any) => { + try { + // 更新后端配置 + await ExtensionService.UpdateExtensionState(id, enabled, config || {}) + + // 更新前端编辑器 + const manager = getExtensionManager() + manager.updateExtension(id, enabled, config || {}) + + // 重新加载扩展配置 + await extensionStore.loadExtensions() + } catch (error) { + console.error('Failed to update extension:', error) + } + } + return { // 状态 documentStats, @@ -272,5 +300,6 @@ export const useEditorStore = defineStore('editor', () => { handleManualSave, destroyEditor, scrollEditorToBottom, + updateExtension }; }); \ No newline at end of file diff --git a/frontend/src/stores/extensionStore.ts b/frontend/src/stores/extensionStore.ts new file mode 100644 index 0000000..d90a7f4 --- /dev/null +++ b/frontend/src/stores/extensionStore.ts @@ -0,0 +1,76 @@ +import { defineStore } from 'pinia' +import { computed, ref } from 'vue' +import { Extension, ExtensionID, ExtensionCategory, ExtensionSettings } from '@/../bindings/voidraft/internal/models/models' +import { ExtensionService } from '@/../bindings/voidraft/internal/services' + +export const useExtensionStore = defineStore('extension', () => { + // 扩展配置数据 + const extensions = ref([]) + const settings = ref(null) + + // 获取启用的扩展 + const enabledExtensions = computed(() => + extensions.value.filter(ext => ext.enabled) + ) + + // 根据分类获取扩展 + const getExtensionsByCategory = computed(() => + (category: ExtensionCategory) => + extensions.value.filter(ext => ext.category === category) + ) + + // 获取启用的扩展按分类分组 + const enabledExtensionsByCategory = computed(() => { + const grouped = new Map() + enabledExtensions.value.forEach(ext => { + if (!grouped.has(ext.category)) { + grouped.set(ext.category, []) + } + grouped.get(ext.category)!.push(ext) + }) + return grouped + }) + + /** + * 从后端加载扩展配置 + */ + const loadExtensions = async (): Promise => { + try { + extensions.value = await ExtensionService.GetAllExtensions() + } catch (err) { + console.error('Failed to load extensions:', err) + } + } + + /** + * 检查扩展是否启用 + */ + const isExtensionEnabled = (id: ExtensionID): boolean => { + const extension = extensions.value.find(ext => ext.id === id) + return extension?.enabled ?? false + } + + /** + * 获取扩展配置 + */ + const getExtensionConfig = (id: ExtensionID): any => { + const extension = extensions.value.find(ext => ext.id === id) + return extension?.config ?? {} + } + + return { + // 状态 + extensions, + settings, + enabledExtensions, + + // 计算属性 + getExtensionsByCategory, + enabledExtensionsByCategory, + + // 方法 + loadExtensions, + isExtensionEnabled, + getExtensionConfig + } +}) \ No newline at end of file diff --git a/frontend/src/stores/systemStore.ts b/frontend/src/stores/systemStore.ts index 06ba102..491bfd8 100644 --- a/frontend/src/stores/systemStore.ts +++ b/frontend/src/stores/systemStore.ts @@ -3,98 +3,64 @@ import {computed, ref} from 'vue'; import * as runtime from '@wailsio/runtime'; export interface SystemEnvironment { - OS: string; - Arch: string; - Debug: boolean; - OSInfo: { - Name: string; - Branding: string; - Version: string; - ID: string; - }; - PlatformInfo?: Record; + OS: string; + Arch: string; + Debug: boolean; + OSInfo: { + Name: string; + Branding: string; + Version: string; + ID: string; + }; + PlatformInfo?: Record; } export const useSystemStore = defineStore('system', () => { - // 状态 - const environment = ref(null); - const isLoading = ref(false); - const error = ref(null); - - // 计算属性 - const isWindows = computed(() => environment.value?.OS === 'windows'); - const isMacOS = computed(() => environment.value?.OS === 'darwin'); - const isLinux = computed(() => environment.value?.OS === 'linux'); - - // 获取操作系统名称 - const osName = computed(() => { - if (!environment.value) return 'Unknown'; - return environment.value.OSInfo?.Name || environment.value.OS || 'Unknown'; - }); - - // 获取架构信息 - const architecture = computed(() => environment.value?.Arch || 'Unknown'); - - // 获取标题栏高度 - const titleBarHeight = computed(() => { - if (isWindows.value) return '32px'; - if (isMacOS.value) return '28px'; - return '34px'; // Linux 默认 - }); - - // 初始化系统信息 - const initializeSystemInfo = async (): Promise => { - if (isLoading.value) return; - - isLoading.value = true; - error.value = null; - - try { - environment.value = await runtime.System.Environment(); - } catch (err) { - error.value = 'Failed to get system environment'; - environment.value = null; - } finally { - isLoading.value = false; - } - }; - - // 获取平台特定信息 - const getPlatformInfo = () => { - return environment.value?.PlatformInfo || {}; - }; - - // 检查是否支持某项功能(基于操作系统) - const supportsFeature = (feature: string): boolean => { - switch (feature) { - case 'systemTray': - return true; // 所有平台都支持 - case 'globalHotkeys': - return !isLinux.value; // Linux 支持可能有限 - case 'transparency': - return isWindows.value || isMacOS.value; - default: - return false; - } - }; - - return { // 状态 - environment, - isLoading, - error, + const environment = ref(null); + const isLoading = ref(false); + // 计算属性 - isWindows, - isMacOS, - isLinux, - osName, - architecture, - titleBarHeight, + const isWindows = computed(() => environment.value?.OS === 'windows'); + const isMacOS = computed(() => environment.value?.OS === 'darwin'); + const isLinux = computed(() => environment.value?.OS === 'linux'); - // 方法 - initializeSystemInfo, - getPlatformInfo, - supportsFeature - }; + + // 获取标题栏高度 + const titleBarHeight = computed(() => { + if (isWindows.value) return '32px'; + if (isMacOS.value) return '28px'; + return '34px'; // Linux 默认 + }); + + // 初始化系统信息 + const initializeSystemInfo = async (): Promise => { + if (isLoading.value) return; + + isLoading.value = true; + + try { + environment.value = await runtime.System.Environment(); + } catch (err) { + environment.value = null; + } finally { + isLoading.value = false; + } + }; + + return { + // 状态 + environment, + isLoading, + + // 计算属性 + isWindows, + isMacOS, + isLinux, + titleBarHeight, + + // 方法 + initializeSystemInfo, + }; }); \ No newline at end of file diff --git a/frontend/src/views/editor/Editor.vue b/frontend/src/views/editor/Editor.vue index 7b80d64..79c7360 100644 --- a/frontend/src/views/editor/Editor.vue +++ b/frontend/src/views/editor/Editor.vue @@ -2,7 +2,7 @@ import {onBeforeUnmount, onMounted, ref} from 'vue'; import {useEditorStore} from '@/stores/editorStore'; import {useConfigStore} from '@/stores/configStore'; -import {createWheelZoomHandler} from './extensions'; +import {createWheelZoomHandler} from './basic/wheelZoomExtension'; import Toolbar from '@/components/toolbar/Toolbar.vue'; const editorStore = useEditorStore(); diff --git a/frontend/src/views/editor/extensions/autoSaveExtension.ts b/frontend/src/views/editor/basic/autoSaveExtension.ts similarity index 97% rename from frontend/src/views/editor/extensions/autoSaveExtension.ts rename to frontend/src/views/editor/basic/autoSaveExtension.ts index ffeacc6..fc8210c 100644 --- a/frontend/src/views/editor/extensions/autoSaveExtension.ts +++ b/frontend/src/views/editor/basic/autoSaveExtension.ts @@ -1,5 +1,5 @@ import { EditorView, ViewPlugin, ViewUpdate } from '@codemirror/view'; -import { DocumentService } from '../../../../bindings/voidraft/internal/services'; +import { DocumentService } from '@/../bindings/voidraft/internal/services'; import { useDebounceFn } from '@vueuse/core'; // 定义自动保存配置选项 diff --git a/frontend/src/views/editor/extensions/basicSetup.ts b/frontend/src/views/editor/basic/basicSetup.ts similarity index 58% rename from frontend/src/views/editor/extensions/basicSetup.ts rename to frontend/src/views/editor/basic/basicSetup.ts index e2943a7..a8df30b 100644 --- a/frontend/src/views/editor/extensions/basicSetup.ts +++ b/frontend/src/views/editor/basic/basicSetup.ts @@ -21,16 +21,6 @@ import { import {history} from '@codemirror/commands'; import {highlightSelectionMatches} from '@codemirror/search'; import {autocompletion, closeBrackets, closeBracketsKeymap} from '@codemirror/autocomplete'; -import {searchVisibilityField, vscodeSearch} from './vscodeSearch'; - -import {hyperLink} from './hyperlink'; -import {color} from './colorSelector'; -import {createTextHighlighter} from './textHighlightExtension'; -import {minimap} from './minimap'; -import {createCodeBlockExtension} from './codeblock/index'; -import {foldingOnIndent} from './foldExtension' -import rainbowBrackets from "./rainbowBrackets"; -import {createCodeBlastExtension} from './codeblast'; // 基本编辑器设置 export const createBasicSetup = (): Extension[] => { return [ @@ -63,30 +53,6 @@ export const createBasicSetup = (): Extension[] => { // 自动完成 autocompletion(), - vscodeSearch, - searchVisibilityField, - foldingOnIndent, - rainbowBrackets(), - createCodeBlastExtension({ - effect: 1, - shake: true, - maxParticles: 300, - shakeIntensity: 3 - }), - hyperLink, - color, - ...createTextHighlighter('hl'), - minimap({ - displayText: 'characters', - showOverlay: 'always', - autohide: false, - }), - - createCodeBlockExtension({ - showBackground: true, - enableAutoDetection: true, - }), - // 键盘映射 keymap.of([ ...closeBracketsKeymap, diff --git a/frontend/src/views/editor/extensions/fontExtension.ts b/frontend/src/views/editor/basic/fontExtension.ts similarity index 93% rename from frontend/src/views/editor/extensions/fontExtension.ts rename to frontend/src/views/editor/basic/fontExtension.ts index 2676d39..880ef26 100644 --- a/frontend/src/views/editor/extensions/fontExtension.ts +++ b/frontend/src/views/editor/basic/fontExtension.ts @@ -81,14 +81,6 @@ export function createFontExtension(config: Partial = {}): Extension return EditorView.theme(styles); } -// 创建响应式字体大小扩展 -export function createResponsiveFontExtension(baseFontSize: number = 14): Extension { - return fontCompartment.of(createFontExtension({ - fontSize: baseFontSize, - lineHeight: 1.5 - })); -} - // 从后端配置创建字体扩展 export function createFontExtensionFromBackend(backendConfig: { fontFamily?: string; diff --git a/frontend/src/views/editor/extensions/statsExtension.ts b/frontend/src/views/editor/basic/statsExtension.ts similarity index 100% rename from frontend/src/views/editor/extensions/statsExtension.ts rename to frontend/src/views/editor/basic/statsExtension.ts diff --git a/frontend/src/views/editor/extensions/tabExtension.ts b/frontend/src/views/editor/basic/tabExtension.ts similarity index 100% rename from frontend/src/views/editor/extensions/tabExtension.ts rename to frontend/src/views/editor/basic/tabExtension.ts diff --git a/frontend/src/views/editor/extensions/themeExtension.ts b/frontend/src/views/editor/basic/themeExtension.ts similarity index 100% rename from frontend/src/views/editor/extensions/themeExtension.ts rename to frontend/src/views/editor/basic/themeExtension.ts diff --git a/frontend/src/views/editor/extensions/wheelZoomExtension.ts b/frontend/src/views/editor/basic/wheelZoomExtension.ts similarity index 100% rename from frontend/src/views/editor/extensions/wheelZoomExtension.ts rename to frontend/src/views/editor/basic/wheelZoomExtension.ts diff --git a/frontend/src/views/editor/extensions/foldExtension.ts b/frontend/src/views/editor/extensions/fold/foldExtension.ts similarity index 100% rename from frontend/src/views/editor/extensions/foldExtension.ts rename to frontend/src/views/editor/extensions/fold/foldExtension.ts diff --git a/frontend/src/views/editor/extensions/index.ts b/frontend/src/views/editor/extensions/index.ts deleted file mode 100644 index e3cb8ce..0000000 --- a/frontend/src/views/editor/extensions/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -// 统一导出所有扩展 -export * from './tabExtension'; -export * from './wheelZoomExtension'; -export * from './statsExtension'; -export * from './autoSaveExtension'; -export * from './fontExtension'; -export * from './themeExtension'; -export * from './codeblast'; -export * from './codeblock'; -export * from './keymap'; \ No newline at end of file diff --git a/frontend/src/views/editor/extensions/rainbowBrackets.ts b/frontend/src/views/editor/extensions/rainbowBracket/rainbowBracketsExtension.ts similarity index 98% rename from frontend/src/views/editor/extensions/rainbowBrackets.ts rename to frontend/src/views/editor/extensions/rainbowBracket/rainbowBracketsExtension.ts index 0737bee..77bae48 100644 --- a/frontend/src/views/editor/extensions/rainbowBrackets.ts +++ b/frontend/src/views/editor/extensions/rainbowBracket/rainbowBracketsExtension.ts @@ -70,7 +70,7 @@ const rainbowBracketsPlugin = ViewPlugin.fromClass(RainbowBracketsView, { decorations: (v) => v.decorations, }); -export default function rainbowBrackets() { +export default function rainbowBracketsExtension() { return [ rainbowBracketsPlugin, EditorView.baseTheme({ diff --git a/frontend/src/views/editor/extensions/textHighlightExtension.ts b/frontend/src/views/editor/extensions/textHighlight/textHighlightExtension.ts similarity index 100% rename from frontend/src/views/editor/extensions/textHighlightExtension.ts rename to frontend/src/views/editor/extensions/textHighlight/textHighlightExtension.ts diff --git a/frontend/src/views/editor/extensions/keymap/commandRegistry.ts b/frontend/src/views/editor/keymap/commandRegistry.ts similarity index 90% rename from frontend/src/views/editor/extensions/keymap/commandRegistry.ts rename to frontend/src/views/editor/keymap/commandRegistry.ts index 55069e3..ab811f8 100644 --- a/frontend/src/views/editor/extensions/keymap/commandRegistry.ts +++ b/frontend/src/views/editor/keymap/commandRegistry.ts @@ -7,7 +7,7 @@ import { searchToggleRegex, searchToggleWholeWord, showSearchVisibilityCommand -} from '../vscodeSearch/commands' +} from '../extensions/vscodeSearch/commands' import { addNewBlockAfterCurrent, addNewBlockAfterLast, @@ -20,14 +20,39 @@ import { moveCurrentBlockUp, selectNextBlock, selectPreviousBlock -} from '../codeblock/commands' -import { selectAll } from '../codeblock/selectAll' -import { deleteLineCommand } from '../codeblock/deleteLine' -import { moveLineUp, moveLineDown } from '../codeblock/moveLines' -import { transposeChars } from '@/views/editor/extensions' -import { copyCommand, cutCommand, pasteCommand } from '../codeblock/copyPaste' -import { undo, redo, undoSelection, redoSelection, cursorSyntaxLeft, cursorSyntaxRight, selectSyntaxLeft, selectSyntaxRight, copyLineUp, copyLineDown, insertBlankLine, selectLine, selectParentSyntax, indentLess, indentMore, indentSelection, cursorMatchingBracket, toggleComment, toggleBlockComment, insertNewlineAndIndent, deleteCharBackward, deleteCharForward, deleteGroupBackward, deleteGroupForward } from '@codemirror/commands' -import { foldCode, unfoldCode, foldAll, unfoldAll } from '@codemirror/language' +} from '../extensions/codeblock/commands' +import {selectAll} from '../extensions/codeblock/selectAll' +import {deleteLineCommand} from '../extensions/codeblock/deleteLine' +import {moveLineDown, moveLineUp} from '../extensions/codeblock/moveLines' +import {transposeChars} from '../extensions/codeblock' +import {copyCommand, cutCommand, pasteCommand} from '../extensions/codeblock/copyPaste' +import { + copyLineDown, + copyLineUp, + cursorMatchingBracket, + cursorSyntaxLeft, + cursorSyntaxRight, + deleteCharBackward, + deleteCharForward, + deleteGroupBackward, + deleteGroupForward, + indentLess, + indentMore, + indentSelection, + insertBlankLine, + insertNewlineAndIndent, + redo, + redoSelection, + selectLine, + selectParentSyntax, + selectSyntaxLeft, + selectSyntaxRight, + toggleBlockComment, + toggleComment, + undo, + undoSelection +} from '@codemirror/commands' +import {foldAll, foldCode, unfoldAll, unfoldCode} from '@codemirror/language' import i18n from '@/i18n' // 默认编辑器选项 diff --git a/frontend/src/views/editor/extensions/keymap/index.ts b/frontend/src/views/editor/keymap/index.ts similarity index 100% rename from frontend/src/views/editor/extensions/keymap/index.ts rename to frontend/src/views/editor/keymap/index.ts diff --git a/frontend/src/views/editor/extensions/keymap/keymapManager.ts b/frontend/src/views/editor/keymap/keymapManager.ts similarity index 100% rename from frontend/src/views/editor/extensions/keymap/keymapManager.ts rename to frontend/src/views/editor/keymap/keymapManager.ts diff --git a/frontend/src/views/editor/extensions/keymap/types.ts b/frontend/src/views/editor/keymap/types.ts similarity index 100% rename from frontend/src/views/editor/extensions/keymap/types.ts rename to frontend/src/views/editor/keymap/types.ts diff --git a/frontend/src/views/editor/manager/ExtensionManager.ts b/frontend/src/views/editor/manager/ExtensionManager.ts new file mode 100644 index 0000000..b227854 --- /dev/null +++ b/frontend/src/views/editor/manager/ExtensionManager.ts @@ -0,0 +1,264 @@ +import { Compartment, Extension, StateEffect } from '@codemirror/state' +import { EditorView } from '@codemirror/view' +import { ExtensionID, Extension as ExtensionConfig } 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 +} + +/** + * 扩展区间信息 + */ +interface ExtensionCompartment { + id: ExtensionID + compartment: Compartment + factory: ExtensionFactory + currentConfig?: any + enabled: boolean +} + +/** + * 扩展管理器 + * 负责管理所有动态扩展的注册、启用、禁用和配置更新 + */ +export class ExtensionManager { + private view: EditorView | null = null + private compartments = new Map() + private extensionFactories = new Map() + + /** + * 注册扩展工厂 + * @param id 扩展ID + * @param factory 扩展工厂 + */ + registerExtension(id: ExtensionID, factory: ExtensionFactory): void { + this.extensionFactories.set(id, factory) + this.compartments.set(id, { + id, + compartment: new Compartment(), + factory, + currentConfig: factory.getDefaultConfig(), + enabled: false + }) + } + + /** + * 获取所有注册的扩展ID列表 + */ + getRegisteredExtensions(): ExtensionID[] { + return Array.from(this.extensionFactories.keys()) + } + + /** + * 检查扩展是否已注册 + * @param id 扩展ID + */ + isExtensionRegistered(id: ExtensionID): boolean { + return this.extensionFactories.has(id) + } + + /** + * 根据后端配置获取初始扩展数组 + * @param extensionConfigs 后端扩展配置列表 + * @returns CodeMirror扩展数组 + */ + getInitialExtensions(extensionConfigs: ExtensionConfig[]): Extension[] { + const extensions: Extension[] = [] + + for (const config of extensionConfigs) { + const compartmentInfo = this.compartments.get(config.id) + if (!compartmentInfo) { + console.warn(`Extension ${config.id} is not registered`) + continue + } + + // 验证配置 + if (compartmentInfo.factory.validateConfig && + !compartmentInfo.factory.validateConfig(config.config)) { + console.warn(`Invalid config for extension ${config.id}`) + continue + } + + try { + const extension = config.enabled + ? compartmentInfo.factory.create(config.config) + : [] // 空扩展表示禁用 + + extensions.push(compartmentInfo.compartment.of(extension)) + + // 更新状态 + compartmentInfo.currentConfig = config.config + compartmentInfo.enabled = config.enabled + } catch (error) { + console.error(`Failed to create extension ${config.id}:`, error) + } + } + + return extensions + } + + /** + * 设置编辑器视图 + * @param view 编辑器视图实例 + */ + setView(view: EditorView): void { + this.view = view + } + + /** + * 动态更新单个扩展 + * @param id 扩展ID + * @param enabled 是否启用 + * @param config 扩展配置 + */ + updateExtension(id: ExtensionID, enabled: boolean, config: any = {}): void { + if (!this.view) { + console.warn('Editor view not set') + return + } + + const compartmentInfo = this.compartments.get(id) + if (!compartmentInfo) { + console.warn(`Extension ${id} is not registered`) + return + } + + try { + // 验证配置 + if (compartmentInfo.factory.validateConfig && + !compartmentInfo.factory.validateConfig(config)) { + console.warn(`Invalid config for extension ${id}`) + return + } + + const extension = enabled + ? compartmentInfo.factory.create(config) + : [] + + this.view.dispatch({ + effects: compartmentInfo.compartment.reconfigure(extension) + }) + + // 更新状态 + compartmentInfo.currentConfig = config + compartmentInfo.enabled = enabled + } catch (error) { + console.error(`Failed to update extension ${id}:`, error) + } + } + + /** + * 批量更新扩展 + * @param updates 更新配置数组 + */ + updateExtensions(updates: Array<{ + id: ExtensionID + enabled: boolean + config: any + }>): void { + if (!this.view) { + console.warn('Editor view not set') + return + } + + const effects: StateEffect[] = [] + + for (const update of updates) { + const compartmentInfo = this.compartments.get(update.id) + if (!compartmentInfo) { + console.warn(`Extension ${update.id} is not registered`) + continue + } + + try { + // 验证配置 + if (compartmentInfo.factory.validateConfig && + !compartmentInfo.factory.validateConfig(update.config)) { + console.warn(`Invalid config for extension ${update.id}`) + continue + } + + const extension = update.enabled + ? compartmentInfo.factory.create(update.config) + : [] + + effects.push(compartmentInfo.compartment.reconfigure(extension)) + + // 更新状态 + compartmentInfo.currentConfig = update.config + compartmentInfo.enabled = update.enabled + } catch (error) { + console.error(`Failed to update extension ${update.id}:`, error) + } + } + + if (effects.length > 0) { + this.view.dispatch({ effects }) + } + } + + /** + * 获取扩展当前状态 + * @param id 扩展ID + */ + getExtensionState(id: ExtensionID): { + enabled: boolean + config: any + } | null { + const compartmentInfo = this.compartments.get(id) + if (!compartmentInfo) { + return null + } + + return { + enabled: compartmentInfo.enabled, + config: compartmentInfo.currentConfig + } + } + + /** + * 重置扩展到默认配置 + * @param id 扩展ID + */ + resetExtensionToDefault(id: ExtensionID): void { + const compartmentInfo = this.compartments.get(id) + if (!compartmentInfo) { + console.warn(`Extension ${id} is not registered`) + return + } + + const defaultConfig = compartmentInfo.factory.getDefaultConfig() + this.updateExtension(id, true, defaultConfig) + } + + /** + * 销毁管理器 + */ + destroy(): void { + this.view = null + this.compartments.clear() + this.extensionFactories.clear() + } +} \ No newline at end of file diff --git a/frontend/src/views/editor/manager/factories.ts b/frontend/src/views/editor/manager/factories.ts new file mode 100644 index 0000000..e281472 --- /dev/null +++ b/frontend/src/views/editor/manager/factories.ts @@ -0,0 +1,296 @@ +import {ExtensionFactory, ExtensionManager} from './ExtensionManager' +import {ExtensionID} from '@/../bindings/voidraft/internal/models/models' + +// 导入现有扩展的创建函数 +import rainbowBracketsExtension from '../extensions/rainbowBracket/rainbowBracketsExtension' +import {createTextHighlighter} from '../extensions/textHighlight/textHighlightExtension' +import {createCodeBlastExtension} from '../extensions/codeblast' +import {color} from '../extensions/colorSelector' +import {hyperLink} from '../extensions/hyperlink' +import {minimap} from '../extensions/minimap' +import {vscodeSearch} from '../extensions/vscodeSearch' +import {createCodeBlockExtension} from '../extensions/codeblock' +import {foldingOnIndent} from '../extensions/fold/foldExtension' + +/** + * 彩虹括号扩展工厂 + */ +export const rainbowBracketsFactory: ExtensionFactory = { + create(config: any) { + return rainbowBracketsExtension() + }, + getDefaultConfig() { + return {} + }, + validateConfig(config: any) { + return typeof config === 'object' + } +} + +/** + * 文本高亮扩展工厂 + */ +export const textHighlightFactory: ExtensionFactory = { + create(config: any) { + return createTextHighlighter('default') + }, + getDefaultConfig() { + return { + highlightClass: 'hl' + } + }, + validateConfig(config: any) { + return typeof config === 'object' && + (!config.highlightClass || typeof config.highlightClass === 'string') + } +} + +/** + * 小地图扩展工厂 + */ +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 codeBlastFactory: ExtensionFactory = { + create(config: any) { + const options = { + effect: config.effect || 1, + shake: config.shake !== false, + maxParticles: config.maxParticles || 300, + shakeIntensity: config.shakeIntensity || 3 + } + return createCodeBlastExtension(options) + }, + getDefaultConfig() { + return { + effect: 1, + shake: true, + maxParticles: 300, + shakeIntensity: 3 + } + }, + validateConfig(config: any) { + return typeof config === 'object' && + (!config.effect || [1, 2].includes(config.effect)) && + (!config.shake || typeof config.shake === 'boolean') && + (!config.maxParticles || typeof config.maxParticles === 'number') && + (!config.shakeIntensity || typeof config.shakeIntensity === 'number') + } +} + +/** + * 超链接扩展工厂 + */ +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 codeBlockFactory: ExtensionFactory = { + create(config: any) { + const options = { + showBackground: config.showBackground !== false, + enableAutoDetection: config.enableAutoDetection !== false + } + return createCodeBlockExtension(options) + }, + getDefaultConfig() { + return { + showBackground: true, + enableAutoDetection: true + } + }, + validateConfig(config: any) { + return typeof config === 'object' && + (!config.showBackground || typeof config.showBackground === 'boolean') && + (!config.enableAutoDetection || typeof config.enableAutoDetection === 'boolean') + } +} + +export const foldFactory: ExtensionFactory = { + create(config: any) { + return foldingOnIndent; + }, + getDefaultConfig(): any { + return {} + }, + validateConfig(config: any): boolean { + return typeof config === 'object' + } +} + +/** + * 所有扩展的统一配置 + * 排除$zero值以避免TypeScript类型错误 + */ +const EXTENSION_CONFIGS = { + + // 编辑增强扩展 + [ExtensionID.ExtensionRainbowBrackets]: { + factory: rainbowBracketsFactory, + displayName: '彩虹括号', + description: '用不同颜色显示嵌套括号' + }, + [ExtensionID.ExtensionHyperlink]: { + factory: hyperlinkFactory, + displayName: '超链接', + description: '识别并可点击超链接' + }, + [ExtensionID.ExtensionColorSelector]: { + factory: colorSelectorFactory, + displayName: '颜色选择器', + description: '颜色值的可视化和选择' + }, + + // UI增强扩展 + [ExtensionID.ExtensionMinimap]: { + factory: minimapFactory, + displayName: '小地图', + description: '显示小地图视图' + }, + [ExtensionID.ExtensionCodeBlast]: { + factory: codeBlastFactory, + displayName: '爆炸效果', + description: '编写时的动画效果' + }, + + // 工具扩展 + [ExtensionID.ExtensionSearch]: { + factory: searchFactory, + displayName: '搜索功能', + description: '文本搜索和替换功能' + }, + [ExtensionID.ExtensionCodeBlock]: { + factory: codeBlockFactory, + displayName: '代码块', + description: '代码块语法高亮和格式化' + }, + [ExtensionID.ExtensionFold]: { + factory: foldFactory, + displayName: '折叠', + description: '折叠' + }, + [ExtensionID.ExtensionTextHighlight]:{ + factory: textHighlightFactory, + displayName: '文本高亮', + description: '文本高亮' + } +} + +/** + * 注册所有扩展工厂到管理器 + * @param manager 扩展管理器实例 + */ +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 { + if (id === ExtensionID.$zero) return '' + return EXTENSION_CONFIGS[id as Exclude]?.displayName || id +} + +/** + * 获取扩展工厂的描述 + * @param id 扩展ID + * @returns 描述 + */ +export function getExtensionDescription(id: ExtensionID): string { + if (id === ExtensionID.$zero) return '' + return EXTENSION_CONFIGS[id as Exclude]?.description || '未知扩展' +} + +/** + * 获取扩展的完整信息 + * @param id 扩展ID + * @returns 扩展信息对象 + */ +export function getExtensionInfo(id: ExtensionID): { displayName: string; description: string } | null { + if (id === ExtensionID.$zero) return null + const config = EXTENSION_CONFIGS[id as Exclude] + if (!config) return null + + return { + displayName: config.displayName, + description: config.description + } +} + +/** + * 获取所有可用扩展的ID列表 + * @returns 扩展ID数组 + */ +export function getAllExtensionIds(): ExtensionID[] { + return Object.keys(EXTENSION_CONFIGS) as ExtensionID[] +} \ No newline at end of file diff --git a/frontend/src/views/editor/manager/index.ts b/frontend/src/views/editor/manager/index.ts new file mode 100644 index 0000000..dbb543b --- /dev/null +++ b/frontend/src/views/editor/manager/index.ts @@ -0,0 +1,51 @@ +import {Extension} from '@codemirror/state' +import {useExtensionStore} from '@/stores/extensionStore' +import {ExtensionManager} from './ExtensionManager' +import {registerAllExtensions} from './factories' + +/** + * 全局扩展管理器实例 + */ +const extensionManager = new ExtensionManager() + +/** + * 异步创建动态扩展 + * 确保扩展配置已加载 + */ +export const createDynamicExtensions = async (): Promise => { + const extensionStore = useExtensionStore() + + // 注册所有扩展工厂 + registerAllExtensions(extensionManager) + + // 确保扩展配置已加载 + if (extensionStore.extensions.length === 0) { + await extensionStore.loadExtensions() + } + + // 获取启用的扩展配置 + const enabledExtensions = extensionStore.enabledExtensions + + return extensionManager.getInitialExtensions(enabledExtensions) +} + +/** + * 获取扩展管理器实例 + * @returns 扩展管理器 + */ +export const getExtensionManager = (): ExtensionManager => { + return extensionManager +} + +/** + * 设置编辑器视图到扩展管理器 + * @param view 编辑器视图 + */ +export const setExtensionManagerView = (view: any): void => { + extensionManager.setView(view) +} + +// 导出相关模块 +export {ExtensionManager} from './ExtensionManager' +export {registerAllExtensions, getExtensionDisplayName, getExtensionDescription} from './factories' +export type {ExtensionFactory} from './ExtensionManager' \ No newline at end of file diff --git a/frontend/src/views/settings/pages/KeyBindingsPage.vue b/frontend/src/views/settings/pages/KeyBindingsPage.vue index ef4d5a4..f1be592 100644 --- a/frontend/src/views/settings/pages/KeyBindingsPage.vue +++ b/frontend/src/views/settings/pages/KeyBindingsPage.vue @@ -4,7 +4,7 @@ import { onMounted, computed } from 'vue'; import SettingSection from '../components/SettingSection.vue'; import { useKeybindingStore } from '@/stores/keybindingStore'; import { useSystemStore } from '@/stores/systemStore'; -import { getCommandDescription } from '@/views/editor/extensions/keymap/commandRegistry'; +import { getCommandDescription } from '@/views/editor/keymap/commandRegistry'; import {KeyBindingCommand} from "@/../bindings/voidraft/internal/models"; const { t } = useI18n(); diff --git a/internal/models/extensions.go b/internal/models/extensions.go new file mode 100644 index 0000000..c0c1d9b --- /dev/null +++ b/internal/models/extensions.go @@ -0,0 +1,220 @@ +package models + +import "time" + +// Extension 单个扩展配置 +type Extension struct { + ID ExtensionID `json:"id"` // 扩展唯一标识 + Category ExtensionCategory `json:"category"` // 扩展分类 + Enabled bool `json:"enabled"` // 是否启用 + IsDefault bool `json:"isDefault"` // 是否为默认扩展 + Config ExtensionConfig `json:"config"` // 扩展配置项 +} + +// ExtensionID 扩展标识符 +type ExtensionID string + +const ( + // 编辑增强扩展 + ExtensionRainbowBrackets ExtensionID = "rainbowBrackets" // 彩虹括号 + ExtensionHyperlink ExtensionID = "hyperlink" // 超链接 + ExtensionColorSelector ExtensionID = "colorSelector" // 颜色选择器 + ExtensionFold ExtensionID = "fold" + ExtensionTextHighlight ExtensionID = "textHighlight" + + // UI增强扩展 + ExtensionMinimap ExtensionID = "minimap" // 小地图 + ExtensionCodeBlast ExtensionID = "codeBlast" // 代码爆炸效果 + + // 工具扩展 + ExtensionSearch ExtensionID = "search" // 搜索功能 + ExtensionCodeBlock ExtensionID = "codeBlock" // 代码块 +) + +// ExtensionCategory 扩展分类 +type ExtensionCategory string + +const ( + CategoryEditing ExtensionCategory = "editing" // 编辑增强 + CategoryUI ExtensionCategory = "ui" // 界面增强 + CategoryTools ExtensionCategory = "tools" // 工具类 +) + +// ExtensionConfig 扩展配置项(动态配置) +type ExtensionConfig map[string]interface{} + +// ExtensionMetadata 扩展配置元数据 +type ExtensionMetadata struct { + Version string `json:"version"` // 配置版本 + LastUpdated string `json:"lastUpdated"` // 最后更新时间 +} + +// ExtensionSettings 扩展设置配置 +type ExtensionSettings struct { + Extensions []Extension `json:"extensions"` // 扩展列表 + Metadata ExtensionMetadata `json:"metadata"` // 配置元数据 +} + +// NewDefaultExtensionSettings 创建默认扩展配置 +func NewDefaultExtensionSettings() *ExtensionSettings { + return &ExtensionSettings{ + Extensions: NewDefaultExtensions(), + Metadata: ExtensionMetadata{ + Version: "1.0.0", + LastUpdated: time.Now().Format(time.RFC3339), + }, + } +} + +// NewDefaultExtensions 创建默认扩展配置 +func NewDefaultExtensions() []Extension { + return []Extension{ + // 编辑增强扩展 + { + ID: ExtensionRainbowBrackets, + Category: CategoryEditing, + Enabled: true, + IsDefault: true, + Config: ExtensionConfig{}, + }, + { + ID: ExtensionHyperlink, + Category: CategoryEditing, + Enabled: true, + IsDefault: true, + Config: ExtensionConfig{}, + }, + { + ID: ExtensionColorSelector, + Category: CategoryEditing, + Enabled: true, + IsDefault: true, + Config: ExtensionConfig{}, + }, + + // UI增强扩展 + { + ID: ExtensionMinimap, + Category: CategoryUI, + Enabled: true, + IsDefault: true, + Config: ExtensionConfig{ + "displayText": "characters", + "showOverlay": "always", + "autohide": false, + }, + }, + { + ID: ExtensionCodeBlast, + Category: CategoryUI, + Enabled: true, + IsDefault: true, + Config: ExtensionConfig{ + "effect": 1, + "shake": true, + "maxParticles": 300, + "shakeIntensity": 3, + }, + }, + + // 工具扩展 + { + ID: ExtensionSearch, + Category: CategoryTools, + Enabled: true, + IsDefault: true, + Config: ExtensionConfig{}, + }, + { + ID: ExtensionCodeBlock, + Category: CategoryTools, + Enabled: true, + IsDefault: true, + Config: ExtensionConfig{ + "showBackground": true, + "enableAutoDetection": true, + }, + }, + { + ID: ExtensionFold, + Category: CategoryEditing, + Enabled: true, + IsDefault: true, + Config: ExtensionConfig{}, + }, + { + ID: ExtensionTextHighlight, + Category: CategoryEditing, + Enabled: true, + IsDefault: true, + Config: ExtensionConfig{}, + }, + } +} + +// GetVersion 获取配置版本 +func (es *ExtensionSettings) GetVersion() string { + return es.Metadata.Version +} + +// SetVersion 设置配置版本 +func (es *ExtensionSettings) SetVersion(version string) { + es.Metadata.Version = version +} + +// SetLastUpdated 设置最后更新时间 +func (es *ExtensionSettings) SetLastUpdated(timeStr string) { + es.Metadata.LastUpdated = timeStr +} + +// GetDefaultConfig 获取默认配置 +func (es *ExtensionSettings) GetDefaultConfig() any { + return NewDefaultExtensionSettings() +} + +// GetExtensionByID 根据ID获取扩展 +func (es *ExtensionSettings) GetExtensionByID(id ExtensionID) *Extension { + for i := range es.Extensions { + if es.Extensions[i].ID == id { + return &es.Extensions[i] + } + } + return nil +} + +// GetEnabledExtensions 获取所有启用的扩展 +func (es *ExtensionSettings) GetEnabledExtensions() []Extension { + var enabled []Extension + for _, ext := range es.Extensions { + if ext.Enabled { + enabled = append(enabled, ext) + } + } + return enabled +} + +// GetExtensionsByCategory 根据分类获取扩展 +func (es *ExtensionSettings) GetExtensionsByCategory(category ExtensionCategory) []Extension { + var extensions []Extension + for _, ext := range es.Extensions { + if ext.Category == category { + extensions = append(extensions, ext) + } + } + return extensions +} + +// UpdateExtension 更新扩展配置 +func (es *ExtensionSettings) UpdateExtension(id ExtensionID, enabled bool, config ExtensionConfig) bool { + for i := range es.Extensions { + if es.Extensions[i].ID == id { + es.Extensions[i].Enabled = enabled + if config != nil { + es.Extensions[i].Config = config + } + es.SetLastUpdated(time.Now().Format(time.RFC3339)) + return true + } + } + return false +} diff --git a/internal/services/config_migration_service.go b/internal/services/config_migration_service.go index d92cf2a..e5fd67c 100644 --- a/internal/services/config_migration_service.go +++ b/internal/services/config_migration_service.go @@ -22,6 +22,9 @@ const ( CurrentAppConfigVersion = "1.0.0" // CurrentKeyBindingConfigVersion 当前快捷键配置版本 CurrentKeyBindingConfigVersion = "1.0.0" + + CurrentExtensionConfigVersion = "1.0.0" + // BackupFilePattern 备份文件名模式 BackupFilePattern = "%s.backup.%s.json" @@ -323,3 +326,9 @@ func NewKeyBindingMigrationService(logger *log.LoggerService, pathManager *PathM return NewConfigMigrationService[*models.KeyBindingConfig]( logger, pathManager, "keybindings", CurrentKeyBindingConfigVersion, pathManager.GetKeybindsPath()) } + +// NewExtensionMigrationService 创建扩展配置迁移服务 +func NewExtensionMigrationService(logger *log.LoggerService, pathManager *PathManager) *ConfigMigrationService[*models.ExtensionSettings] { + return NewConfigMigrationService[*models.ExtensionSettings]( + logger, pathManager, "extensions", CurrentExtensionConfigVersion, pathManager.GetExtensionsPath()) +} diff --git a/internal/services/extension_service.go b/internal/services/extension_service.go new file mode 100644 index 0000000..47ea367 --- /dev/null +++ b/internal/services/extension_service.go @@ -0,0 +1,278 @@ +package services + +import ( + "context" + "errors" + "fmt" + "os" + "sync" + "voidraft/internal/models" + + jsonparser "github.com/knadh/koanf/parsers/json" + "github.com/knadh/koanf/providers/file" + "github.com/knadh/koanf/providers/structs" + "github.com/knadh/koanf/v2" + "github.com/wailsapp/wails/v3/pkg/services/log" +) + +// ExtensionService 扩展管理服务 +type ExtensionService struct { + koanf *koanf.Koanf + logger *log.LoggerService + pathManager *PathManager + fileProvider *file.File + + mu sync.RWMutex + ctx context.Context + cancel context.CancelFunc + initOnce sync.Once + + // 配置迁移服务 + migrationService *ConfigMigrationService[*models.ExtensionSettings] +} + +// ExtensionError 扩展错误 +type ExtensionError struct { + Operation string + Extension string + Err error +} + +func (e *ExtensionError) Error() string { + if e.Extension != "" { + return fmt.Sprintf("extension %s for %s: %v", e.Operation, e.Extension, e.Err) + } + return fmt.Sprintf("extension %s: %v", e.Operation, e.Err) +} + +func (e *ExtensionError) Unwrap() error { + return e.Err +} + +func (e *ExtensionError) Is(target error) bool { + var extensionError *ExtensionError + return errors.As(target, &extensionError) +} + +// NewExtensionService 创建扩展服务实例 +func NewExtensionService(logger *log.LoggerService, pathManager *PathManager) *ExtensionService { + if logger == nil { + logger = log.New() + } + if pathManager == nil { + pathManager = NewPathManager() + } + + ctx, cancel := context.WithCancel(context.Background()) + + k := koanf.New(".") + + migrationService := NewExtensionMigrationService(logger, pathManager) + + service := &ExtensionService{ + koanf: k, + logger: logger, + pathManager: pathManager, + ctx: ctx, + cancel: cancel, + migrationService: migrationService, + } + + // 异步初始化 + go service.initialize() + + return service +} + +// initialize 初始化配置 +func (es *ExtensionService) initialize() { + es.initOnce.Do(func() { + if err := es.initConfig(); err != nil { + es.logger.Error("failed to initialize extension config", "error", err) + } + }) +} + +// setDefaults 设置默认值 +func (es *ExtensionService) setDefaults() error { + defaultConfig := models.NewDefaultExtensionSettings() + + if err := es.koanf.Load(structs.Provider(defaultConfig, "json"), nil); err != nil { + return &ExtensionError{"load_defaults", "", err} + } + + return nil +} + +// initConfig 初始化配置 +func (es *ExtensionService) initConfig() error { + es.mu.Lock() + defer es.mu.Unlock() + + // 检查配置文件是否存在 + configPath := es.pathManager.GetExtensionsPath() + if _, err := os.Stat(configPath); os.IsNotExist(err) { + return es.createDefaultConfig() + } + + // 配置文件存在,先加载现有配置 + es.fileProvider = file.Provider(configPath) + if err := es.koanf.Load(es.fileProvider, jsonparser.Parser()); err != nil { + return &ExtensionError{"load_config_file", "", err} + } + + // 检查并执行配置迁移 + if es.migrationService != nil { + result, err := es.migrationService.MigrateConfig(es.koanf) + if err != nil { + return &ExtensionError{"migrate_config", "", err} + } + + if result.Migrated && result.ConfigUpdated { + // 迁移完成且配置已更新,重新创建文件提供器以监听新文件 + es.fileProvider = file.Provider(configPath) + } + } + + return nil +} + +// createDefaultConfig 创建默认配置文件 +func (es *ExtensionService) createDefaultConfig() error { + if err := es.pathManager.EnsureConfigDir(); err != nil { + return &ExtensionError{"create_config_dir", "", err} + } + + if err := es.setDefaults(); err != nil { + return err + } + + configBytes, err := es.koanf.Marshal(jsonparser.Parser()) + if err != nil { + return &ExtensionError{"marshal_config", "", err} + } + + if err := os.WriteFile(es.pathManager.GetExtensionsPath(), configBytes, 0644); err != nil { + return &ExtensionError{"write_config", "", err} + } + + // 创建文件提供器 + es.fileProvider = file.Provider(es.pathManager.GetExtensionsPath()) + if err = es.koanf.Load(es.fileProvider, jsonparser.Parser()); err != nil { + return err + } + return nil +} + +// saveConfig 保存配置到文件 +func (es *ExtensionService) saveExtensionConfig() error { + configBytes, err := es.koanf.Marshal(jsonparser.Parser()) + if err != nil { + return &ExtensionError{"marshal_config", "", err} + } + + if err := os.WriteFile(es.pathManager.GetExtensionsPath(), configBytes, 0644); err != nil { + return &ExtensionError{"write_config", "", err} + } + + return nil +} + +// GetExtensionSettings 获取完整扩展配置 +func (es *ExtensionService) GetExtensionSettings() (*models.ExtensionSettings, error) { + es.mu.RLock() + defer es.mu.RUnlock() + + var settings models.ExtensionSettings + if err := es.koanf.Unmarshal("", &settings); err != nil { + return nil, &ExtensionError{"unmarshal_config", "", err} + } + return &settings, nil +} + +// GetAllExtensions 获取所有扩展配置 +func (es *ExtensionService) GetAllExtensions() ([]models.Extension, error) { + settings, err := es.GetExtensionSettings() + if err != nil { + return nil, err + } + return settings.Extensions, nil +} + +// EnableExtension 启用扩展 +func (es *ExtensionService) EnableExtension(id models.ExtensionID) error { + return es.UpdateExtensionState(id, true, nil) +} + +// DisableExtension 禁用扩展 +func (es *ExtensionService) DisableExtension(id models.ExtensionID) error { + return es.UpdateExtensionState(id, false, nil) +} + +// UpdateExtensionState 更新扩展状态 +func (es *ExtensionService) UpdateExtensionState(id models.ExtensionID, enabled bool, config models.ExtensionConfig) error { + es.mu.Lock() + defer es.mu.Unlock() + + // 获取当前配置 + var settings models.ExtensionSettings + if err := es.koanf.Unmarshal("", &settings); err != nil { + return &ExtensionError{"unmarshal_config", string(id), err} + } + + // 更新扩展状态 + if !settings.UpdateExtension(id, enabled, config) { + return &ExtensionError{"extension_not_found", string(id), nil} + } + + // 重新加载到koanf + if err := es.koanf.Load(structs.Provider(&settings, "json"), nil); err != nil { + return &ExtensionError{"reload_config", string(id), err} + } + + // 保存到文件 + if err := es.saveExtensionConfig(); err != nil { + return &ExtensionError{"save_config", string(id), err} + } + + es.logger.Info("extension state updated", "id", id, "enabled", enabled) + return nil +} + +// ResetExtensionToDefault 重置扩展到默认状态 +func (es *ExtensionService) ResetExtensionToDefault(id models.ExtensionID) error { + // 获取默认配置 + defaultSettings := models.NewDefaultExtensionSettings() + defaultExtension := defaultSettings.GetExtensionByID(id) + if defaultExtension == nil { + return &ExtensionError{"default_extension_not_found", string(id), nil} + } + + return es.UpdateExtensionState(id, defaultExtension.Enabled, defaultExtension.Config) +} + +// ResetAllExtensionsToDefault 重置所有扩展到默认状态 +func (es *ExtensionService) ResetAllExtensionsToDefault() error { + es.mu.Lock() + defer es.mu.Unlock() + + // 加载默认配置 + defaultSettings := models.NewDefaultExtensionSettings() + if err := es.koanf.Load(structs.Provider(defaultSettings, "json"), nil); err != nil { + return &ExtensionError{"load_defaults", "", err} + } + + // 保存到文件 + if err := es.saveExtensionConfig(); err != nil { + return &ExtensionError{"save_config", "", err} + } + + es.logger.Info("all extensions reset to default") + return nil +} + +// ServiceShutdown 关闭服务 +func (es *ExtensionService) ServiceShutdown() error { + es.cancel() + return nil +} diff --git a/internal/services/path_manager.go b/internal/services/path_manager.go index 80735e3..1f823e3 100644 --- a/internal/services/path_manager.go +++ b/internal/services/path_manager.go @@ -7,9 +7,10 @@ import ( // PathManager 路径管理器 type PathManager struct { - configDir string // 配置目录 - settingsPath string // 设置文件路径 - keybindsPath string // 快捷键配置文件路径 + configDir string // 配置目录 + settingsPath string // 设置文件路径 + keybindsPath string // 快捷键配置文件路径 + extensionsPath string // 扩展配置文件路径 } // NewPathManager 创建新的路径管理器 @@ -25,9 +26,10 @@ func NewPathManager() *PathManager { configDir := filepath.Join(userConfigDir, ".voidraft", "config") return &PathManager{ - configDir: configDir, - settingsPath: filepath.Join(configDir, "settings.json"), - keybindsPath: filepath.Join(configDir, "keybindings.json"), + configDir: configDir, + settingsPath: filepath.Join(configDir, "settings.json"), + keybindsPath: filepath.Join(configDir, "keybindings.json"), + extensionsPath: filepath.Join(configDir, "extensions.json"), } } @@ -46,6 +48,11 @@ func (pm *PathManager) GetConfigDir() string { return pm.configDir } +// GetExtensionsPath 获取扩展配置文件路径 +func (pm *PathManager) GetExtensionsPath() string { + return pm.extensionsPath +} + // EnsureConfigDir 确保配置目录存在 func (pm *PathManager) EnsureConfigDir() error { return os.MkdirAll(pm.configDir, 0755) diff --git a/internal/services/service_manager.go b/internal/services/service_manager.go index 982ccf3..464117f 100644 --- a/internal/services/service_manager.go +++ b/internal/services/service_manager.go @@ -18,6 +18,7 @@ type ServiceManager struct { dialogService *DialogService trayService *TrayService keyBindingService *KeyBindingService + extensionService *ExtensionService startupService *StartupService updateService *UpdateService logger *log.LoggerService @@ -55,6 +56,9 @@ func NewServiceManager() *ServiceManager { // 初始化快捷键服务 keyBindingService := NewKeyBindingService(logger, pathManager) + // 初始化扩展服务 + extensionService := NewExtensionService(logger, pathManager) + // 初始化开机启动服务 startupService := NewStartupService(configService, logger) @@ -92,6 +96,7 @@ func NewServiceManager() *ServiceManager { dialogService: dialogService, trayService: trayService, keyBindingService: keyBindingService, + extensionService: extensionService, startupService: startupService, updateService: updateService, logger: logger, @@ -109,6 +114,7 @@ func (sm *ServiceManager) GetServices() []application.Service { application.NewService(sm.dialogService), application.NewService(sm.trayService), application.NewService(sm.keyBindingService), + application.NewService(sm.extensionService), application.NewService(sm.startupService), application.NewService(sm.updateService), } @@ -149,6 +155,11 @@ func (sm *ServiceManager) GetStartupService() *StartupService { return sm.startupService } +// GetExtensionService 获取扩展服务实例 +func (sm *ServiceManager) GetExtensionService() *ExtensionService { + return sm.extensionService +} + // GetUpdateService 获取更新服务实例 func (sm *ServiceManager) GetUpdateService() *UpdateService { return sm.updateService