🚧 Refactor basic services

This commit is contained in:
2025-12-14 02:19:50 +08:00
parent d16905c0a3
commit cc4c2189dc
126 changed files with 18164 additions and 4247 deletions

View File

@@ -1,7 +1,7 @@
import { EditorView } from '@codemirror/view';
import { Extension } from '@codemirror/state';
import { copyCommand, cutCommand, pasteCommand } from '../codeblock/copyPaste';
import { KeyBindingCommand } from '../../../../../bindings/voidraft/internal/models/models';
import { KeyBindingKey } from '@/../bindings/voidraft/internal/models/models';
import { useKeybindingStore } from '@/stores/keybindingStore';
import { undo, redo } from '@codemirror/commands';
import i18n from '@/i18n';
@@ -32,53 +32,54 @@ function formatKeyBinding(keyBinding: string): string {
.replace(/-/g, " + ");
}
const shortcutCache = new Map<KeyBindingCommand, string>();
const shortcutCache = new Map<KeyBindingKey, string>();
function getShortcutText(command?: KeyBindingCommand): string {
if (command === undefined) {
function getShortcutText(keyBindingKey?: KeyBindingKey): string {
if (keyBindingKey === undefined) {
return "";
}
const cached = shortcutCache.get(command);
const cached = shortcutCache.get(keyBindingKey);
if (cached !== undefined) {
return cached;
}
try {
const keybindingStore = useKeybindingStore();
// binding.key 是命令标识符binding.command 是快捷键组合
const binding = keybindingStore.keyBindings.find(
(kb) => kb.command === command && kb.enabled
(kb) => kb.key === keyBindingKey && kb.enabled
);
if (binding?.key) {
const formatted = formatKeyBinding(binding.key);
shortcutCache.set(command, formatted);
if (binding?.command) {
const formatted = formatKeyBinding(binding.command);
shortcutCache.set(keyBindingKey, formatted);
return formatted;
}
} catch (error) {
console.warn("An error occurred while getting the shortcut:", error);
}
shortcutCache.set(command, "");
shortcutCache.set(keyBindingKey, "");
return "";
}
function getBuiltinMenuNodes(): MenuSchemaNode[] {
function builtinMenuNodes(): MenuSchemaNode[] {
return [
{
id: "copy",
labelKey: "keybindings.commands.blockCopy",
command: copyCommand,
shortcutCommand: KeyBindingCommand.BlockCopyCommand,
keyBindingKey: KeyBindingKey.BlockCopyKeyBindingKey,
enabled: (context) => context.hasSelection
},
{
id: "cut",
labelKey: "keybindings.commands.blockCut",
command: cutCommand,
shortcutCommand: KeyBindingCommand.BlockCutCommand,
keyBindingKey: KeyBindingKey.BlockCutKeyBindingKey,
visible: (context) => context.isEditable,
enabled: (context) => context.hasSelection && context.isEditable
},
@@ -86,21 +87,21 @@ function getBuiltinMenuNodes(): MenuSchemaNode[] {
id: "paste",
labelKey: "keybindings.commands.blockPaste",
command: pasteCommand,
shortcutCommand: KeyBindingCommand.BlockPasteCommand,
keyBindingKey: KeyBindingKey.BlockPasteKeyBindingKey,
visible: (context) => context.isEditable
},
{
id: "undo",
labelKey: "keybindings.commands.historyUndo",
command: undo,
shortcutCommand: KeyBindingCommand.HistoryUndoCommand,
keyBindingKey: KeyBindingKey.HistoryUndoKeyBindingKey,
visible: (context) => context.isEditable
},
{
id: "redo",
labelKey: "keybindings.commands.historyRedo",
command: redo,
shortcutCommand: KeyBindingCommand.HistoryRedoCommand,
keyBindingKey: KeyBindingKey.HistoryRedoKeyBindingKey,
visible: (context) => context.isEditable
}
];
@@ -110,7 +111,7 @@ let builtinMenuRegistered = false;
function ensureBuiltinMenuRegistered(): void {
if (builtinMenuRegistered) return;
registerMenuNodes(getBuiltinMenuNodes());
registerMenuNodes(builtinMenuNodes());
builtinMenuRegistered = true;
}

View File

@@ -1,6 +1,6 @@
import type { EditorView } from '@codemirror/view';
import { EditorState } from '@codemirror/state';
import type { KeyBindingCommand } from '../../../../../bindings/voidraft/internal/models/models';
import { KeyBindingKey } from '@/../bindings/voidraft/internal/models/models';
export interface MenuContext {
view: EditorView;
@@ -16,7 +16,7 @@ export type MenuSchemaNode =
type?: "action";
labelKey: string;
command?: (view: EditorView) => boolean;
shortcutCommand?: KeyBindingCommand;
keyBindingKey?: KeyBindingKey;
visible?: (context: MenuContext) => boolean;
enabled?: (context: MenuContext) => boolean;
}
@@ -37,7 +37,7 @@ export interface RenderMenuItem {
interface MenuBuildOptions {
translate: (key: string) => string;
formatShortcut: (command?: KeyBindingCommand) => string;
formatShortcut: (keyBindingKey?: KeyBindingKey) => string;
}
const menuRegistry: MenuSchemaNode[] = [];
@@ -89,7 +89,7 @@ function convertNode(
}
const disabled = node.enabled ? !node.enabled(context) : false;
const shortcut = options.formatShortcut(node.shortcutCommand);
const shortcut = options.formatShortcut(node.keyBindingKey);
return {
id: node.id,

View File

@@ -1,7 +1,11 @@
import { Extension, StateField } from '@codemirror/state';
import { EditorView, showTooltip, Tooltip } from '@codemirror/view';
import { translatorManager } from './manager';
import { TRANSLATION_ICON_SVG } from '@/common/constant/translation';
const TRANSLATION_ICON_SVG = `
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24">
<path d="M599.68 485.056h-8l30.592 164.672c20.352-7.04 38.72-17.344 54.912-31.104a271.36 271.36 0 0 1-40.704-64.64l32.256-4.032c8.896 17.664 19.072 33.28 30.592 46.72 23.872-27.968 42.24-65.152 55.04-111.744l-154.688 0.128z m121.92 133.76c18.368 15.36 39.36 26.56 62.848 33.472l14.784 4.416-8.64 30.336-14.72-4.352a205.696 205.696 0 0 1-76.48-41.728c-20.672 17.92-44.928 31.552-71.232 40.064l20.736 110.912H519.424l-9.984 72.512h385.152c18.112 0 32.704-14.144 32.704-31.616V295.424a32.128 32.128 0 0 0-32.704-31.552H550.528l35.2 189.696h79.424v-31.552h61.44v31.552h102.4v31.616h-42.688c-14.272 55.488-35.712 100.096-64.64 133.568zM479.36 791.68H193.472c-36.224 0-65.472-28.288-65.472-63.168V191.168C128 156.16 157.312 128 193.472 128h327.68l20.544 104.32h352.832c36.224 0 65.472 28.224 65.472 63.104v537.408c0 34.944-29.312 63.168-65.472 63.168H468.608l10.688-104.32zM337.472 548.352v-33.28H272.768v-48.896h60.16V433.28h-60.16v-41.728h64.704v-32.896h-102.4v189.632h102.4z m158.272 0V453.76c0-17.216-4.032-30.272-12.16-39.488-8.192-9.152-20.288-13.696-36.032-13.696a55.04 55.04 0 0 0-24.768 5.376 39.04 39.04 0 0 0-17.088 15.936h-1.984l-5.056-18.56h-28.352V548.48h37.12V480c0-17.088 2.304-29.376 6.912-36.736 4.608-7.424 12.16-11.072 22.528-11.072 7.616 0 13.248 2.56 16.64 7.872 3.52 5.248 5.312 13.056 5.312 23.488v84.736h36.928z" fill="currentColor"></path>
</svg>`;
function TranslationTooltips(state: any): readonly Tooltip[] {
const selection = state.selection.main;

View File

@@ -1,8 +1,4 @@
import {KeyBindingCommand} from '@/../bindings/voidraft/internal/models/models';
import {
openSearchPanel,
closeSearchPanel,
} from '@codemirror/search';
import {closeSearchPanel, openSearchPanel,} from '@codemirror/search';
import {
addNewBlockAfterCurrent,
addNewBlockAfterLast,
@@ -49,249 +45,249 @@ import {
} from '@codemirror/commands';
import {foldAll, foldCode, unfoldAll, unfoldCode} from '@codemirror/language';
import i18n from '@/i18n';
import {KeyBindingKey} from '@/../bindings/voidraft/internal/models/models';
// 默认编辑器选项
const defaultEditorOptions = {
// 默认代码块扩展选项
const defaultBlockExtensionOptions = {
defaultBlockToken: 'text',
defaultBlockAutoDetect: true,
};
/**
* 前端命令注册表
* 将后端定义的command字段映射到具体的前端方法和翻译键
* 将后端定义的key字段映射到具体的前端方法和翻译键
*/
export const commands = {
[KeyBindingCommand.ShowSearchCommand]: {
export const commands: Record<string, { handler: any; descriptionKey: string }> = {
[KeyBindingKey.ShowSearchKeyBindingKey]: {
handler: openSearchPanel,
descriptionKey: 'keybindings.commands.showSearch'
},
[KeyBindingCommand.HideSearchCommand]: {
[KeyBindingKey.HideSearchKeyBindingKey]: {
handler: closeSearchPanel,
descriptionKey: 'keybindings.commands.hideSearch'
},
// 代码块操作命令
[KeyBindingCommand.BlockSelectAllCommand]: {
[KeyBindingKey.BlockSelectAllKeyBindingKey]: {
handler: selectAll,
descriptionKey: 'keybindings.commands.blockSelectAll'
},
[KeyBindingCommand.BlockAddAfterCurrentCommand]: {
handler: addNewBlockAfterCurrent(defaultEditorOptions),
[KeyBindingKey.BlockAddAfterCurrentKeyBindingKey]: {
handler: addNewBlockAfterCurrent(defaultBlockExtensionOptions),
descriptionKey: 'keybindings.commands.blockAddAfterCurrent'
},
[KeyBindingCommand.BlockAddAfterLastCommand]: {
handler: addNewBlockAfterLast(defaultEditorOptions),
[KeyBindingKey.BlockAddAfterLastKeyBindingKey]: {
handler: addNewBlockAfterLast(defaultBlockExtensionOptions),
descriptionKey: 'keybindings.commands.blockAddAfterLast'
},
[KeyBindingCommand.BlockAddBeforeCurrentCommand]: {
handler: addNewBlockBeforeCurrent(defaultEditorOptions),
[KeyBindingKey.BlockAddBeforeCurrentKeyBindingKey]: {
handler: addNewBlockBeforeCurrent(defaultBlockExtensionOptions),
descriptionKey: 'keybindings.commands.blockAddBeforeCurrent'
},
[KeyBindingCommand.BlockGotoPreviousCommand]: {
[KeyBindingKey.BlockGotoPreviousKeyBindingKey]: {
handler: gotoPreviousBlock,
descriptionKey: 'keybindings.commands.blockGotoPrevious'
},
[KeyBindingCommand.BlockGotoNextCommand]: {
[KeyBindingKey.BlockGotoNextKeyBindingKey]: {
handler: gotoNextBlock,
descriptionKey: 'keybindings.commands.blockGotoNext'
},
[KeyBindingCommand.BlockSelectPreviousCommand]: {
[KeyBindingKey.BlockSelectPreviousKeyBindingKey]: {
handler: selectPreviousBlock,
descriptionKey: 'keybindings.commands.blockSelectPrevious'
},
[KeyBindingCommand.BlockSelectNextCommand]: {
[KeyBindingKey.BlockSelectNextKeyBindingKey]: {
handler: selectNextBlock,
descriptionKey: 'keybindings.commands.blockSelectNext'
},
[KeyBindingCommand.BlockDeleteCommand]: {
handler: deleteBlock(defaultEditorOptions),
[KeyBindingKey.BlockDeleteKeyBindingKey]: {
handler: deleteBlock(defaultBlockExtensionOptions),
descriptionKey: 'keybindings.commands.blockDelete'
},
[KeyBindingCommand.BlockMoveUpCommand]: {
[KeyBindingKey.BlockMoveUpKeyBindingKey]: {
handler: moveCurrentBlockUp,
descriptionKey: 'keybindings.commands.blockMoveUp'
},
[KeyBindingCommand.BlockMoveDownCommand]: {
[KeyBindingKey.BlockMoveDownKeyBindingKey]: {
handler: moveCurrentBlockDown,
descriptionKey: 'keybindings.commands.blockMoveDown'
},
[KeyBindingCommand.BlockDeleteLineCommand]: {
[KeyBindingKey.BlockDeleteLineKeyBindingKey]: {
handler: deleteLineCommand,
descriptionKey: 'keybindings.commands.blockDeleteLine'
},
[KeyBindingCommand.BlockMoveLineUpCommand]: {
[KeyBindingKey.BlockMoveLineUpKeyBindingKey]: {
handler: moveLineUp,
descriptionKey: 'keybindings.commands.blockMoveLineUp'
},
[KeyBindingCommand.BlockMoveLineDownCommand]: {
[KeyBindingKey.BlockMoveLineDownKeyBindingKey]: {
handler: moveLineDown,
descriptionKey: 'keybindings.commands.blockMoveLineDown'
},
[KeyBindingCommand.BlockTransposeCharsCommand]: {
[KeyBindingKey.BlockTransposeCharsKeyBindingKey]: {
handler: transposeChars,
descriptionKey: 'keybindings.commands.blockTransposeChars'
},
[KeyBindingCommand.BlockFormatCommand]: {
[KeyBindingKey.BlockFormatKeyBindingKey]: {
handler: formatCurrentBlock,
descriptionKey: 'keybindings.commands.blockFormat'
},
[KeyBindingCommand.BlockCopyCommand]: {
[KeyBindingKey.BlockCopyKeyBindingKey]: {
handler: copyCommand,
descriptionKey: 'keybindings.commands.blockCopy'
},
[KeyBindingCommand.BlockCutCommand]: {
[KeyBindingKey.BlockCutKeyBindingKey]: {
handler: cutCommand,
descriptionKey: 'keybindings.commands.blockCut'
},
[KeyBindingCommand.BlockPasteCommand]: {
[KeyBindingKey.BlockPasteKeyBindingKey]: {
handler: pasteCommand,
descriptionKey: 'keybindings.commands.blockPaste'
},
[KeyBindingCommand.HistoryUndoCommand]: {
[KeyBindingKey.HistoryUndoKeyBindingKey]: {
handler: undo,
descriptionKey: 'keybindings.commands.historyUndo'
},
[KeyBindingCommand.HistoryRedoCommand]: {
[KeyBindingKey.HistoryRedoKeyBindingKey]: {
handler: redo,
descriptionKey: 'keybindings.commands.historyRedo'
},
[KeyBindingCommand.HistoryUndoSelectionCommand]: {
[KeyBindingKey.HistoryUndoSelectionKeyBindingKey]: {
handler: undoSelection,
descriptionKey: 'keybindings.commands.historyUndoSelection'
},
[KeyBindingCommand.HistoryRedoSelectionCommand]: {
[KeyBindingKey.HistoryRedoSelectionKeyBindingKey]: {
handler: redoSelection,
descriptionKey: 'keybindings.commands.historyRedoSelection'
},
[KeyBindingCommand.FoldCodeCommand]: {
[KeyBindingKey.FoldCodeKeyBindingKey]: {
handler: foldCode,
descriptionKey: 'keybindings.commands.foldCode'
},
[KeyBindingCommand.UnfoldCodeCommand]: {
[KeyBindingKey.UnfoldCodeKeyBindingKey]: {
handler: unfoldCode,
descriptionKey: 'keybindings.commands.unfoldCode'
},
[KeyBindingCommand.FoldAllCommand]: {
[KeyBindingKey.FoldAllKeyBindingKey]: {
handler: foldAll,
descriptionKey: 'keybindings.commands.foldAll'
},
[KeyBindingCommand.UnfoldAllCommand]: {
[KeyBindingKey.UnfoldAllKeyBindingKey]: {
handler: unfoldAll,
descriptionKey: 'keybindings.commands.unfoldAll'
},
[KeyBindingCommand.CursorSyntaxLeftCommand]: {
[KeyBindingKey.CursorSyntaxLeftKeyBindingKey]: {
handler: cursorSyntaxLeft,
descriptionKey: 'keybindings.commands.cursorSyntaxLeft'
},
[KeyBindingCommand.CursorSyntaxRightCommand]: {
[KeyBindingKey.CursorSyntaxRightKeyBindingKey]: {
handler: cursorSyntaxRight,
descriptionKey: 'keybindings.commands.cursorSyntaxRight'
},
[KeyBindingCommand.SelectSyntaxLeftCommand]: {
[KeyBindingKey.SelectSyntaxLeftKeyBindingKey]: {
handler: selectSyntaxLeft,
descriptionKey: 'keybindings.commands.selectSyntaxLeft'
},
[KeyBindingCommand.SelectSyntaxRightCommand]: {
[KeyBindingKey.SelectSyntaxRightKeyBindingKey]: {
handler: selectSyntaxRight,
descriptionKey: 'keybindings.commands.selectSyntaxRight'
},
[KeyBindingCommand.CopyLineUpCommand]: {
[KeyBindingKey.CopyLineUpKeyBindingKey]: {
handler: copyLineUp,
descriptionKey: 'keybindings.commands.copyLineUp'
},
[KeyBindingCommand.CopyLineDownCommand]: {
[KeyBindingKey.CopyLineDownKeyBindingKey]: {
handler: copyLineDown,
descriptionKey: 'keybindings.commands.copyLineDown'
},
[KeyBindingCommand.InsertBlankLineCommand]: {
[KeyBindingKey.InsertBlankLineKeyBindingKey]: {
handler: insertBlankLine,
descriptionKey: 'keybindings.commands.insertBlankLine'
},
[KeyBindingCommand.SelectLineCommand]: {
[KeyBindingKey.SelectLineKeyBindingKey]: {
handler: selectLine,
descriptionKey: 'keybindings.commands.selectLine'
},
[KeyBindingCommand.SelectParentSyntaxCommand]: {
[KeyBindingKey.SelectParentSyntaxKeyBindingKey]: {
handler: selectParentSyntax,
descriptionKey: 'keybindings.commands.selectParentSyntax'
},
[KeyBindingCommand.IndentLessCommand]: {
[KeyBindingKey.IndentLessKeyBindingKey]: {
handler: indentLess,
descriptionKey: 'keybindings.commands.indentLess'
},
[KeyBindingCommand.IndentMoreCommand]: {
[KeyBindingKey.IndentMoreKeyBindingKey]: {
handler: indentMore,
descriptionKey: 'keybindings.commands.indentMore'
},
[KeyBindingCommand.IndentSelectionCommand]: {
[KeyBindingKey.IndentSelectionKeyBindingKey]: {
handler: indentSelection,
descriptionKey: 'keybindings.commands.indentSelection'
},
[KeyBindingCommand.CursorMatchingBracketCommand]: {
[KeyBindingKey.CursorMatchingBracketKeyBindingKey]: {
handler: cursorMatchingBracket,
descriptionKey: 'keybindings.commands.cursorMatchingBracket'
},
[KeyBindingCommand.ToggleCommentCommand]: {
[KeyBindingKey.ToggleCommentKeyBindingKey]: {
handler: toggleComment,
descriptionKey: 'keybindings.commands.toggleComment'
},
[KeyBindingCommand.ToggleBlockCommentCommand]: {
[KeyBindingKey.ToggleBlockCommentKeyBindingKey]: {
handler: toggleBlockComment,
descriptionKey: 'keybindings.commands.toggleBlockComment'
},
[KeyBindingCommand.InsertNewlineAndIndentCommand]: {
[KeyBindingKey.InsertNewlineAndIndentKeyBindingKey]: {
handler: insertNewlineAndIndent,
descriptionKey: 'keybindings.commands.insertNewlineAndIndent'
},
[KeyBindingCommand.DeleteCharBackwardCommand]: {
[KeyBindingKey.DeleteCharBackwardKeyBindingKey]: {
handler: deleteCharBackward,
descriptionKey: 'keybindings.commands.deleteCharBackward'
},
[KeyBindingCommand.DeleteCharForwardCommand]: {
[KeyBindingKey.DeleteCharForwardKeyBindingKey]: {
handler: deleteCharForward,
descriptionKey: 'keybindings.commands.deleteCharForward'
},
[KeyBindingCommand.DeleteGroupBackwardCommand]: {
[KeyBindingKey.DeleteGroupBackwardKeyBindingKey]: {
handler: deleteGroupBackward,
descriptionKey: 'keybindings.commands.deleteGroupBackward'
},
[KeyBindingCommand.DeleteGroupForwardCommand]: {
[KeyBindingKey.DeleteGroupForwardKeyBindingKey]: {
handler: deleteGroupForward,
descriptionKey: 'keybindings.commands.deleteGroupForward'
},
} as const;
};
/**
* 获取命令处理函数
* @param command 命令名称
* @param key 命令标识符
* @returns 对应的处理函数,如果不存在则返回 undefined
*/
export const getCommandHandler = (command: KeyBindingCommand) => {
return commands[command]?.handler;
export const getCommandHandler = (key: string) => {
return commands[key]?.handler;
};
/**
* 获取命令描述
* @param command 命令名称
* @param key 命令标识符
* @returns 对应的描述,如果不存在则返回 undefined
*/
export const getCommandDescription = (command: KeyBindingCommand) => {
const descriptionKey = commands[command]?.descriptionKey;
export const getCommandDescription = (key: string) => {
const descriptionKey = commands[key]?.descriptionKey;
return descriptionKey ? i18n.global.t(descriptionKey) : undefined;
};
/**
* 检查命令是否已注册
* @param command 命令名称
* @param key 命令标识符
* @returns 是否已注册
*/
export const isCommandRegistered = (command: KeyBindingCommand): boolean => {
return command in commands;
export const isCommandRegistered = (key: string): boolean => {
return key in commands;
};
/**
* 获取所有已注册的命令
* @returns 已注册的命令列表
*/
export const getRegisteredCommands = (): KeyBindingCommand[] => {
return Object.keys(commands) as KeyBindingCommand[];
};
export const getRegisteredCommands = (): string[] => {
return Object.keys(commands);
};

View File

@@ -20,10 +20,12 @@ export const createDynamicKeymapExtension = async (): Promise<Extension> => {
await extensionStore.loadExtensions();
}
// 获取启用的扩展ID列表
const enabledExtensionIds = extensionStore.enabledExtensions.map(ext => ext.id);
// 获取启用的扩展key列表
const enabledExtensionKeys = extensionStore.enabledExtensions
.map(ext => ext.key)
.filter((key): key is string => key !== undefined);
return Manager.createKeymapExtension(keybindingStore.keyBindings, enabledExtensionIds);
return Manager.createKeymapExtension(keybindingStore.keyBindings, enabledExtensionKeys);
};
/**
@@ -34,10 +36,12 @@ export const updateKeymapExtension = (view: any): void => {
const keybindingStore = useKeybindingStore();
const extensionStore = useExtensionStore();
// 获取启用的扩展ID列表
const enabledExtensionIds = extensionStore.enabledExtensions.map(ext => ext.id);
// 获取启用的扩展key列表
const enabledExtensionKeys = extensionStore.enabledExtensions
.map(ext => ext.key)
.filter((key): key is string => key !== undefined);
Manager.updateKeymap(view, keybindingStore.keyBindings, enabledExtensionIds);
Manager.updateKeymap(view, keybindingStore.keyBindings, enabledExtensionKeys);
};
// 导出相关模块

View File

@@ -1,6 +1,6 @@
import {keymap} from '@codemirror/view';
import {Extension, Compartment} from '@codemirror/state';
import {KeyBinding as KeyBindingConfig, ExtensionID} from '@/../bindings/voidraft/internal/models/models';
import {KeyBinding as KeyBindingConfig} from '@/../bindings/voidraft/internal/models/ent/models';
import {KeyBinding, KeymapResult} from './types';
import {getCommandHandler, isCommandRegistered} from './commands';
@@ -14,10 +14,10 @@ export class Manager {
/**
* 将后端快捷键配置转换为CodeMirror快捷键绑定
* @param keyBindings 后端快捷键配置列表
* @param enabledExtensions 启用的扩展ID列表,如果不提供则使用所有启用的快捷键
* @param enabledExtensions 启用的扩展key列表,如果不提供则使用所有启用的快捷键
* @returns 转换结果
*/
static convertToKeyBindings(keyBindings: KeyBindingConfig[], enabledExtensions?: ExtensionID[]): KeymapResult {
static convertToKeyBindings(keyBindings: KeyBindingConfig[], enabledExtensions?: string[]): KeymapResult {
const result: KeyBinding[] = [];
for (const binding of keyBindings) {
@@ -27,24 +27,25 @@ export class Manager {
}
// 如果提供了扩展列表,则只处理启用扩展的快捷键
if (enabledExtensions && !enabledExtensions.includes(binding.extension)) {
if (enabledExtensions && binding.extension && !enabledExtensions.includes(binding.extension)) {
continue;
}
// 检查命令是否已注册
if (!isCommandRegistered(binding.command)) {
// 检查命令是否已注册(使用 key 字段作为命令标识符)
if (!binding.key || !isCommandRegistered(binding.key)) {
continue;
}
// 获取命令处理函数
const handler = getCommandHandler(binding.command);
const handler = getCommandHandler(binding.key);
if (!handler) {
continue;
}
// 转换为CodeMirror快捷键格式
// binding.command 是快捷键组合 (如 "Mod-f")binding.key 是命令标识符
const keyBinding: KeyBinding = {
key: binding.key,
key: binding.command || '',
run: handler,
preventDefault: true
};
@@ -58,10 +59,10 @@ export class Manager {
/**
* 创建CodeMirror快捷键扩展
* @param keyBindings 后端快捷键配置列表
* @param enabledExtensions 启用的扩展ID列表
* @param enabledExtensions 启用的扩展key列表
* @returns CodeMirror扩展
*/
static createKeymapExtension(keyBindings: KeyBindingConfig[], enabledExtensions?: ExtensionID[]): Extension {
static createKeymapExtension(keyBindings: KeyBindingConfig[], enabledExtensions?: string[]): Extension {
const {keyBindings: cmKeyBindings} =
this.convertToKeyBindings(keyBindings, enabledExtensions);
@@ -72,9 +73,9 @@ export class Manager {
* 动态更新快捷键扩展
* @param view 编辑器视图
* @param keyBindings 后端快捷键配置列表
* @param enabledExtensions 启用的扩展ID列表
* @param enabledExtensions 启用的扩展key列表
*/
static updateKeymap(view: any, keyBindings: KeyBindingConfig[], enabledExtensions: ExtensionID[]): void {
static updateKeymap(view: any, keyBindings: KeyBindingConfig[], enabledExtensions: string[]): void {
const {keyBindings: cmKeyBindings} =
this.convertToKeyBindings(keyBindings, enabledExtensions);

View File

@@ -1,5 +1,4 @@
import {Manager} from './manager';
import {ExtensionID} from '@/../bindings/voidraft/internal/models/models';
import i18n from '@/i18n';
import {ExtensionDefinition} from './types';
import {Prec} from '@codemirror/state';
@@ -15,6 +14,8 @@ import {foldGutter} from "@codemirror/language";
import {highlightActiveLineGutter, highlightWhitespace, highlightTrailingWhitespace} from "@codemirror/view";
import createEditorContextMenu from '../extensions/contextMenu';
import {blockLineNumbers} from '../extensions/codeblock';
import {createHttpClientExtension} from '../extensions/httpclient';
import {ExtensionKey} from '@/../bindings/voidraft/internal/models/models';
type ExtensionEntry = {
definition: ExtensionDefinition
@@ -22,35 +23,36 @@ type ExtensionEntry = {
descriptionKey: string
};
type RegisteredExtensionID = Exclude<ExtensionID, ExtensionID.$zero | ExtensionID.ExtensionEditor>;
// 排除 $zero 的有效扩展 Key 类型
type ValidExtensionKey = Exclude<ExtensionKey, ExtensionKey.$zero>;
const defineExtension = (create: (config: any) => any, defaultConfig: Record<string, any> = {}): ExtensionDefinition => ({
create,
defaultConfig
});
const EXTENSION_REGISTRY: Record<RegisteredExtensionID, ExtensionEntry> = {
[ExtensionID.ExtensionRainbowBrackets]: {
const EXTENSION_REGISTRY: Record<ValidExtensionKey, ExtensionEntry> = {
[ExtensionKey.ExtensionRainbowBrackets]: {
definition: defineExtension(() => rainbowBrackets()),
displayNameKey: 'extensions.rainbowBrackets.name',
descriptionKey: 'extensions.rainbowBrackets.description'
},
[ExtensionID.ExtensionHyperlink]: {
[ExtensionKey.ExtensionHyperlink]: {
definition: defineExtension(() => hyperLink),
displayNameKey: 'extensions.hyperlink.name',
descriptionKey: 'extensions.hyperlink.description'
},
[ExtensionID.ExtensionColorSelector]: {
[ExtensionKey.ExtensionColorSelector]: {
definition: defineExtension(() => color),
displayNameKey: 'extensions.colorSelector.name',
descriptionKey: 'extensions.colorSelector.description'
},
[ExtensionID.ExtensionTranslator]: {
[ExtensionKey.ExtensionTranslator]: {
definition: defineExtension(() => createTranslatorExtension()),
displayNameKey: 'extensions.translator.name',
descriptionKey: 'extensions.translator.description'
},
[ExtensionID.ExtensionMinimap]: {
[ExtensionKey.ExtensionMinimap]: {
definition: defineExtension((config: any) => minimap({
displayText: config?.displayText ?? 'characters',
showOverlay: config?.showOverlay ?? 'always',
@@ -63,85 +65,90 @@ const EXTENSION_REGISTRY: Record<RegisteredExtensionID, ExtensionEntry> = {
displayNameKey: 'extensions.minimap.name',
descriptionKey: 'extensions.minimap.description'
},
[ExtensionID.ExtensionSearch]: {
[ExtensionKey.ExtensionSearch]: {
definition: defineExtension(() => vscodeSearch),
displayNameKey: 'extensions.search.name',
descriptionKey: 'extensions.search.description'
},
[ExtensionID.ExtensionFold]: {
[ExtensionKey.ExtensionFold]: {
definition: defineExtension(() => Prec.low(foldGutter())),
displayNameKey: 'extensions.fold.name',
descriptionKey: 'extensions.fold.description'
},
[ExtensionID.ExtensionMarkdown]: {
[ExtensionKey.ExtensionMarkdown]: {
definition: defineExtension(() => markdownExtensions),
displayNameKey: 'extensions.markdown.name',
descriptionKey: 'extensions.markdown.description'
},
[ExtensionID.ExtensionLineNumbers]: {
[ExtensionKey.ExtensionLineNumbers]: {
definition: defineExtension(() => Prec.high([blockLineNumbers, highlightActiveLineGutter()])),
displayNameKey: 'extensions.lineNumbers.name',
descriptionKey: 'extensions.lineNumbers.description'
},
[ExtensionID.ExtensionContextMenu]: {
[ExtensionKey.ExtensionContextMenu]: {
definition: defineExtension(() => createEditorContextMenu()),
displayNameKey: 'extensions.contextMenu.name',
descriptionKey: 'extensions.contextMenu.description'
},
[ExtensionID.ExtensionHighlightWhitespace]: {
[ExtensionKey.ExtensionHighlightWhitespace]: {
definition: defineExtension(() => highlightWhitespace()),
displayNameKey: 'extensions.highlightWhitespace.name',
descriptionKey: 'extensions.highlightWhitespace.description'
},
[ExtensionID.ExtensionHighlightTrailingWhitespace]: {
[ExtensionKey.ExtensionHighlightTrailingWhitespace]: {
definition: defineExtension(() => highlightTrailingWhitespace()),
displayNameKey: 'extensions.highlightTrailingWhitespace.name',
descriptionKey: 'extensions.highlightTrailingWhitespace.description'
},
[ExtensionKey.ExtensionHttpClient]: {
definition: defineExtension(() => createHttpClientExtension()),
displayNameKey: 'extensions.httpClient.name',
descriptionKey: 'extensions.httpClient.description'
}
} as const;
};
const isRegisteredExtension = (id: ExtensionID): id is RegisteredExtensionID =>
Object.prototype.hasOwnProperty.call(EXTENSION_REGISTRY, id);
const isRegisteredExtension = (key: string): key is ValidExtensionKey =>
Object.prototype.hasOwnProperty.call(EXTENSION_REGISTRY, key);
const getRegistryEntry = (id: ExtensionID): ExtensionEntry | undefined => {
if (!isRegisteredExtension(id)) {
const getRegistryEntry = (key: string): ExtensionEntry | undefined => {
if (!isRegisteredExtension(key)) {
return undefined;
}
return EXTENSION_REGISTRY[id];
return EXTENSION_REGISTRY[key];
};
export function registerAllExtensions(manager: Manager): void {
(Object.entries(EXTENSION_REGISTRY) as [RegisteredExtensionID, ExtensionEntry][]).forEach(([id, entry]) => {
(Object.entries(EXTENSION_REGISTRY) as [ValidExtensionKey, ExtensionEntry][]).forEach(([id, entry]) => {
manager.registerExtension(id, entry.definition);
});
}
export function getExtensionDisplayName(id: ExtensionID): string {
const entry = getRegistryEntry(id);
return entry?.displayNameKey ? i18n.global.t(entry.displayNameKey) : id;
export function getExtensionDisplayName(key: string): string {
const entry = getRegistryEntry(key);
return entry?.displayNameKey ? i18n.global.t(entry.displayNameKey) : key;
}
export function getExtensionDescription(id: ExtensionID): string {
const entry = getRegistryEntry(id);
export function getExtensionDescription(key: string): string {
const entry = getRegistryEntry(key);
return entry?.descriptionKey ? i18n.global.t(entry.descriptionKey) : '';
}
function getExtensionDefinition(id: ExtensionID): ExtensionDefinition | undefined {
return getRegistryEntry(id)?.definition;
function getExtensionDefinition(key: string): ExtensionDefinition | undefined {
return getRegistryEntry(key)?.definition;
}
export function getExtensionDefaultConfig(id: ExtensionID): any {
const definition = getExtensionDefinition(id);
export function getExtensionDefaultConfig(key: string): any {
const definition = getExtensionDefinition(key);
if (!definition) return {};
return cloneConfig(definition.defaultConfig);
}
export function hasExtensionConfig(id: ExtensionID): boolean {
return Object.keys(getExtensionDefaultConfig(id)).length > 0;
export function hasExtensionConfig(key: string): boolean {
return Object.keys(getExtensionDefaultConfig(key)).length > 0;
}
export function getAllExtensionIds(): ExtensionID[] {
return Object.keys(EXTENSION_REGISTRY) as RegisteredExtensionID[];
export function getAllExtensionIds(): string[] {
return Object.keys(EXTENSION_REGISTRY);
}
const cloneConfig = (config: any) => {

View File

@@ -12,9 +12,8 @@ const extensionManager = new Manager();
/**
* 异步创建动态扩展
* 确保扩展配置已加载
* @param _documentId 可选的文档ID用于提前初始化视图
*/
export const createDynamicExtensions = async (_documentId?: number): Promise<Extension[]> => {
export const createDynamicExtensions = async (): Promise<Extension[]> => {
const extensionStore = useExtensionStore();
// 注册所有扩展工厂

View File

@@ -1,6 +1,6 @@
import {Compartment, Extension} from '@codemirror/state';
import {EditorView} from '@codemirror/view';
import {Extension as ExtensionConfig, ExtensionID} from '@/../bindings/voidraft/internal/models/models';
import {Extension as ExtensionConfig} from '@/../bindings/voidraft/internal/models/ent/models';
import {ExtensionDefinition, ExtensionState} from './types';
/**
@@ -8,10 +8,10 @@ import {ExtensionDefinition, ExtensionState} from './types';
* 负责注册、初始化与同步所有动态扩展
*/
export class Manager {
private extensionStates = new Map<ExtensionID, ExtensionState>();
private extensionStates = new Map<string, ExtensionState>();
private views = new Map<number, EditorView>();
registerExtension(id: ExtensionID, definition: ExtensionDefinition): void {
registerExtension(id: string, definition: ExtensionDefinition): void {
const existingState = this.extensionStates.get(id);
if (existingState) {
existingState.definition = definition;
@@ -34,10 +34,11 @@ export class Manager {
initExtensions(extensionConfigs: ExtensionConfig[]): void {
for (const config of extensionConfigs) {
const state = this.extensionStates.get(config.id);
if (!config.key) continue;
const state = this.extensionStates.get(config.key);
if (!state) continue;
const resolvedConfig = this.cloneConfig(config.config ?? state.definition.defaultConfig ?? {});
this.commitExtensionState(state, config.enabled, resolvedConfig);
this.commitExtensionState(state, config.enabled ?? false, resolvedConfig);
}
}
@@ -54,7 +55,7 @@ export class Manager {
this.applyAllExtensionsToView(view);
}
updateExtension(id: ExtensionID, enabled: boolean, config?: any): void {
updateExtension(id: string, enabled: boolean, config?: any): void {
const state = this.extensionStates.get(id);
if (!state) return;
@@ -93,7 +94,7 @@ export class Manager {
}
}
private applyExtensionToAllViews(id: ExtensionID): void {
private applyExtensionToAllViews(id: string): void {
const state = this.extensionStates.get(id);
if (!state) return;

View File

@@ -1 +1,23 @@
import {Compartment, Extension} from '@codemirror/state';
import {Compartment, Extension} from '@codemirror/state';
/**
* 扩展定义
* 标准化 create 方法和默认配置
*/
export interface ExtensionDefinition {
create(config: any): Extension
defaultConfig: Record<string, any>
}
/**
* 扩展运行时状态
*/
export interface ExtensionState {
id: string // 扩展 key
definition: ExtensionDefinition
config: any
enabled: boolean
compartment: Compartment
extension: Extension
}

View File

@@ -1,5 +1,5 @@
import type {ThemeColors} from './types';
import {ThemeType} from '@/../bindings/voidraft/internal/models/models';
import {Type as ThemeType} from '@/../bindings/voidraft/internal/models/ent/theme/models';
import {defaultDarkColors} from './dark/default-dark';
import {defaultLightColors} from './light/default-light';
import {config as draculaColors} from './dark/dracula';
@@ -24,20 +24,20 @@ export interface ThemePreset {
export const FALLBACK_THEME_NAME = defaultDarkColors.themeName;
export const themePresetList: ThemePreset[] = [
{name: defaultDarkColors.themeName, type: ThemeType.ThemeTypeDark, colors: defaultDarkColors},
{name: draculaColors.themeName, type: ThemeType.ThemeTypeDark, colors: draculaColors},
{name: auraColors.themeName, type: ThemeType.ThemeTypeDark, colors: auraColors},
{name: githubDarkColors.themeName, type: ThemeType.ThemeTypeDark, colors: githubDarkColors},
{name: materialDarkColors.themeName, type: ThemeType.ThemeTypeDark, colors: materialDarkColors},
{name: oneDarkColors.themeName, type: ThemeType.ThemeTypeDark, colors: oneDarkColors},
{name: solarizedDarkColors.themeName, type: ThemeType.ThemeTypeDark, colors: solarizedDarkColors},
{name: tokyoNightColors.themeName, type: ThemeType.ThemeTypeDark, colors: tokyoNightColors},
{name: tokyoNightStormColors.themeName, type: ThemeType.ThemeTypeDark, colors: tokyoNightStormColors},
{name: defaultLightColors.themeName, type: ThemeType.ThemeTypeLight, colors: defaultLightColors},
{name: githubLightColors.themeName, type: ThemeType.ThemeTypeLight, colors: githubLightColors},
{name: materialLightColors.themeName, type: ThemeType.ThemeTypeLight, colors: materialLightColors},
{name: solarizedLightColors.themeName, type: ThemeType.ThemeTypeLight, colors: solarizedLightColors},
{name: tokyoNightDayColors.themeName, type: ThemeType.ThemeTypeLight, colors: tokyoNightDayColors},
{name: defaultDarkColors.themeName, type: ThemeType.TypeDark, colors: defaultDarkColors},
{name: draculaColors.themeName, type: ThemeType.TypeDark, colors: draculaColors},
{name: auraColors.themeName, type: ThemeType.TypeDark, colors: auraColors},
{name: githubDarkColors.themeName, type: ThemeType.TypeDark, colors: githubDarkColors},
{name: materialDarkColors.themeName, type: ThemeType.TypeDark, colors: materialDarkColors},
{name: oneDarkColors.themeName, type: ThemeType.TypeDark, colors: oneDarkColors},
{name: solarizedDarkColors.themeName, type: ThemeType.TypeDark, colors: solarizedDarkColors},
{name: tokyoNightColors.themeName, type: ThemeType.TypeDark, colors: tokyoNightColors},
{name: tokyoNightStormColors.themeName, type: ThemeType.TypeDark, colors: tokyoNightStormColors},
{name: defaultLightColors.themeName, type: ThemeType.TypeLight, colors: defaultLightColors},
{name: githubLightColors.themeName, type: ThemeType.TypeLight, colors: githubLightColors},
{name: materialLightColors.themeName, type: ThemeType.TypeLight, colors: materialLightColors},
{name: solarizedLightColors.themeName, type: ThemeType.TypeLight, colors: solarizedLightColors},
{name: tokyoNightDayColors.themeName, type: ThemeType.TypeLight, colors: tokyoNightDayColors},
];
export const themePresetMap: Record<string, ThemePreset> = themePresetList.reduce(

View File

@@ -7,7 +7,7 @@ import SettingSection from '../components/SettingSection.vue';
import SettingItem from '../components/SettingItem.vue';
import { SystemThemeType, LanguageType } from '@/../bindings/voidraft/internal/models/models';
import { createDebounce } from '@/common/utils/debounce';
import { createTimerManager } from '@/common/utils/timerUtils';
import { useConfirm } from '@/composables/useConfirm';
import PickColors from 'vue-pick-colors';
import type { ThemeColors } from '@/views/editor/theme/types';
@@ -21,31 +21,22 @@ const { debouncedFn: debouncedUpdateColor } = createDebounce(
{ delay: 100 }
);
const { debouncedFn: debouncedResetTheme } = createDebounce(
async () => {
const success = await themeStore.resetCurrentTheme();
if (success) {
// 重新加载临时颜色
syncTempColors();
hasUnsavedChanges.value = false;
}
},
{ delay: 300 }
);
// 创建定时器管理器
const resetTimer = createTimerManager();
// 临时颜色状态(用于编辑)
const tempColors = ref<ThemeColors | null>(null);
// 标记是否有未保存的更改
const hasUnsavedChanges = ref(false);
// 重置按钮状态
const resetButtonState = ref({
confirming: false
// 重置主题确认
const { isConfirming: isResetConfirming, requestConfirm: requestResetConfirm } = useConfirm({
timeout: 3000,
onConfirm: async () => {
const success = await themeStore.resetCurrentTheme();
if (success) {
syncTempColors();
hasUnsavedChanges.value = false;
}
}
});
// 当前选中的主题名称
@@ -125,23 +116,6 @@ const toggleSearch = async () => {
}
};
// 处理重置按钮点击
const handleResetClick = () => {
if (resetButtonState.value.confirming) {
debouncedResetTheme();
resetButtonState.value.confirming = false;
resetTimer.clear();
} else {
resetButtonState.value.confirming = true;
// 设置3秒后自动恢复
resetTimer.set(() => {
resetButtonState.value.confirming = false;
}, 3000);
}
};
// 更新本地颜色配置
const updateLocalColor = (colorKey: string, value: string) => {
if (!tempColors.value) return;
@@ -295,10 +269,10 @@ const handlePickerClose = () => {
</button>
<button
v-if="!hasUnsavedChanges"
:class="['reset-button', resetButtonState.confirming ? 'reset-button-confirming' : '']"
@click="handleResetClick"
:class="['reset-button', isResetConfirming('theme') ? 'reset-button-confirming' : '']"
@click="requestResetConfirm('theme')"
>
{{ resetButtonState.confirming ? t('settings.confirmReset') : t('settings.resetToDefault') }}
{{ isResetConfirming('theme') ? t('settings.confirmReset') : t('settings.resetToDefault') }}
</button>
<template v-else>
<button class="apply-button" @click="applyChanges">

View File

@@ -4,7 +4,6 @@ import {useI18n} from 'vue-i18n';
import {useEditorStore} from '@/stores/editorStore';
import {useExtensionStore} from '@/stores/extensionStore';
import {ExtensionService} from '@/../bindings/voidraft/internal/services';
import {ExtensionID} from '@/../bindings/voidraft/internal/models/models';
import {
getAllExtensionIds,
getExtensionDefaultConfig,
@@ -21,59 +20,58 @@ const editorStore = useEditorStore();
const extensionStore = useExtensionStore();
// 展开状态管理
const expandedExtensions = ref<Set<ExtensionID>>(new Set());
const expandedExtensions = ref<Set<string>>(new Set());
// 获取所有可用的扩展
const availableExtensions = computed(() => {
return getAllExtensionIds().map(id => {
const extension = extensionStore.extensions.find(ext => ext.id === id);
return getAllExtensionIds().map(key => {
const extension = extensionStore.extensions.find(ext => ext.key === key);
return {
id,
displayName: getExtensionDisplayName(id),
description: getExtensionDescription(id),
id: key,
displayName: getExtensionDisplayName(key),
description: getExtensionDescription(key),
enabled: extension?.enabled || false,
isDefault: extension?.isDefault || false,
hasConfig: hasExtensionConfig(id),
hasConfig: hasExtensionConfig(key),
config: extension?.config || {},
defaultConfig: getExtensionDefaultConfig(id)
defaultConfig: getExtensionDefaultConfig(key)
};
});
});
// 切换展开状态
const toggleExpanded = (extensionId: ExtensionID) => {
if (expandedExtensions.value.has(extensionId)) {
expandedExtensions.value.delete(extensionId);
const toggleExpanded = (extensionKey: string) => {
if (expandedExtensions.value.has(extensionKey)) {
expandedExtensions.value.delete(extensionKey);
} else {
expandedExtensions.value.add(extensionId);
expandedExtensions.value.add(extensionKey);
}
};
// 更新扩展状态
const updateExtension = async (extensionId: ExtensionID, enabled: boolean) => {
const updateExtension = async (extensionKey: string, enabled: boolean) => {
try {
await editorStore.updateExtension(extensionId, enabled);
await editorStore.updateExtension(extensionKey, enabled);
} catch (error) {
console.error('Failed to update extension:', error);
}
};
// 更新扩展配置
const updateExtensionConfig = async (extensionId: ExtensionID, configKey: string, value: any) => {
const updateExtensionConfig = async (extensionKey: string, configKey: string, value: any) => {
try {
// 获取当前扩展状态
const extension = extensionStore.extensions.find(ext => ext.id === extensionId);
const extension = extensionStore.extensions.find(ext => ext.key === extensionKey);
if (!extension) return;
// 更新配置
const updatedConfig = {...extension.config};
const updatedConfig = {...(extension.config || {})};
if (value === undefined) {
delete updatedConfig[configKey];
} else {
updatedConfig[configKey] = value;
}
// 使用editorStore的updateExtension方法更新确保应用到所有编辑器实例
await editorStore.updateExtension(extensionId, extension.enabled, updatedConfig);
await editorStore.updateExtension(extensionKey, extension.enabled ?? false, updatedConfig);
} catch (error) {
console.error('Failed to update extension config:', error);
@@ -81,19 +79,19 @@ const updateExtensionConfig = async (extensionId: ExtensionID, configKey: string
};
// 重置扩展到默认配置
const resetExtension = async (extensionId: ExtensionID) => {
const resetExtension = async (extensionKey: string) => {
try {
// 重置到默认配置
await ExtensionService.ResetExtensionToDefault(extensionId);
await ExtensionService.ResetExtensionConfig(extensionKey);
// 重新加载扩展状态以获取最新配置
await extensionStore.loadExtensions();
// 获取重置后的状态,立即应用到所有编辑器视图
const extension = extensionStore.extensions.find(ext => ext.id === extensionId);
const extension = extensionStore.extensions.find(ext => ext.key === extensionKey);
if (extension) {
// 通过editorStore更新确保所有视图都能同步
await editorStore.updateExtension(extensionId, extension.enabled, extension.config);
await editorStore.updateExtension(extensionKey, extension.enabled ?? false, extension.config);
}
} catch (error) {
console.error('Failed to reset extension:', error);
@@ -127,7 +125,7 @@ const formatConfigValue = (value: any): string => {
const handleConfigInput = async (
extensionId: ExtensionID,
extensionKey: string,
configKey: string,
defaultValue: any,
event: Event
@@ -137,15 +135,15 @@ const handleConfigInput = async (
const rawValue = target.value;
const trimmedValue = rawValue.trim();
if (!trimmedValue.length) {
await updateExtensionConfig(extensionId, configKey, undefined);
await updateExtensionConfig(extensionKey, configKey, undefined);
return;
}
try {
const parsedValue = JSON.parse(trimmedValue);
await updateExtensionConfig(extensionId, configKey, parsedValue);
await updateExtensionConfig(extensionKey, configKey, parsedValue);
} catch (_error) {
const extension = extensionStore.extensions.find(ext => ext.id === extensionId);
const extension = extensionStore.extensions.find(ext => ext.key === extensionKey);
const fallbackValue = getConfigValue(extension?.config, configKey, defaultValue);
target.value = formatConfigValue(fallbackValue);

View File

@@ -2,133 +2,67 @@
import {useConfigStore} from '@/stores/configStore';
import {useTabStore} from '@/stores/tabStore';
import {useI18n} from 'vue-i18n';
import {computed, onUnmounted, ref} from 'vue';
import {computed, ref} from 'vue';
import SettingSection from '../components/SettingSection.vue';
import SettingItem from '../components/SettingItem.vue';
import ToggleSwitch from '../components/ToggleSwitch.vue';
import {
DialogService,
MigrationProgress,
MigrationService,
MigrationStatus
} from '@/../bindings/voidraft/internal/services';
import {DialogService, MigrationService} from '@/../bindings/voidraft/internal/services';
import {useSystemStore} from "@/stores/systemStore";
import {useConfirm, usePolling} from '@/composables';
const {t} = useI18n();
const configStore = useConfigStore();
const systemStore = useSystemStore();
const tabStore = useTabStore();
// 迁移进度状态
const migrationProgress = ref<MigrationProgress>(new MigrationProgress({
status: MigrationStatus.MigrationStatusCompleted,
progress: 0
}));
// 轮询相关
let pollingTimer: number | null = null;
const isPolling = ref(false);
// 进度条显示控制
const showProgress = ref(false);
const progressError = ref('');
let hideProgressTimer: any = null;
const showBar = ref(false);
const manualError = ref(''); // 用于捕获 MigrateDirectory 抛出的错误
let hideTimer = 0;
// 开始轮询迁移进度
const startPolling = () => {
if (isPolling.value) return;
isPolling.value = true;
showProgress.value = true;
progressError.value = '';
// 立即重置迁移进度状态,避免从之前的失败状态渐变
migrationProgress.value = new MigrationProgress({
status: MigrationStatus.MigrationStatusMigrating,
progress: 0
});
pollingTimer = window.setInterval(async () => {
try {
const progress = await MigrationService.GetProgress();
migrationProgress.value = progress;
const {status, error} = progress;
const isCompleted = [MigrationStatus.MigrationStatusCompleted, MigrationStatus.MigrationStatusFailed].includes(status);
if (isCompleted) {
stopPolling();
// 设置错误信息(如果是失败状态)
progressError.value = (status === MigrationStatus.MigrationStatusFailed) ? (error || 'Migration failed') : '';
const delay = status === MigrationStatus.MigrationStatusCompleted ? 3000 : 5000;
hideProgressTimer = setTimeout(hideProgress, delay);
// 轮询迁移进度
const {data: progress, error: pollError, isActive: migrating, start, stop, reset} = usePolling(
() => MigrationService.GetProgress(),
{
interval: 300,
shouldStop: ({progress, error}) => !!error || progress >= 100,
onStop: () => {
const hasError = pollError.value || progress.value?.error;
hideTimer = window.setTimeout(hideAll, hasError ? 5000 : 3000);
}
} catch (_error) {
stopPolling();
// 使用常量简化错误处理
const errorMsg = 'Failed to get migration progress';
Object.assign(migrationProgress.value, {
status: MigrationStatus.MigrationStatusFailed,
progress: 0,
error: errorMsg
});
progressError.value = errorMsg;
hideProgressTimer = setTimeout(hideProgress, 5000);
}
}, 200);
};
// 停止轮询
const stopPolling = () => {
if (pollingTimer) {
clearInterval(pollingTimer);
pollingTimer = null;
}
isPolling.value = false;
};
// 隐藏进度条
const hideProgress = () => {
showProgress.value = false;
progressError.value = '';
// 重置迁移状态,避免下次显示时状态不正确
migrationProgress.value = new MigrationProgress({
status: MigrationStatus.MigrationStatusCompleted,
progress: 0
});
if (hideProgressTimer) {
clearTimeout(hideProgressTimer);
hideProgressTimer = null;
}
};
// 简化的迁移状态管理
const isMigrating = computed(() => migrationProgress.value.status === MigrationStatus.MigrationStatusMigrating);
// 进度条样式 - 使用 Map 简化条件判断
const statusClassMap = new Map([
[MigrationStatus.MigrationStatusMigrating, 'migrating'],
[MigrationStatus.MigrationStatusCompleted, 'success'],
[MigrationStatus.MigrationStatusFailed, 'error']
]);
const progressBarClass = computed(() =>
showProgress.value ? statusClassMap.get(migrationProgress.value.status) ?? '' : ''
);
const progressBarWidth = computed(() => {
if (!showProgress.value) return '0%';
return isMigrating.value ? `${migrationProgress.value.progress}%` : '100%';
// 派生状态
const migrationError = computed(() => manualError.value || pollError.value || progress.value?.error || '');
const currentProgress = computed(() => progress.value?.progress ?? 0);
const barClass = computed(() => {
if (!showBar.value) return '';
return migrationError.value ? 'error' : currentProgress.value >= 100 ? 'success' : 'migrating';
});
// 重置确认状态
const resetConfirmState = ref<'idle' | 'confirming'>('idle');
let resetConfirmTimer: any = null;
const barWidth = computed(() => {
if (!showBar.value) return '0%';
return (migrationError.value || currentProgress.value >= 100) ? '100%' : `${currentProgress.value}%`;
});
// 隐藏进度条并清除所有状态
const hideAll = () => {
clearTimeout(hideTimer);
hideTimer = 0;
showBar.value = false;
manualError.value = '';
reset(); // 清除轮询状态
};
// 重置设置确认
const {isConfirming: isResetConfirming, requestConfirm: requestResetConfirm} = useConfirm({
timeout: 3000,
onConfirm: async () => {
await configStore.resetConfig();
}
});
// 可选键列表
const keyOptions = [
@@ -201,7 +135,7 @@ const modifierKeys = computed(() => ({
win: configStore.config.general.globalHotkey.win
}));
// 主键配置 - 只读计算属性
// 主键配置
const selectedKey = computed(() => configStore.config.general.globalHotkey.key);
// 切换修饰键
@@ -218,29 +152,8 @@ const updateSelectedKey = (event: Event) => {
configStore.setGlobalHotkey(newHotkey);
};
// 重置设置
const resetSettings = () => {
if (resetConfirmState.value === 'idle') {
// 第一次点击,进入确认状态
resetConfirmState.value = 'confirming';
// 3秒后自动返回idle状态
resetConfirmTimer = setTimeout(() => {
resetConfirmState.value = 'idle';
}, 3000);
} else if (resetConfirmState.value === 'confirming') {
// 第二次点击,执行重置
clearTimeout(resetConfirmTimer);
resetConfirmState.value = 'idle';
confirmReset();
}
};
// 确认重置
const confirmReset = async () => {
await configStore.resetConfig();
};
// 计算热键预览文本 - 使用现代语法简化
// 计算热键预览文本
const hotkeyPreview = computed(() => {
if (!enableGlobalHotkey.value) return '';
@@ -261,54 +174,30 @@ const currentDataPath = computed(() => configStore.config.general.dataPath);
// 选择数据存储目录
const selectDataDirectory = async () => {
if (isMigrating.value) return;
if (migrating.value) return;
const selectedPath = await DialogService.SelectDirectory();
if (!selectedPath?.trim() || selectedPath === currentDataPath.value) return;
// 检查用户是否取消了选择或路径为空
if (!selectedPath || !selectedPath.trim() || selectedPath === currentDataPath.value) {
return;
}
const oldPath = currentDataPath.value;
const newPath = selectedPath.trim();
const [oldPath, newPath] = [currentDataPath.value, selectedPath.trim()];
// 清除之前的进度状态
hideProgress();
// 清除之前的状态并开始轮询
hideAll();
showBar.value = true;
manualError.value = '';
start();
// 开始轮询迁移进度
startPolling();
// 开始迁移
try {
await MigrationService.MigrateDirectory(oldPath, newPath);
await configStore.setDataPath(newPath);
} catch (error) {
stopPolling();
// 使用解构和默认值简化错误处理
const errorMsg = error?.toString() || 'Migration failed';
showProgress.value = true;
Object.assign(migrationProgress.value, {
status: MigrationStatus.MigrationStatusFailed,
progress: 0,
error: errorMsg
});
progressError.value = errorMsg;
hideProgressTimer = setTimeout(hideProgress, 5000);
} catch (e) {
stop();
// 设置手动捕获的错误(当轮询还没获取到错误时)
manualError.value = String(e).replace(/^Error:\s*/i, '') || 'Migration failed';
showBar.value = true;
hideTimer = window.setTimeout(hideAll, 5000);
}
};
// 清理定时器
onUnmounted(() => {
stopPolling();
hideProgress();
if (resetConfirmTimer) {
clearTimeout(resetConfirmTimer);
}
});
</script>
<template>
@@ -394,24 +283,15 @@ onUnmounted(() => {
class="path-display-input"
@click="selectDataDirectory"
:title="t('settings.clickToSelectPath')"
:disabled="isMigrating"
:disabled="migrating"
/>
<!-- 简洁的进度条 -->
<div
class="progress-bar"
:class="[
{ 'active': showProgress },
progressBarClass
]"
:style="{ width: progressBarWidth }"
></div>
<!-- 进度条 -->
<div class="progress-bar" :class="[{'active': showBar}, barClass]" :style="{width: barWidth}"/>
</div>
<!-- 错误提示 -->
<Transition name="error-fade">
<div v-if="progressError" class="progress-error">
{{ progressError }}
</div>
<div v-if="migrationError" class="progress-error">{{ migrationError }}</div>
</Transition>
</div>
</div>
@@ -421,15 +301,10 @@ onUnmounted(() => {
<SettingItem :title="t('settings.resetAllSettings')">
<button
class="reset-button"
:class="{ 'confirming': resetConfirmState === 'confirming' }"
@click="resetSettings"
:class="{ 'confirming': isResetConfirming('reset') }"
@click="requestResetConfirm('reset')"
>
<template v-if="resetConfirmState === 'idle'">
{{ t('settings.reset') }}
</template>
<template v-else-if="resetConfirmState === 'confirming'">
{{ t('settings.confirmReset') }}
</template>
{{ isResetConfirming('reset') ? t('settings.confirmReset') : t('settings.reset') }}
</button>
</SettingItem>
</SettingSection>
@@ -656,11 +531,8 @@ onUnmounted(() => {
}
.progress-error {
margin-top: 6px;
font-size: 12px;
color: #ef4444;
padding: 0 2px;
line-height: 1.4;
opacity: 1;
transition: all 0.3s ease;
}

View File

@@ -6,7 +6,7 @@ import { useKeybindingStore } from '@/stores/keybindingStore';
import { useExtensionStore } from '@/stores/extensionStore';
import { useSystemStore } from '@/stores/systemStore';
import { getCommandDescription } from '@/views/editor/keymap/commands';
import {KeyBindingCommand} from "@/../bindings/voidraft/internal/models";
import { KeyBindingKey } from '@/../bindings/voidraft/internal/models/models';
const { t } = useI18n();
const keybindingStore = useKeybindingStore();
@@ -25,21 +25,21 @@ const keyBindings = computed(() => {
const enabledExtensionIds = new Set(extensionStore.enabledExtensionIds);
return keybindingStore.keyBindings
.filter(kb => kb.enabled && enabledExtensionIds.has(kb.extension))
.filter(kb => kb.enabled && (!kb.extension || enabledExtensionIds.has(kb.extension)))
.map(kb => ({
id: kb.command,
keys: parseKeyBinding(kb.key, kb.command),
category: kb.extension,
description: getCommandDescription(kb.command) || kb.command
id: kb.key,
keys: parseKeyBinding(kb.command || '', kb.key),
category: kb.extension || '',
description: kb.key ? (getCommandDescription(kb.key) || kb.key) : ''
}));
});
// 解析快捷键字符串为显示数组
const parseKeyBinding = (keyStr: string, command?: string): string[] => {
const parseKeyBinding = (keyStr: string, keyBindingKey?: string): string[] => {
if (!keyStr) return [];
// 特殊处理重做快捷键的操作系统差异
if (command === KeyBindingCommand.HistoryRedoCommand && keyStr === 'Mod-Shift-z') {
if (keyBindingKey === KeyBindingKey.HistoryRedoKeyBindingKey && keyStr === 'Mod-Shift-z') {
if (systemStore.isMacOS) {
return ['⌘', '⇧', 'Z']; // macOS: Cmd+Shift+Z
} else {
@@ -48,7 +48,7 @@ const parseKeyBinding = (keyStr: string, command?: string): string[] => {
}
// 特殊处理重做选择快捷键的操作系统差异
if (command === KeyBindingCommand.HistoryRedoSelectionCommand && keyStr === 'Mod-Shift-u') {
if (keyBindingKey === KeyBindingKey.HistoryRedoSelectionKeyBindingKey && keyStr === 'Mod-Shift-u') {
if (systemStore.isMacOS) {
return ['⌘', '⇧', 'U']; // macOS: Cmd+Shift+U
} else {
@@ -57,7 +57,7 @@ const parseKeyBinding = (keyStr: string, command?: string): string[] => {
}
// 特殊处理代码折叠快捷键的操作系统差异
if (command === KeyBindingCommand.FoldCodeCommand && keyStr === 'Ctrl-Shift-[') {
if (keyBindingKey === KeyBindingKey.FoldCodeKeyBindingKey && keyStr === 'Ctrl-Shift-[') {
if (systemStore.isMacOS) {
return ['⌘', '⌥', '[']; // macOS: Cmd+Alt+[
} else {
@@ -65,7 +65,7 @@ const parseKeyBinding = (keyStr: string, command?: string): string[] => {
}
}
if (command === KeyBindingCommand.UnfoldCodeCommand && keyStr === 'Ctrl-Shift-]') {
if (keyBindingKey === KeyBindingKey.UnfoldCodeKeyBindingKey && keyStr === 'Ctrl-Shift-]') {
if (systemStore.isMacOS) {
return ['⌘', '⌥', ']']; // macOS: Cmd+Alt+]
} else {
@@ -74,7 +74,7 @@ const parseKeyBinding = (keyStr: string, command?: string): string[] => {
}
// 特殊处理编辑快捷键的操作系统差异
if (command === KeyBindingCommand.CursorSyntaxLeftCommand && keyStr === 'Alt-ArrowLeft') {
if (keyBindingKey === KeyBindingKey.CursorSyntaxLeftKeyBindingKey && keyStr === 'Alt-ArrowLeft') {
if (systemStore.isMacOS) {
return ['Ctrl', '←']; // macOS: Ctrl+ArrowLeft
} else {
@@ -82,7 +82,7 @@ const parseKeyBinding = (keyStr: string, command?: string): string[] => {
}
}
if (command === KeyBindingCommand.CursorSyntaxRightCommand && keyStr === 'Alt-ArrowRight') {
if (keyBindingKey === KeyBindingKey.CursorSyntaxRightKeyBindingKey && keyStr === 'Alt-ArrowRight') {
if (systemStore.isMacOS) {
return ['Ctrl', '→']; // macOS: Ctrl+ArrowRight
} else {
@@ -90,7 +90,7 @@ const parseKeyBinding = (keyStr: string, command?: string): string[] => {
}
}
if (command === KeyBindingCommand.InsertBlankLineCommand && keyStr === 'Ctrl-Enter') {
if (keyBindingKey === KeyBindingKey.InsertBlankLineKeyBindingKey && keyStr === 'Ctrl-Enter') {
if (systemStore.isMacOS) {
return ['⌘', 'Enter']; // macOS: Cmd+Enter
} else {
@@ -98,7 +98,7 @@ const parseKeyBinding = (keyStr: string, command?: string): string[] => {
}
}
if (command === KeyBindingCommand.SelectLineCommand && keyStr === 'Alt-l') {
if (keyBindingKey === KeyBindingKey.SelectLineKeyBindingKey && keyStr === 'Alt-l') {
if (systemStore.isMacOS) {
return ['Ctrl', 'L']; // macOS: Ctrl+l
} else {
@@ -106,7 +106,7 @@ const parseKeyBinding = (keyStr: string, command?: string): string[] => {
}
}
if (command === KeyBindingCommand.SelectParentSyntaxCommand && keyStr === 'Ctrl-i') {
if (keyBindingKey === KeyBindingKey.SelectParentSyntaxKeyBindingKey && keyStr === 'Ctrl-i') {
if (systemStore.isMacOS) {
return ['⌘', 'I']; // macOS: Cmd+i
} else {
@@ -114,7 +114,7 @@ const parseKeyBinding = (keyStr: string, command?: string): string[] => {
}
}
if (command === KeyBindingCommand.IndentLessCommand && keyStr === 'Ctrl-[') {
if (keyBindingKey === KeyBindingKey.IndentLessKeyBindingKey && keyStr === 'Ctrl-[') {
if (systemStore.isMacOS) {
return ['⌘', '[']; // macOS: Cmd+[
} else {
@@ -122,7 +122,7 @@ const parseKeyBinding = (keyStr: string, command?: string): string[] => {
}
}
if (command === KeyBindingCommand.IndentMoreCommand && keyStr === 'Ctrl-]') {
if (keyBindingKey === KeyBindingKey.IndentMoreKeyBindingKey && keyStr === 'Ctrl-]') {
if (systemStore.isMacOS) {
return ['⌘', ']']; // macOS: Cmd+]
} else {
@@ -130,7 +130,7 @@ const parseKeyBinding = (keyStr: string, command?: string): string[] => {
}
}
if (command === KeyBindingCommand.IndentSelectionCommand && keyStr === 'Ctrl-Alt-\\') {
if (keyBindingKey === KeyBindingKey.IndentSelectionKeyBindingKey && keyStr === 'Ctrl-Alt-\\') {
if (systemStore.isMacOS) {
return ['⌘', '⌥', '\\']; // macOS: Cmd+Alt+\
} else {
@@ -138,7 +138,7 @@ const parseKeyBinding = (keyStr: string, command?: string): string[] => {
}
}
if (command === KeyBindingCommand.CursorMatchingBracketCommand && keyStr === 'Shift-Ctrl-\\') {
if (keyBindingKey === KeyBindingKey.CursorMatchingBracketKeyBindingKey && keyStr === 'Shift-Ctrl-\\') {
if (systemStore.isMacOS) {
return ['⇧', '⌘', '\\']; // macOS: Shift+Cmd+\
} else {
@@ -146,7 +146,7 @@ const parseKeyBinding = (keyStr: string, command?: string): string[] => {
}
}
if (command === KeyBindingCommand.ToggleCommentCommand && keyStr === 'Ctrl-/') {
if (keyBindingKey === KeyBindingKey.ToggleCommentKeyBindingKey && keyStr === 'Ctrl-/') {
if (systemStore.isMacOS) {
return ['⌘', '/']; // macOS: Cmd+/
} else {
@@ -155,7 +155,7 @@ const parseKeyBinding = (keyStr: string, command?: string): string[] => {
}
// 特殊处理删除快捷键的操作系统差异
if (command === KeyBindingCommand.DeleteGroupBackwardCommand && keyStr === 'Ctrl-Backspace') {
if (keyBindingKey === KeyBindingKey.DeleteGroupBackwardKeyBindingKey && keyStr === 'Ctrl-Backspace') {
if (systemStore.isMacOS) {
return ['⌘', 'Backspace']; // macOS: Cmd+Backspace
} else {
@@ -163,7 +163,7 @@ const parseKeyBinding = (keyStr: string, command?: string): string[] => {
}
}
if (command === KeyBindingCommand.DeleteGroupForwardCommand && keyStr === 'Ctrl-Delete') {
if (keyBindingKey === KeyBindingKey.DeleteGroupForwardKeyBindingKey && keyStr === 'Ctrl-Delete') {
if (systemStore.isMacOS) {
return ['⌘', 'Delete']; // macOS: Cmd+Delete
} else {