♻️ Refactor keybinding service

This commit is contained in:
2025-12-20 16:43:04 +08:00
parent 401eb3ab39
commit 7b746155f7
60 changed files with 4526 additions and 1816 deletions

View File

@@ -1,6 +1,7 @@
import {
AppConfig,
AuthMethod,
KeyBindingType,
LanguageType,
SystemThemeType,
TabType,
@@ -31,6 +32,7 @@ export const CONFIG_KEY_MAP = {
enableTabIndent: 'editing.enableTabIndent',
tabSize: 'editing.tabSize',
tabType: 'editing.tabType',
keymapMode: 'editing.keymapMode',
autoSaveDelay: 'editing.autoSaveDelay',
// appearance
language: 'appearance.language',
@@ -95,11 +97,12 @@ export const DEFAULT_CONFIG: AppConfig = {
enableTabIndent: true,
tabSize: CONFIG_LIMITS.tabSize.default,
tabType: CONFIG_LIMITS.tabType.default,
keymapMode: KeyBindingType.Standard,
autoSaveDelay: 5000
},
appearance: {
language: LanguageType.LangZhCN,
systemTheme: SystemThemeType.SystemThemeAuto,
systemTheme: SystemThemeType.SystemThemeDark,
currentTheme: 'default-dark'
},
updates: {

View File

@@ -44,11 +44,18 @@ export default {
auto: 'Follow System'
},
keybindings: {
keymapMode: 'Keymap Mode',
modes: {
standard: 'Standard Mode',
emacs: 'Emacs Mode'
},
headers: {
shortcut: 'Shortcut',
extension: 'Extension',
description: 'Description'
},
resetToDefault: 'Reset to Default',
confirmReset: 'Confirm Reset?',
commands: {
showSearch: 'Show search panel',
hideSearch: 'Hide search panel',
@@ -93,6 +100,25 @@ export default {
insertBlankLine: 'Insert blank line',
selectLine: 'Select line',
selectParentSyntax: 'Select parent syntax',
simplifySelection: 'Simplify selection',
addCursorAbove: 'Add cursor above',
addCursorBelow: 'Add cursor below',
cursorGroupLeft: 'Cursor word left',
cursorGroupRight: 'Cursor word right',
selectGroupLeft: 'Select word left',
selectGroupRight: 'Select word right',
deleteToLineEnd: 'Delete to line end',
deleteToLineStart: 'Delete to line start',
cursorLineStart: 'Cursor to line start',
cursorLineEnd: 'Cursor to line end',
selectLineStart: 'Select to line start',
selectLineEnd: 'Select to line end',
cursorDocStart: 'Cursor to document start',
cursorDocEnd: 'Cursor to document end',
selectDocStart: 'Select to document start',
selectDocEnd: 'Select to document end',
selectMatchingBracket: 'Select to matching bracket',
splitLine: 'Split line',
indentLess: 'Indent less',
indentMore: 'Indent more',
indentSelection: 'Indent selection',
@@ -104,6 +130,18 @@ export default {
deleteCharForward: 'Delete character forward',
deleteGroupBackward: 'Delete group backward',
deleteGroupForward: 'Delete group forward',
// Emacs mode additional basic navigation commands
cursorCharLeft: 'Cursor left one character',
cursorCharRight: 'Cursor right one character',
cursorLineUp: 'Cursor up one line',
cursorLineDown: 'Cursor down one line',
cursorPageUp: 'Page up',
cursorPageDown: 'Page down',
selectCharLeft: 'Select left one character',
selectCharRight: 'Select right one character',
selectLineUp: 'Select up one line',
selectLineDown: 'Select down one line',
}
},
tabs: {

View File

@@ -44,11 +44,18 @@ export default {
auto: '跟随系统'
},
keybindings: {
keymapMode: '快捷键模式',
modes: {
standard: '标准模式',
emacs: 'Emacs 模式'
},
headers: {
shortcut: '快捷键',
extension: '扩展',
description: '描述'
},
resetToDefault: '重置为默认',
confirmReset: '确认重置?',
commands: {
showSearch: '显示搜索面板',
hideSearch: '隐藏搜索面板',
@@ -93,6 +100,25 @@ export default {
insertBlankLine: '插入空行',
selectLine: '选择行',
selectParentSyntax: '选择父级语法',
simplifySelection: '简化选择',
addCursorAbove: '在上方添加光标',
addCursorBelow: '在下方添加光标',
cursorGroupLeft: '光标按单词左移',
cursorGroupRight: '光标按单词右移',
selectGroupLeft: '按单词选择左侧',
selectGroupRight: '按单词选择右侧',
deleteToLineEnd: '删除到行尾',
deleteToLineStart: '删除到行首',
cursorLineStart: '移动到行首',
cursorLineEnd: '移动到行尾',
selectLineStart: '选择到行首',
selectLineEnd: '选择到行尾',
cursorDocStart: '跳转到文档开头',
cursorDocEnd: '跳转到文档结尾',
selectDocStart: '选择到文档开头',
selectDocEnd: '选择到文档结尾',
selectMatchingBracket: '选择到匹配括号',
splitLine: '分割行',
indentLess: '减少缩进',
indentMore: '增加缩进',
indentSelection: '缩进选择',
@@ -104,6 +130,18 @@ export default {
deleteCharForward: '向前删除字符',
deleteGroupBackward: '向后删除组',
deleteGroupForward: '向前删除组',
// Emacs 模式额外的基础导航命令
cursorCharLeft: '光标左移一个字符',
cursorCharRight: '光标右移一个字符',
cursorLineUp: '光标上移一行',
cursorLineDown: '光标下移一行',
cursorPageUp: '向上翻页',
cursorPageDown: '向下翻页',
selectCharLeft: '选择左移一个字符',
selectCharRight: '选择右移一个字符',
selectLineUp: '选择上移一行',
selectLineDown: '选择下移一行',
}
},
tabs: {

View File

@@ -275,6 +275,9 @@ export const useConfigStore = defineStore('config', () => {
// 标签页配置相关方法
setEnableTabs: (value: boolean) => updateConfig('enableTabs', value),
// 快捷键模式配置相关方法
setKeymapMode: (value: any) => updateConfig('keymapMode', value),
// 更新配置相关方法
setAutoUpdate: (value: boolean) => updateConfig('autoUpdate', value),

View File

@@ -204,8 +204,8 @@ export const useDocumentStore = defineStore('document', () => {
await openDocument(currentDocumentId.value);
} else {
// 否则打开第一个文档
if (documents.value[0].id) {
await openDocument(documents.value[0].id);
if (documentList.value[0].id) {
await openDocument(documentList.value[0].id);
}
}
} catch (error) {

View File

@@ -29,6 +29,7 @@ import {generateContentHash} from "@/common/utils/hashUtils";
import {createTimerManager, type TimerManager} from '@/common/utils/timerUtils';
import {EDITOR_CONFIG} from '@/common/constant/editor';
import {createDebounce} from '@/common/utils/debounce';
import {useKeybindingStore} from "@/stores/keybindingStore";
export interface DocumentStats {
lines: number;
@@ -602,28 +603,30 @@ export const useEditorStore = defineStore('editor', () => {
};
// 更新扩展
const updateExtension = async (key: string, enabled: boolean, config?: any) => {
const updateExtension = async (id: number, enabled: boolean, config?: any) => {
// 更新启用状态
await ExtensionService.UpdateExtensionEnabled(key, enabled);
await ExtensionService.UpdateExtensionEnabled(id, enabled);
// 如果需要更新配置
if (config !== undefined) {
await ExtensionService.UpdateExtensionConfig(key, config);
await ExtensionService.UpdateExtensionConfig(id, config);
}
// 重新加载扩展配置
await extensionStore.loadExtensions();
// 获取更新后的扩展名称
const extension = extensionStore.extensions.find(ext => ext.id === id);
if (!extension) return;
// 更新前端编辑器扩展 - 应用于所有实例
const manager = getExtensionManager();
if (manager) {
// 直接更新前端扩展至所有视图
manager.updateExtension(key, enabled, config);
}
// 重新加载扩展配置
await extensionStore.loadExtensions();
if (manager) {
manager.initExtensions(extensionStore.extensions);
manager.updateExtension(extension.name, enabled, config);
}
await useKeybindingStore().loadKeyBindings();
await applyKeymapSettings();
};

View File

@@ -7,22 +7,12 @@ export const useExtensionStore = defineStore('extension', () => {
// 扩展配置数据
const extensions = ref<Extension[]>([]);
// 获取启用的扩展
const enabledExtensions = computed(() =>
extensions.value.filter(ext => ext.enabled)
);
// 获取启用的扩展ID列表 (key)
const enabledExtensionIds = computed(() =>
enabledExtensions.value.map(ext => ext.key).filter((k): k is string => k !== undefined)
);
/**
* 从后端加载扩展配置
*/
const loadExtensions = async (): Promise<void> => {
try {
const result = await ExtensionService.GetAllExtensions();
const result = await ExtensionService.GetExtensions();
extensions.value = result.filter((ext): ext is Extension => ext !== null);
} catch (err) {
console.error('[ExtensionStore] Failed to load extensions:', err);
@@ -32,17 +22,19 @@ export const useExtensionStore = defineStore('extension', () => {
/**
* 获取扩展配置
*/
const getExtensionConfig = (key: string): any => {
const extension = extensions.value.find(ext => ext.key === key);
return extension?.config ?? {};
const getExtensionConfig = async (id: number): Promise<any> => {
try {
const config = await ExtensionService.GetExtensionConfig(id);
return config ?? {};
} catch (err) {
console.error('[ExtensionStore] Failed to get extension config:', err);
return {};
}
};
return {
// 状态
extensions,
enabledExtensions,
enabledExtensionIds,
// 方法
loadExtensions,
getExtensionConfig,

View File

@@ -1,86 +1,38 @@
import {defineStore} from 'pinia';
import {computed, ref} from 'vue';
import {KeyBinding} from '@/../bindings/voidraft/internal/models/ent/models';
import {GetAllKeyBindings} from '@/../bindings/voidraft/internal/services/keybindingservice';
import {KeyBindingService} from '@/../bindings/voidraft/internal/services';
import {KeyBindingType} from '@/../bindings/voidraft/internal/models/models';
import {useConfigStore} from './configStore';
export const useKeybindingStore = defineStore('keybinding', () => {
const configStore = useConfigStore();
// 快捷键配置数据
const keyBindings = ref<KeyBinding[]>([]);
// 获取启用的快捷键
const enabledKeyBindings = computed(() =>
keyBindings.value.filter(kb => kb.enabled)
);
// 按扩展分组的快捷键
const keyBindingsByExtension = computed(() => {
const groups = new Map<string, KeyBinding[]>();
for (const binding of keyBindings.value) {
const ext = binding.extension || '';
if (!groups.has(ext)) {
groups.set(ext, []);
}
groups.get(ext)!.push(binding);
}
return groups;
});
// 获取指定扩展的快捷键
const getKeyBindingsByExtension = computed(() =>
(extension: string) =>
keyBindings.value.filter(kb => kb.extension === extension)
);
// 按命令获取快捷键
const getKeyBindingByCommand = computed(() =>
(command: string) =>
keyBindings.value.find(kb => kb.command === command)
);
/**
* 从后端加载快捷键配置
* 从后端加载快捷键配置(根据当前配置的模式)
*/
const loadKeyBindings = async (): Promise<void> => {
const result = await GetAllKeyBindings();
const keymapMode = configStore.config.editing.keymapMode || KeyBindingType.Standard;
const result = await KeyBindingService.GetKeyBindings(keymapMode);
keyBindings.value = result.filter((kb): kb is KeyBinding => kb !== null);
};
/**
* 检查是否存在指定命令的快捷键
* 更新快捷键绑定
*/
const hasCommand = (command: string): boolean => {
return keyBindings.value.some(kb => kb.command === command && kb.enabled);
const updateKeyBinding = async (id: number, key: string): Promise<void> => {
await KeyBindingService.UpdateKeyBindingKeys(id, key);
await loadKeyBindings();
};
/**
* 获取扩展相关的所有扩展ID
*/
const getAllExtensionIds = computed(() => {
const extensionIds = new Set<string>();
for (const binding of keyBindings.value) {
if (binding.extension) {
extensionIds.add(binding.extension);
}
}
return Array.from(extensionIds);
});
return {
// 状态
keyBindings,
enabledKeyBindings,
keyBindingsByExtension,
getAllExtensionIds,
// 计算属性
getKeyBindingByCommand,
getKeyBindingsByExtension,
// 方法
loadKeyBindings,
hasCommand,
updateKeyBinding,
};
});

View File

@@ -8,45 +8,19 @@ import {useEditorStore} from './editorStore';
import type {ThemeColors} from '@/views/editor/theme/types';
import {cloneThemeColors, FALLBACK_THEME_NAME, themePresetList, themePresetMap} from '@/views/editor/theme/presets';
type ThemeColorConfig = { [_: string]: any };
type ThemeOption = { name: string; type: ThemeType };
// 类型定义
type ThemeOption = {name: string; type: ThemeType};
const resolveThemeName = (name?: string) =>
// 解析主题名称,确保返回有效的主题
const resolveThemeName = (name?: string): string =>
name && themePresetMap[name] ? name : FALLBACK_THEME_NAME;
// 根据主题类型创建主题选项列表
const createThemeOptions = (type: ThemeType): ThemeOption[] =>
themePresetList
.filter(preset => preset.type === type)
.map(preset => ({name: preset.name, type: preset.type}));
const darkThemeOptions = createThemeOptions(ThemeType.TypeDark);
const lightThemeOptions = createThemeOptions(ThemeType.TypeLight);
const cloneColors = (colors: ThemeColorConfig): ThemeColors =>
JSON.parse(JSON.stringify(colors)) as ThemeColors;
const getPresetColors = (name: string): ThemeColors => {
const preset = themePresetMap[name] ?? themePresetMap[FALLBACK_THEME_NAME];
const colors = cloneThemeColors(preset.colors);
colors.themeName = name;
return colors;
};
const fetchThemeColors = async (themeName: string): Promise<ThemeColors> => {
const safeName = resolveThemeName(themeName);
try {
const theme = await ThemeService.GetThemeByKey(safeName);
if (theme?.colors) {
const colors = cloneColors(theme.colors);
colors.themeName = safeName;
return colors;
}
} catch (error) {
console.error('Failed to load theme override:', error);
}
return getPresetColors(safeName);
};
export const useThemeStore = defineStore('theme', () => {
const configStore = useConfigStore();
const currentColors = ref<ThemeColors | null>(null);
@@ -62,10 +36,12 @@ export const useThemeStore = defineStore('theme', () => {
window.matchMedia('(prefers-color-scheme: dark)').matches)
);
// 根据当前模式动态计算可用主题列表
const availableThemes = computed<ThemeOption[]>(() =>
isDarkMode.value ? darkThemeOptions : lightThemeOptions
createThemeOptions(isDarkMode.value ? ThemeType.TypeDark : ThemeType.TypeLight)
);
// 应用主题到 DOM
const applyThemeToDOM = (theme: SystemThemeType) => {
const themeMap = {
[SystemThemeType.SystemThemeAuto]: 'auto',
@@ -75,6 +51,31 @@ export const useThemeStore = defineStore('theme', () => {
document.documentElement.setAttribute('data-theme', themeMap[theme]);
};
// 获取预设主题颜色
const getPresetColors = (name: string): ThemeColors => {
const preset = themePresetMap[name] ?? themePresetMap[FALLBACK_THEME_NAME];
const colors = cloneThemeColors(preset.colors);
colors.themeName = name;
return colors;
};
// 从服务器获取主题颜色
const fetchThemeColors = async (themeName: string): Promise<ThemeColors> => {
const safeName = resolveThemeName(themeName);
try {
const theme = await ThemeService.GetThemeByName(safeName);
if (theme?.colors) {
const colors = cloneThemeColors(theme.colors as ThemeColors);
colors.themeName = safeName;
return colors;
}
} catch (error) {
console.error('Failed to load theme override:', error);
}
return getPresetColors(safeName);
};
// 加载主题颜色
const loadThemeColors = async (themeName?: string) => {
const targetName = resolveThemeName(
themeName || configStore.config?.appearance?.currentTheme
@@ -82,17 +83,21 @@ export const useThemeStore = defineStore('theme', () => {
currentColors.value = await fetchThemeColors(targetName);
};
// 初始化主题
const initTheme = async () => {
applyThemeToDOM(currentTheme.value);
await loadThemeColors();
refreshEditorTheme();
};
// 设置系统主题
const setTheme = async (theme: SystemThemeType) => {
await configStore.setSystemTheme(theme);
applyThemeToDOM(theme);
refreshEditorTheme();
};
// 切换到指定主题
const switchToTheme = async (themeName: string) => {
if (!themePresetMap[themeName]) {
console.error('Theme not found:', themeName);
@@ -105,11 +110,13 @@ export const useThemeStore = defineStore('theme', () => {
return true;
};
// 更新当前主题颜色
const updateCurrentColors = (colors: Partial<ThemeColors>) => {
if (!currentColors.value) return;
Object.assign(currentColors.value, colors);
};
// 保存当前主题
const saveCurrentTheme = async () => {
if (!currentColors.value) {
throw new Error('No theme selected');
@@ -118,13 +125,14 @@ export const useThemeStore = defineStore('theme', () => {
const themeName = resolveThemeName(currentColors.value.themeName);
currentColors.value.themeName = themeName;
await ThemeService.UpdateTheme(themeName, currentColors.value as ThemeColorConfig);
await ThemeService.UpdateTheme(themeName, currentColors.value);
await loadThemeColors(themeName);
refreshEditorTheme();
return true;
};
// 重置当前主题到默认值
const resetCurrentTheme = async () => {
if (!currentColors.value) {
throw new Error('No theme selected');
@@ -138,6 +146,7 @@ export const useThemeStore = defineStore('theme', () => {
return true;
};
// 刷新编辑器主题
const refreshEditorTheme = () => {
applyThemeToDOM(currentTheme.value);
const editorStore = useEditorStore();

View File

@@ -1,140 +1,136 @@
import { EditorView } from '@codemirror/view';
import { Extension } from '@codemirror/state';
import { copyCommand, cutCommand, pasteCommand } from '../codeblock/copyPaste';
import { KeyBindingKey } from '@/../bindings/voidraft/internal/models/models';
import { useKeybindingStore } from '@/stores/keybindingStore';
import { undo, redo } from '@codemirror/commands';
import {EditorView} from '@codemirror/view';
import {Extension} from '@codemirror/state';
import {copyCommand, cutCommand, pasteCommand} from '../codeblock/copyPaste';
import {KeyBindingName} from '@/../bindings/voidraft/internal/models/models';
import {useKeybindingStore} from '@/stores/keybindingStore';
import {redo, undo} from '@codemirror/commands';
import i18n from '@/i18n';
import { useSystemStore } from '@/stores/systemStore';
import { showContextMenu } from './manager';
import {
buildRegisteredMenu,
createMenuContext,
registerMenuNodes
} from './menuSchema';
import type { MenuSchemaNode } from './menuSchema';
import {useSystemStore} from '@/stores/systemStore';
import {showContextMenu} from './manager';
import type {MenuSchemaNode} from './menuSchema';
import {buildRegisteredMenu, createMenuContext, registerMenuNodes} from './menuSchema';
function t(key: string): string {
return i18n.global.t(key);
return i18n.global.t(key);
}
function formatKeyBinding(keyBinding: string): string {
const systemStore = useSystemStore();
const isMac = systemStore.isMacOS;
const systemStore = useSystemStore();
const isMac = systemStore.isMacOS;
return keyBinding
.replace("Mod", isMac ? "Cmd" : "Ctrl")
.replace("Alt", isMac ? "Option" : "Alt")
.replace(/-/g, " + ");
return keyBinding
.replace("Mod", isMac ? "Cmd" : "Ctrl")
.replace("Alt", isMac ? "Option" : "Alt")
.replace(/-/g, " + ");
}
const shortcutCache = new Map<KeyBindingKey, string>();
const shortcutCache = new Map<KeyBindingName, string>();
function getShortcutText(keyBindingKey?: KeyBindingKey): string {
if (keyBindingKey === undefined) {
return "";
}
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.key === keyBindingKey && kb.enabled
);
if (binding?.command) {
const formatted = formatKeyBinding(binding.command);
shortcutCache.set(keyBindingKey, formatted);
return formatted;
function getShortcutText(keyBindingKey?: KeyBindingName): string {
if (keyBindingKey === undefined) {
return "";
}
} catch (error) {
console.warn("An error occurred while getting the shortcut:", error);
}
shortcutCache.set(keyBindingKey, "");
return "";
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.key === keyBindingKey && kb.enabled
);
if (binding?.key) {
const formatted = formatKeyBinding(binding.key);
shortcutCache.set(keyBindingKey, formatted);
return formatted;
}
} catch (error) {
console.warn("An error occurred while getting the shortcut:", error);
}
shortcutCache.set(keyBindingKey, "");
return "";
}
function builtinMenuNodes(): MenuSchemaNode[] {
return [
{
id: "copy",
labelKey: "keybindings.commands.blockCopy",
command: copyCommand,
keyBindingKey: KeyBindingKey.BlockCopyKeyBindingKey,
enabled: (context) => context.hasSelection
},
{
id: "cut",
labelKey: "keybindings.commands.blockCut",
command: cutCommand,
keyBindingKey: KeyBindingKey.BlockCutKeyBindingKey,
visible: (context) => context.isEditable,
enabled: (context) => context.hasSelection && context.isEditable
},
{
id: "paste",
labelKey: "keybindings.commands.blockPaste",
command: pasteCommand,
keyBindingKey: KeyBindingKey.BlockPasteKeyBindingKey,
visible: (context) => context.isEditable
},
{
id: "undo",
labelKey: "keybindings.commands.historyUndo",
command: undo,
keyBindingKey: KeyBindingKey.HistoryUndoKeyBindingKey,
visible: (context) => context.isEditable
},
{
id: "redo",
labelKey: "keybindings.commands.historyRedo",
command: redo,
keyBindingKey: KeyBindingKey.HistoryRedoKeyBindingKey,
visible: (context) => context.isEditable
}
];
return [
{
id: "copy",
labelKey: "keybindings.commands.blockCopy",
command: copyCommand,
keyBindingName: KeyBindingName.BlockCopy,
enabled: (context) => context.hasSelection
},
{
id: "cut",
labelKey: "keybindings.commands.blockCut",
command: cutCommand,
keyBindingName: KeyBindingName.BlockCut,
visible: (context) => context.isEditable,
enabled: (context) => context.hasSelection && context.isEditable
},
{
id: "paste",
labelKey: "keybindings.commands.blockPaste",
command: pasteCommand,
keyBindingName: KeyBindingName.BlockPaste,
visible: (context) => context.isEditable
},
{
id: "undo",
labelKey: "keybindings.commands.historyUndo",
command: undo,
keyBindingName: KeyBindingName.HistoryUndo,
visible: (context) => context.isEditable
},
{
id: "redo",
labelKey: "keybindings.commands.historyRedo",
command: redo,
keyBindingName: KeyBindingName.HistoryRedo,
visible: (context) => context.isEditable
}
];
}
let builtinMenuRegistered = false;
function ensureBuiltinMenuRegistered(): void {
if (builtinMenuRegistered) return;
registerMenuNodes(builtinMenuNodes());
builtinMenuRegistered = true;
if (builtinMenuRegistered) return;
registerMenuNodes(builtinMenuNodes());
builtinMenuRegistered = true;
}
export function createEditorContextMenu(): Extension {
ensureBuiltinMenuRegistered();
ensureBuiltinMenuRegistered();
return EditorView.domEventHandlers({
contextmenu: (event, view) => {
event.preventDefault();
return EditorView.domEventHandlers({
contextmenu: (event, view) => {
event.preventDefault();
const context = createMenuContext(view, event as MouseEvent);
const menuItems = buildRegisteredMenu(context, {
translate: t,
formatShortcut: getShortcutText
});
const context = createMenuContext(view, event as MouseEvent);
const menuItems = buildRegisteredMenu(context, {
translate: t,
formatShortcut: getShortcutText
});
if (menuItems.length === 0) {
return false;
}
if (menuItems.length === 0) {
return false;
}
showContextMenu(view, event.clientX, event.clientY, menuItems);
return true;
}
});
showContextMenu(view, event.clientX, event.clientY, menuItems);
return true;
}
});
}
export default createEditorContextMenu;

View File

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

View File

@@ -65,9 +65,15 @@ export function handleCodeBlock(
if (ctx.seen.has(nf)) return;
ctx.seen.add(nf);
ranges.push([nf, nt]);
// When cursor/selection is in this code block, don't add any decorations
// This allows the selection background to be visible
if (inCursor) return;
const startLine = ctx.view.state.doc.lineAt(nf);
const endLine = ctx.view.state.doc.lineAt(nt);
// Add background decorations for each line
for (let num = startLine.number; num <= endLine.number; num++) {
const line = ctx.view.state.doc.line(num);
let deco = DECO_CODEBLOCK_LINE;
@@ -76,14 +82,14 @@ export function handleCodeBlock(
else if (num === endLine.number) deco = DECO_CODEBLOCK_END;
ctx.items.push({ from: line.from, to: line.from, deco });
}
if (!inCursor) {
const codeInfo = node.getChild('CodeInfo');
const codeMarks = node.getChildren('CodeMark');
const language = codeInfo ? ctx.view.state.doc.sliceString(codeInfo.from, codeInfo.to).trim() : null;
ctx.items.push({ from: startLine.to, to: startLine.to, deco: Decoration.widget({ widget: new CodeBlockInfoWidget(nf, nt, language), side: 1 }), priority: 1 });
if (codeInfo) ctx.items.push({ from: codeInfo.from, to: codeInfo.to, deco: invisibleDecoration });
for (const mark of codeMarks) ctx.items.push({ from: mark.from, to: mark.to, deco: invisibleDecoration });
}
// Add language info widget and hide code marks
const codeInfo = node.getChild('CodeInfo');
const codeMarks = node.getChildren('CodeMark');
const language = codeInfo ? ctx.view.state.doc.sliceString(codeInfo.from, codeInfo.to).trim() : null;
ctx.items.push({ from: startLine.to, to: startLine.to, deco: Decoration.widget({ widget: new CodeBlockInfoWidget(nf, nt, language), side: 1 }), priority: 1 });
if (codeInfo) ctx.items.push({ from: codeInfo.from, to: codeInfo.to, deco: invisibleDecoration });
for (const mark of codeMarks) ctx.items.push({ from: mark.from, to: mark.to, deco: invisibleDecoration });
}
/**

View File

@@ -18,8 +18,22 @@ import {moveLineDown, moveLineUp} from '../extensions/codeblock/moveLines';
import {transposeChars} from '../extensions/codeblock';
import {copyCommand, cutCommand, pasteCommand} from '../extensions/codeblock/copyPaste';
import {
addCursorAbove,
addCursorBelow,
copyLineDown,
copyLineUp,
cursorCharLeft,
cursorCharRight,
cursorLineDown,
cursorLineUp,
cursorPageDown,
cursorPageUp,
cursorDocEnd,
cursorDocStart,
cursorGroupLeft,
cursorGroupRight,
cursorLineEnd,
cursorLineStart,
cursorMatchingBracket,
cursorSyntaxLeft,
cursorSyntaxRight,
@@ -27,6 +41,8 @@ import {
deleteCharForward,
deleteGroupBackward,
deleteGroupForward,
deleteToLineEnd,
deleteToLineStart,
indentLess,
indentMore,
indentSelection,
@@ -34,10 +50,23 @@ import {
insertNewlineAndIndent,
redo,
redoSelection,
selectCharLeft,
selectCharRight,
selectLineDown,
selectLineUp,
selectDocEnd,
selectDocStart,
selectGroupLeft,
selectGroupRight,
selectLine,
selectLineEnd,
selectLineStart,
selectMatchingBracket,
selectParentSyntax,
selectSyntaxLeft,
selectSyntaxRight,
simplifySelection,
splitLine,
toggleBlockComment,
toggleComment,
undo,
@@ -45,9 +74,8 @@ import {
} from '@codemirror/commands';
import {foldAll, foldCode, unfoldAll, unfoldCode} from '@codemirror/language';
import i18n from '@/i18n';
import {KeyBindingKey} from '@/../bindings/voidraft/internal/models/models';
import {KeyBindingName} from '@/../bindings/voidraft/internal/models/models';
// 默认代码块扩展选项
const defaultBlockExtensionOptions = {
defaultBlockToken: 'text',
defaultBlockAutoDetect: true,
@@ -58,202 +86,320 @@ const defaultBlockExtensionOptions = {
* 将后端定义的key字段映射到具体的前端方法和翻译键
*/
export const commands: Record<string, { handler: any; descriptionKey: string }> = {
[KeyBindingKey.ShowSearchKeyBindingKey]: {
[KeyBindingName.ShowSearch]: {
handler: openSearchPanel,
descriptionKey: 'keybindings.commands.showSearch'
},
[KeyBindingKey.HideSearchKeyBindingKey]: {
[KeyBindingName.HideSearch]: {
handler: closeSearchPanel,
descriptionKey: 'keybindings.commands.hideSearch'
},
[KeyBindingKey.BlockSelectAllKeyBindingKey]: {
[KeyBindingName.BlockSelectAll]: {
handler: selectAll,
descriptionKey: 'keybindings.commands.blockSelectAll'
},
[KeyBindingKey.BlockAddAfterCurrentKeyBindingKey]: {
[KeyBindingName.BlockAddAfterCurrent]: {
handler: addNewBlockAfterCurrent(defaultBlockExtensionOptions),
descriptionKey: 'keybindings.commands.blockAddAfterCurrent'
},
[KeyBindingKey.BlockAddAfterLastKeyBindingKey]: {
[KeyBindingName.BlockAddAfterLast]: {
handler: addNewBlockAfterLast(defaultBlockExtensionOptions),
descriptionKey: 'keybindings.commands.blockAddAfterLast'
},
[KeyBindingKey.BlockAddBeforeCurrentKeyBindingKey]: {
[KeyBindingName.BlockAddBeforeCurrent]: {
handler: addNewBlockBeforeCurrent(defaultBlockExtensionOptions),
descriptionKey: 'keybindings.commands.blockAddBeforeCurrent'
},
[KeyBindingKey.BlockGotoPreviousKeyBindingKey]: {
[KeyBindingName.BlockGotoPrevious]: {
handler: gotoPreviousBlock,
descriptionKey: 'keybindings.commands.blockGotoPrevious'
},
[KeyBindingKey.BlockGotoNextKeyBindingKey]: {
[KeyBindingName.BlockGotoNext]: {
handler: gotoNextBlock,
descriptionKey: 'keybindings.commands.blockGotoNext'
},
[KeyBindingKey.BlockSelectPreviousKeyBindingKey]: {
[KeyBindingName.BlockSelectPrevious]: {
handler: selectPreviousBlock,
descriptionKey: 'keybindings.commands.blockSelectPrevious'
},
[KeyBindingKey.BlockSelectNextKeyBindingKey]: {
[KeyBindingName.BlockSelectNext]: {
handler: selectNextBlock,
descriptionKey: 'keybindings.commands.blockSelectNext'
},
[KeyBindingKey.BlockDeleteKeyBindingKey]: {
[KeyBindingName.BlockDelete]: {
handler: deleteBlock(defaultBlockExtensionOptions),
descriptionKey: 'keybindings.commands.blockDelete'
},
[KeyBindingKey.BlockMoveUpKeyBindingKey]: {
[KeyBindingName.BlockMoveUp]: {
handler: moveCurrentBlockUp,
descriptionKey: 'keybindings.commands.blockMoveUp'
},
[KeyBindingKey.BlockMoveDownKeyBindingKey]: {
[KeyBindingName.BlockMoveDown]: {
handler: moveCurrentBlockDown,
descriptionKey: 'keybindings.commands.blockMoveDown'
},
[KeyBindingKey.BlockDeleteLineKeyBindingKey]: {
[KeyBindingName.BlockDeleteLine]: {
handler: deleteLineCommand,
descriptionKey: 'keybindings.commands.blockDeleteLine'
},
[KeyBindingKey.BlockMoveLineUpKeyBindingKey]: {
[KeyBindingName.BlockMoveLineUp]: {
handler: moveLineUp,
descriptionKey: 'keybindings.commands.blockMoveLineUp'
},
[KeyBindingKey.BlockMoveLineDownKeyBindingKey]: {
[KeyBindingName.BlockMoveLineDown]: {
handler: moveLineDown,
descriptionKey: 'keybindings.commands.blockMoveLineDown'
},
[KeyBindingKey.BlockTransposeCharsKeyBindingKey]: {
[KeyBindingName.BlockTransposeChars]: {
handler: transposeChars,
descriptionKey: 'keybindings.commands.blockTransposeChars'
},
[KeyBindingKey.BlockFormatKeyBindingKey]: {
[KeyBindingName.BlockFormat]: {
handler: formatCurrentBlock,
descriptionKey: 'keybindings.commands.blockFormat'
},
[KeyBindingKey.BlockCopyKeyBindingKey]: {
[KeyBindingName.BlockCopy]: {
handler: copyCommand,
descriptionKey: 'keybindings.commands.blockCopy'
},
[KeyBindingKey.BlockCutKeyBindingKey]: {
[KeyBindingName.BlockCut]: {
handler: cutCommand,
descriptionKey: 'keybindings.commands.blockCut'
},
[KeyBindingKey.BlockPasteKeyBindingKey]: {
[KeyBindingName.BlockPaste]: {
handler: pasteCommand,
descriptionKey: 'keybindings.commands.blockPaste'
},
[KeyBindingKey.HistoryUndoKeyBindingKey]: {
[KeyBindingName.HistoryUndo]: {
handler: undo,
descriptionKey: 'keybindings.commands.historyUndo'
},
[KeyBindingKey.HistoryRedoKeyBindingKey]: {
[KeyBindingName.HistoryRedo]: {
handler: redo,
descriptionKey: 'keybindings.commands.historyRedo'
},
[KeyBindingKey.HistoryUndoSelectionKeyBindingKey]: {
[KeyBindingName.HistoryUndoSelection]: {
handler: undoSelection,
descriptionKey: 'keybindings.commands.historyUndoSelection'
},
[KeyBindingKey.HistoryRedoSelectionKeyBindingKey]: {
[KeyBindingName.HistoryRedoSelection]: {
handler: redoSelection,
descriptionKey: 'keybindings.commands.historyRedoSelection'
},
[KeyBindingKey.FoldCodeKeyBindingKey]: {
[KeyBindingName.FoldCode]: {
handler: foldCode,
descriptionKey: 'keybindings.commands.foldCode'
},
[KeyBindingKey.UnfoldCodeKeyBindingKey]: {
[KeyBindingName.UnfoldCode]: {
handler: unfoldCode,
descriptionKey: 'keybindings.commands.unfoldCode'
},
[KeyBindingKey.FoldAllKeyBindingKey]: {
[KeyBindingName.FoldAll]: {
handler: foldAll,
descriptionKey: 'keybindings.commands.foldAll'
},
[KeyBindingKey.UnfoldAllKeyBindingKey]: {
[KeyBindingName.UnfoldAll]: {
handler: unfoldAll,
descriptionKey: 'keybindings.commands.unfoldAll'
},
[KeyBindingKey.CursorSyntaxLeftKeyBindingKey]: {
[KeyBindingName.CursorSyntaxLeft]: {
handler: cursorSyntaxLeft,
descriptionKey: 'keybindings.commands.cursorSyntaxLeft'
},
[KeyBindingKey.CursorSyntaxRightKeyBindingKey]: {
[KeyBindingName.CursorSyntaxRight]: {
handler: cursorSyntaxRight,
descriptionKey: 'keybindings.commands.cursorSyntaxRight'
},
[KeyBindingKey.SelectSyntaxLeftKeyBindingKey]: {
[KeyBindingName.SelectSyntaxLeft]: {
handler: selectSyntaxLeft,
descriptionKey: 'keybindings.commands.selectSyntaxLeft'
},
[KeyBindingKey.SelectSyntaxRightKeyBindingKey]: {
[KeyBindingName.SelectSyntaxRight]: {
handler: selectSyntaxRight,
descriptionKey: 'keybindings.commands.selectSyntaxRight'
},
[KeyBindingKey.CopyLineUpKeyBindingKey]: {
[KeyBindingName.CopyLineUp]: {
handler: copyLineUp,
descriptionKey: 'keybindings.commands.copyLineUp'
},
[KeyBindingKey.CopyLineDownKeyBindingKey]: {
[KeyBindingName.CopyLineDown]: {
handler: copyLineDown,
descriptionKey: 'keybindings.commands.copyLineDown'
},
[KeyBindingKey.InsertBlankLineKeyBindingKey]: {
[KeyBindingName.InsertBlankLine]: {
handler: insertBlankLine,
descriptionKey: 'keybindings.commands.insertBlankLine'
},
[KeyBindingKey.SelectLineKeyBindingKey]: {
[KeyBindingName.SelectLine]: {
handler: selectLine,
descriptionKey: 'keybindings.commands.selectLine'
},
[KeyBindingKey.SelectParentSyntaxKeyBindingKey]: {
[KeyBindingName.SelectParentSyntax]: {
handler: selectParentSyntax,
descriptionKey: 'keybindings.commands.selectParentSyntax'
},
[KeyBindingKey.IndentLessKeyBindingKey]: {
[KeyBindingName.SimplifySelection]: {
handler: simplifySelection,
descriptionKey: 'keybindings.commands.simplifySelection'
},
[KeyBindingName.AddCursorAbove]: {
handler: addCursorAbove,
descriptionKey: 'keybindings.commands.addCursorAbove'
},
[KeyBindingName.AddCursorBelow]: {
handler: addCursorBelow,
descriptionKey: 'keybindings.commands.addCursorBelow'
},
[KeyBindingName.CursorGroupLeft]: {
handler: cursorGroupLeft,
descriptionKey: 'keybindings.commands.cursorGroupLeft'
},
[KeyBindingName.CursorGroupRight]: {
handler: cursorGroupRight,
descriptionKey: 'keybindings.commands.cursorGroupRight'
},
[KeyBindingName.SelectGroupLeft]: {
handler: selectGroupLeft,
descriptionKey: 'keybindings.commands.selectGroupLeft'
},
[KeyBindingName.SelectGroupRight]: {
handler: selectGroupRight,
descriptionKey: 'keybindings.commands.selectGroupRight'
},
[KeyBindingName.DeleteToLineEnd]: {
handler: deleteToLineEnd,
descriptionKey: 'keybindings.commands.deleteToLineEnd'
},
[KeyBindingName.DeleteToLineStart]: {
handler: deleteToLineStart,
descriptionKey: 'keybindings.commands.deleteToLineStart'
},
[KeyBindingName.CursorLineStart]: {
handler: cursorLineStart,
descriptionKey: 'keybindings.commands.cursorLineStart'
},
[KeyBindingName.CursorLineEnd]: {
handler: cursorLineEnd,
descriptionKey: 'keybindings.commands.cursorLineEnd'
},
[KeyBindingName.SelectLineStart]: {
handler: selectLineStart,
descriptionKey: 'keybindings.commands.selectLineStart'
},
[KeyBindingName.SelectLineEnd]: {
handler: selectLineEnd,
descriptionKey: 'keybindings.commands.selectLineEnd'
},
[KeyBindingName.CursorDocStart]: {
handler: cursorDocStart,
descriptionKey: 'keybindings.commands.cursorDocStart'
},
[KeyBindingName.CursorDocEnd]: {
handler: cursorDocEnd,
descriptionKey: 'keybindings.commands.cursorDocEnd'
},
[KeyBindingName.SelectDocStart]: {
handler: selectDocStart,
descriptionKey: 'keybindings.commands.selectDocStart'
},
[KeyBindingName.SelectDocEnd]: {
handler: selectDocEnd,
descriptionKey: 'keybindings.commands.selectDocEnd'
},
[KeyBindingName.SelectMatchingBracket]: {
handler: selectMatchingBracket,
descriptionKey: 'keybindings.commands.selectMatchingBracket'
},
[KeyBindingName.SplitLine]: {
handler: splitLine,
descriptionKey: 'keybindings.commands.splitLine'
},
[KeyBindingName.IndentLess]: {
handler: indentLess,
descriptionKey: 'keybindings.commands.indentLess'
},
[KeyBindingKey.IndentMoreKeyBindingKey]: {
[KeyBindingName.IndentMore]: {
handler: indentMore,
descriptionKey: 'keybindings.commands.indentMore'
},
[KeyBindingKey.IndentSelectionKeyBindingKey]: {
[KeyBindingName.IndentSelection]: {
handler: indentSelection,
descriptionKey: 'keybindings.commands.indentSelection'
},
[KeyBindingKey.CursorMatchingBracketKeyBindingKey]: {
[KeyBindingName.CursorMatchingBracket]: {
handler: cursorMatchingBracket,
descriptionKey: 'keybindings.commands.cursorMatchingBracket'
},
[KeyBindingKey.ToggleCommentKeyBindingKey]: {
[KeyBindingName.ToggleComment]: {
handler: toggleComment,
descriptionKey: 'keybindings.commands.toggleComment'
},
[KeyBindingKey.ToggleBlockCommentKeyBindingKey]: {
[KeyBindingName.ToggleBlockComment]: {
handler: toggleBlockComment,
descriptionKey: 'keybindings.commands.toggleBlockComment'
},
[KeyBindingKey.InsertNewlineAndIndentKeyBindingKey]: {
[KeyBindingName.InsertNewlineAndIndent]: {
handler: insertNewlineAndIndent,
descriptionKey: 'keybindings.commands.insertNewlineAndIndent'
},
[KeyBindingKey.DeleteCharBackwardKeyBindingKey]: {
[KeyBindingName.DeleteCharBackward]: {
handler: deleteCharBackward,
descriptionKey: 'keybindings.commands.deleteCharBackward'
},
[KeyBindingKey.DeleteCharForwardKeyBindingKey]: {
[KeyBindingName.DeleteCharForward]: {
handler: deleteCharForward,
descriptionKey: 'keybindings.commands.deleteCharForward'
},
[KeyBindingKey.DeleteGroupBackwardKeyBindingKey]: {
[KeyBindingName.DeleteGroupBackward]: {
handler: deleteGroupBackward,
descriptionKey: 'keybindings.commands.deleteGroupBackward'
},
[KeyBindingKey.DeleteGroupForwardKeyBindingKey]: {
[KeyBindingName.DeleteGroupForward]: {
handler: deleteGroupForward,
descriptionKey: 'keybindings.commands.deleteGroupForward'
},
// Emacs 模式额外的基础导航命令
[KeyBindingName.CursorCharLeft]: {
handler: cursorCharLeft,
descriptionKey: 'keybindings.commands.cursorCharLeft'
},
[KeyBindingName.CursorCharRight]: {
handler: cursorCharRight,
descriptionKey: 'keybindings.commands.cursorCharRight'
},
[KeyBindingName.CursorLineUp]: {
handler: cursorLineUp,
descriptionKey: 'keybindings.commands.cursorLineUp'
},
[KeyBindingName.CursorLineDown]: {
handler: cursorLineDown,
descriptionKey: 'keybindings.commands.cursorLineDown'
},
[KeyBindingName.CursorPageUp]: {
handler: cursorPageUp,
descriptionKey: 'keybindings.commands.cursorPageUp'
},
[KeyBindingName.CursorPageDown]: {
handler: cursorPageDown,
descriptionKey: 'keybindings.commands.cursorPageDown'
},
[KeyBindingName.SelectCharLeft]: {
handler: selectCharLeft,
descriptionKey: 'keybindings.commands.selectCharLeft'
},
[KeyBindingName.SelectCharRight]: {
handler: selectCharRight,
descriptionKey: 'keybindings.commands.selectCharRight'
},
[KeyBindingName.SelectLineUp]: {
handler: selectLineUp,
descriptionKey: 'keybindings.commands.selectLineUp'
},
[KeyBindingName.SelectLineDown]: {
handler: selectLineDown,
descriptionKey: 'keybindings.commands.selectLineDown'
},
};
/**

View File

@@ -27,5 +27,4 @@ export const updateKeymapExtension = (view: any): void => {
// 导出相关模块
export { Manager } from './manager';
export { commands, getCommandHandler, getCommandDescription, isCommandRegistered, getRegisteredCommands } from './commands';
export type { KeyBinding, CommandHandler, CommandDefinition, KeymapResult } from './types';
export { commands, getCommandHandler, getCommandDescription, isCommandRegistered, getRegisteredCommands } from './commands';

View File

@@ -1,7 +1,6 @@
import {keymap} from '@codemirror/view';
import {Extension, Compartment} from '@codemirror/state';
import {KeyBinding, keymap} from '@codemirror/view';
import {Compartment, Extension} from '@codemirror/state';
import {KeyBinding as KeyBindingConfig} from '@/../bindings/voidraft/internal/models/ent/models';
import {KeyBinding, KeymapResult} from './types';
import {getCommandHandler, isCommandRegistered} from './commands';
/**
@@ -10,13 +9,13 @@ import {getCommandHandler, isCommandRegistered} from './commands';
*/
export class Manager {
private static compartment = new Compartment();
/**
* 将后端快捷键配置转换为CodeMirror快捷键绑定
* @param keyBindings 后端快捷键配置列表
* @returns 转换结果
*/
static convertToKeyBindings(keyBindings: KeyBindingConfig[]): KeymapResult {
static convertToKeyBindings(keyBindings: KeyBindingConfig[]): KeyBinding[] {
const result: KeyBinding[] = [];
for (const binding of keyBindings) {
@@ -25,29 +24,31 @@ export class Manager {
continue;
}
// 检查命令是否已注册(使用 key 字段作为命令标识符)
if (!binding.key || !isCommandRegistered(binding.key)) {
// 检查命令是否已注册
if (!binding.name || !isCommandRegistered(binding.name)) {
continue;
}
// 获取命令处理函数
const handler = getCommandHandler(binding.key);
const handler = getCommandHandler(binding.name);
if (!handler) {
continue;
}
// 转换为CodeMirror快捷键格式
// binding.command 是快捷键组合 (如 "Mod-f")binding.key 是命令标识符
const keyBinding: KeyBinding = {
key: binding.command || '',
key: binding.key || '',
mac: binding.macos || undefined,
win: binding.windows || undefined,
linux: binding.linux || undefined,
run: handler,
preventDefault: true
preventDefault: binding.preventDefault,
scope: binding.scope || undefined
};
result.push(keyBinding);
}
return {keyBindings: result};
return result;
}
/**
@@ -56,7 +57,7 @@ export class Manager {
* @returns CodeMirror扩展
*/
static createKeymapExtension(keyBindings: KeyBindingConfig[]): Extension {
const {keyBindings: cmKeyBindings} = this.convertToKeyBindings(keyBindings);
const cmKeyBindings = this.convertToKeyBindings(keyBindings);
return this.compartment.of(keymap.of(cmKeyBindings));
}
@@ -66,7 +67,7 @@ export class Manager {
* @param keyBindings 后端快捷键配置列表
*/
static updateKeymap(view: any, keyBindings: KeyBindingConfig[]): void {
const {keyBindings: cmKeyBindings} = this.convertToKeyBindings(keyBindings);
const cmKeyBindings = this.convertToKeyBindings(keyBindings);
view.dispatch({
effects: this.compartment.reconfigure(keymap.of(cmKeyBindings))
});

View File

@@ -1,30 +0,0 @@
import {Command} from '@codemirror/view';
/**
* CodeMirror快捷键绑定格式
*/
export interface KeyBinding {
key: string
run: Command
preventDefault?: boolean
}
/**
* 命令处理函数类型
*/
export type CommandHandler = Command
/**
* 命令定义接口
*/
export interface CommandDefinition {
handler: CommandHandler
descriptionKey: string // 翻译键
}
/**
* 快捷键转换结果
*/
export interface KeymapResult {
keyBindings: KeyBinding[]
}

View File

@@ -15,7 +15,7 @@ import {highlightActiveLineGutter, highlightWhitespace, highlightTrailingWhitesp
import createEditorContextMenu from '../extensions/contextMenu';
import {blockLineNumbers} from '../extensions/codeblock';
import {createHttpClientExtension} from '../extensions/httpclient';
import {ExtensionKey} from '@/../bindings/voidraft/internal/models/models';
import {ExtensionName} from '@/../bindings/voidraft/internal/models/models';
type ExtensionEntry = {
definition: ExtensionDefinition
@@ -24,35 +24,35 @@ type ExtensionEntry = {
};
// 排除 $zero 的有效扩展 Key 类型
type ValidExtensionKey = Exclude<ExtensionKey, ExtensionKey.$zero>;
type ValidExtensionName = Exclude<ExtensionName, ExtensionName.$zero>;
const defineExtension = (create: (config: any) => any, defaultConfig: Record<string, any> = {}): ExtensionDefinition => ({
create,
defaultConfig
});
const EXTENSION_REGISTRY: Record<ValidExtensionKey, ExtensionEntry> = {
[ExtensionKey.ExtensionRainbowBrackets]: {
const EXTENSION_REGISTRY: Record<ValidExtensionName, ExtensionEntry> = {
[ExtensionName.RainbowBrackets]: {
definition: defineExtension(() => rainbowBrackets()),
displayNameKey: 'extensions.rainbowBrackets.name',
descriptionKey: 'extensions.rainbowBrackets.description'
},
[ExtensionKey.ExtensionHyperlink]: {
[ExtensionName.Hyperlink]: {
definition: defineExtension(() => hyperLink),
displayNameKey: 'extensions.hyperlink.name',
descriptionKey: 'extensions.hyperlink.description'
},
[ExtensionKey.ExtensionColorSelector]: {
[ExtensionName.ColorSelector]: {
definition: defineExtension(() => color),
displayNameKey: 'extensions.colorSelector.name',
descriptionKey: 'extensions.colorSelector.description'
},
[ExtensionKey.ExtensionTranslator]: {
[ExtensionName.Translator]: {
definition: defineExtension(() => createTranslatorExtension()),
displayNameKey: 'extensions.translator.name',
descriptionKey: 'extensions.translator.description'
},
[ExtensionKey.ExtensionMinimap]: {
[ExtensionName.Minimap]: {
definition: defineExtension((config: any) => minimap({
displayText: config?.displayText ?? 'characters',
showOverlay: config?.showOverlay ?? 'always',
@@ -65,49 +65,49 @@ const EXTENSION_REGISTRY: Record<ValidExtensionKey, ExtensionEntry> = {
displayNameKey: 'extensions.minimap.name',
descriptionKey: 'extensions.minimap.description'
},
[ExtensionKey.ExtensionSearch]: {
[ExtensionName.Search]: {
definition: defineExtension(() => vscodeSearch),
displayNameKey: 'extensions.search.name',
descriptionKey: 'extensions.search.description'
},
[ExtensionKey.ExtensionFold]: {
[ExtensionName.Fold]: {
definition: defineExtension(() => Prec.low(foldGutter())),
displayNameKey: 'extensions.fold.name',
descriptionKey: 'extensions.fold.description'
},
[ExtensionKey.ExtensionMarkdown]: {
[ExtensionName.Markdown]: {
definition: defineExtension(() => markdownExtensions),
displayNameKey: 'extensions.markdown.name',
descriptionKey: 'extensions.markdown.description'
},
[ExtensionKey.ExtensionLineNumbers]: {
[ExtensionName.LineNumbers]: {
definition: defineExtension(() => Prec.high([blockLineNumbers, highlightActiveLineGutter()])),
displayNameKey: 'extensions.lineNumbers.name',
descriptionKey: 'extensions.lineNumbers.description'
},
[ExtensionKey.ExtensionContextMenu]: {
[ExtensionName.ContextMenu]: {
definition: defineExtension(() => createEditorContextMenu()),
displayNameKey: 'extensions.contextMenu.name',
descriptionKey: 'extensions.contextMenu.description'
},
[ExtensionKey.ExtensionHighlightWhitespace]: {
[ExtensionName.HighlightWhitespace]: {
definition: defineExtension(() => highlightWhitespace()),
displayNameKey: 'extensions.highlightWhitespace.name',
descriptionKey: 'extensions.highlightWhitespace.description'
},
[ExtensionKey.ExtensionHighlightTrailingWhitespace]: {
[ExtensionName.HighlightTrailingWhitespace]: {
definition: defineExtension(() => highlightTrailingWhitespace()),
displayNameKey: 'extensions.highlightTrailingWhitespace.name',
descriptionKey: 'extensions.highlightTrailingWhitespace.description'
},
[ExtensionKey.ExtensionHttpClient]: {
[ExtensionName.HttpClient]: {
definition: defineExtension(() => createHttpClientExtension()),
displayNameKey: 'extensions.httpClient.name',
descriptionKey: 'extensions.httpClient.description'
}
};
const isRegisteredExtension = (key: string): key is ValidExtensionKey =>
const isRegisteredExtension = (key: string): key is ValidExtensionName =>
Object.prototype.hasOwnProperty.call(EXTENSION_REGISTRY, key);
const getRegistryEntry = (key: string): ExtensionEntry | undefined => {
@@ -118,7 +118,7 @@ const getRegistryEntry = (key: string): ExtensionEntry | undefined => {
};
export function registerAllExtensions(manager: Manager): void {
(Object.entries(EXTENSION_REGISTRY) as [ValidExtensionKey, ExtensionEntry][]).forEach(([id, entry]) => {
(Object.entries(EXTENSION_REGISTRY) as [ValidExtensionName, ExtensionEntry][]).forEach(([id, entry]) => {
manager.registerExtension(id, entry.definition);
});
}
@@ -147,7 +147,7 @@ export function hasExtensionConfig(key: string): boolean {
return Object.keys(getExtensionDefaultConfig(key)).length > 0;
}
export function getAllExtensionIds(): string[] {
export function getExtensionsMap(): string[] {
return Object.keys(EXTENSION_REGISTRY);
}

View File

@@ -11,8 +11,8 @@ export class Manager {
private extensionStates = new Map<string, ExtensionState>();
private views = new Map<number, EditorView>();
registerExtension(id: string, definition: ExtensionDefinition): void {
const existingState = this.extensionStates.get(id);
registerExtension(name: string, definition: ExtensionDefinition): void {
const existingState = this.extensionStates.get(name);
if (existingState) {
existingState.definition = definition;
if (existingState.config === undefined) {
@@ -21,8 +21,8 @@ export class Manager {
} else {
const compartment = new Compartment();
const defaultConfig = this.cloneConfig(definition.defaultConfig ?? {});
this.extensionStates.set(id, {
id,
this.extensionStates.set(name, {
name,
definition,
config: defaultConfig,
enabled: false,
@@ -34,8 +34,8 @@ export class Manager {
initExtensions(extensionConfigs: ExtensionConfig[]): void {
for (const config of extensionConfigs) {
if (!config.key) continue;
const state = this.extensionStates.get(config.key);
if (!config.name) continue;
const state = this.extensionStates.get(config.name);
if (!state) continue;
const resolvedConfig = this.cloneConfig(config.config ?? state.definition.defaultConfig ?? {});
this.commitExtensionState(state, config.enabled ?? false, resolvedConfig);
@@ -88,9 +88,9 @@ export class Manager {
state.enabled = enabled;
state.config = config;
state.extension = runtimeExtension;
this.applyExtensionToAllViews(state.id);
this.applyExtensionToAllViews(state.name);
} catch (error) {
console.error(`Failed to update extension ${state.id}:`, error);
console.error(`Failed to update extension ${state.name}:`, error);
}
}

View File

@@ -13,7 +13,7 @@ export interface ExtensionDefinition {
* 扩展运行时状态
*/
export interface ExtensionState {
id: string // 扩展 key
name: string
definition: ExtensionDefinition
config: any
enabled: boolean

View File

@@ -1,14 +1,13 @@
<script setup lang="ts">
import {computed, ref} from 'vue';
import {computed, onMounted, ref} from 'vue';
import {useI18n} from 'vue-i18n';
import {useEditorStore} from '@/stores/editorStore';
import {useExtensionStore} from '@/stores/extensionStore';
import {ExtensionService} from '@/../bindings/voidraft/internal/services';
import {
getAllExtensionIds,
getExtensionDefaultConfig,
getExtensionDescription,
getExtensionDisplayName,
getExtensionDisplayName, getExtensionsMap,
hasExtensionConfig
} from '@/views/editor/manager/extensions';
import SettingSection from '../components/SettingSection.vue';
@@ -19,48 +18,54 @@ const {t} = useI18n();
const editorStore = useEditorStore();
const extensionStore = useExtensionStore();
// 页面初始化时加载扩展数据
onMounted(async () => {
await extensionStore.loadExtensions();
});
// 展开状态管理
const expandedExtensions = ref<Set<string>>(new Set());
const expandedExtensions = ref<Set<number>>(new Set());
// 获取所有可用的扩展
const availableExtensions = computed(() => {
return getAllExtensionIds().map(key => {
const extension = extensionStore.extensions.find(ext => ext.key === key);
return getExtensionsMap().map(name => {
const extension = extensionStore.extensions.find(ext => ext.name === name);
return {
id: key,
displayName: getExtensionDisplayName(key),
description: getExtensionDescription(key),
id: extension?.id ?? 0,
name: name,
displayName: getExtensionDisplayName(name),
description: getExtensionDescription(name),
enabled: extension?.enabled || false,
hasConfig: hasExtensionConfig(key),
hasConfig: hasExtensionConfig(name),
config: extension?.config || {},
defaultConfig: getExtensionDefaultConfig(key)
defaultConfig: getExtensionDefaultConfig(name)
};
});
});
// 切换展开状态
const toggleExpanded = (extensionKey: string) => {
if (expandedExtensions.value.has(extensionKey)) {
expandedExtensions.value.delete(extensionKey);
const toggleExpanded = (extensionId: number) => {
if (expandedExtensions.value.has(extensionId)) {
expandedExtensions.value.delete(extensionId);
} else {
expandedExtensions.value.add(extensionKey);
expandedExtensions.value.add(extensionId);
}
};
// 更新扩展状态
const updateExtension = async (extensionKey: string, enabled: boolean) => {
const updateExtension = async (extensionId: number, enabled: boolean) => {
try {
await editorStore.updateExtension(extensionKey, enabled);
await editorStore.updateExtension(extensionId, enabled);
} catch (error) {
console.error('Failed to update extension:', error);
}
};
// 更新扩展配置
const updateExtensionConfig = async (extensionKey: string, configKey: string, value: any) => {
const updateExtensionConfig = async (extensionId: number, configKey: string, value: any) => {
try {
// 获取当前扩展状态
const extension = extensionStore.extensions.find(ext => ext.key === extensionKey);
const extension = extensionStore.extensions.find(ext => ext.id === extensionId);
if (!extension) return;
// 更新配置
@@ -71,7 +76,7 @@ const updateExtensionConfig = async (extensionKey: string, configKey: string, va
updatedConfig[configKey] = value;
}
// 使用editorStore的updateExtension方法更新确保应用到所有编辑器实例
await editorStore.updateExtension(extensionKey, extension.enabled ?? false, updatedConfig);
await editorStore.updateExtension(extensionId, extension.enabled ?? false, updatedConfig);
} catch (error) {
console.error('Failed to update extension config:', error);
@@ -79,19 +84,19 @@ const updateExtensionConfig = async (extensionKey: string, configKey: string, va
};
// 重置扩展到默认配置
const resetExtension = async (extensionKey: string) => {
const resetExtension = async (extensionId: number) => {
try {
// 重置到默认配置
await ExtensionService.ResetExtensionConfig(extensionKey);
await ExtensionService.ResetExtensionConfig(extensionId);
// 重新加载扩展状态以获取最新配置
await extensionStore.loadExtensions();
// 获取重置后的状态,立即应用到所有编辑器视图
const extension = extensionStore.extensions.find(ext => ext.key === extensionKey);
const extension = extensionStore.extensions.find(ext => ext.id === extensionId);
if (extension) {
// 通过editorStore更新确保所有视图都能同步
await editorStore.updateExtension(extensionKey, extension.enabled ?? false, extension.config);
await editorStore.updateExtension(extensionId, extension.enabled ?? false, extension.config);
}
} catch (error) {
console.error('Failed to reset extension:', error);
@@ -125,7 +130,7 @@ const formatConfigValue = (value: any): string => {
const handleConfigInput = async (
extensionKey: string,
extensionId: number,
configKey: string,
defaultValue: any,
event: Event
@@ -135,15 +140,15 @@ const handleConfigInput = async (
const rawValue = target.value;
const trimmedValue = rawValue.trim();
if (!trimmedValue.length) {
await updateExtensionConfig(extensionKey, configKey, undefined);
await updateExtensionConfig(extensionId, configKey, undefined);
return;
}
try {
const parsedValue = JSON.parse(trimmedValue);
await updateExtensionConfig(extensionKey, configKey, parsedValue);
await updateExtensionConfig(extensionId, configKey, parsedValue);
} catch (_error) {
const extension = extensionStore.extensions.find(ext => ext.key === extensionKey);
const extension = extensionStore.extensions.find(ext => ext.id === extensionId);
const fallbackValue = getConfigValue(extension?.config, configKey, defaultValue);
target.value = formatConfigValue(fallbackValue);
@@ -158,7 +163,7 @@ const handleConfigInput = async (
<SettingSection :title="t('settings.extensions')">
<div
v-for="extension in availableExtensions"
:key="extension.id"
:key="extension.name"
class="extension-item"
>
<!-- 扩展主项 -->

View File

@@ -1,200 +1,256 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { onMounted, computed } from 'vue';
import { onMounted, computed, ref, onUnmounted, watch } from 'vue';
import SettingSection from '../components/SettingSection.vue';
import SettingItem from '../components/SettingItem.vue';
import { useKeybindingStore } from '@/stores/keybindingStore';
import { useSystemStore } from '@/stores/systemStore';
import { useConfigStore } from '@/stores/configStore';
import { useEditorStore } from '@/stores/editorStore';
import { getCommandDescription } from '@/views/editor/keymap/commands';
import { KeyBindingKey } from '@/../bindings/voidraft/internal/models/models';
import { KeyBindingType } from '@/../bindings/voidraft/internal/models/models';
import { KeyBindingService } from '@/../bindings/voidraft/internal/services';
import { useConfirm } from '@/composables/useConfirm';
const { t } = useI18n();
const keybindingStore = useKeybindingStore();
const systemStore = useSystemStore();
const configStore = useConfigStore();
const editorStore = useEditorStore();
interface EditingState {
id: number;
name: string;
originalKey: string;
}
const editingBinding = ref<EditingState | null>(null);
const capturedKey = ref('');
const capturedKeyDisplay = ref<string[]>([]);
const isConflict = ref(false);
const isEditing = computed(() => !!editingBinding.value);
// 加载数据
onMounted(async () => {
await keybindingStore.loadKeyBindings();
});
// 从store中获取快捷键数据并转换为显示格式
const keyBindings = computed(() => {
return keybindingStore.keyBindings
.filter(kb => kb.enabled)
.map(kb => ({
key: kb.key,
command: parseKeyBinding(kb.command || '', kb.key),
extension: kb.extension || '',
description: kb.key ? (getCommandDescription(kb.key) || kb.key) : ''
}));
const keymapModeOptions = [
{ label: t('keybindings.modes.standard'), value: KeyBindingType.Standard },
{ label: t('keybindings.modes.emacs'), value: KeyBindingType.Emacs }
];
const updateKeymapMode = async (mode: KeyBindingType) => {
await configStore.setKeymapMode(mode);
await keybindingStore.loadKeyBindings();
await editorStore.applyKeymapSettings();
};
// 重置快捷键确认
const { isConfirming: isResetConfirming, requestConfirm: requestResetConfirm } = useConfirm({
timeout: 3000,
onConfirm: async () => {
await KeyBindingService.ResetKeyBindings();
await keybindingStore.loadKeyBindings();
await editorStore.applyKeymapSettings();
}
});
// 解析快捷键字符串为显示数组
const parseKeyBinding = (keyStr: string, keyBindingKey?: string): string[] => {
if (!keyStr) return [];
const keyBindings = computed(() =>
keybindingStore.keyBindings.map(kb => ({
id: kb.id,
name: kb.name,
command: getDisplayKeybinding(kb),
rawKey: getRawKey(kb),
extension: kb.extension || '',
description: getCommandDescription(kb.name) || kb.name || ''
}))
);
const getRawKey = (kb: any): string => {
const platformKey = systemStore.isMacOS ? kb.macos
: systemStore.isWindows ? kb.windows
: systemStore.isLinux ? kb.linux
: kb.key;
// 特殊处理重做快捷键的操作系统差异
if (keyBindingKey === KeyBindingKey.HistoryRedoKeyBindingKey && keyStr === 'Mod-Shift-z') {
if (systemStore.isMacOS) {
return ['⌘', '⇧', 'Z']; // macOS: Cmd+Shift+Z
} else {
return ['Ctrl', 'Y']; // Windows/Linux: Ctrl+Y
}
return platformKey || kb.key || '';
};
const getDisplayKeybinding = (kb: any): string[] => {
const keyStr = getRawKey(kb);
return keyStr ? parseKeyString(keyStr) : [];
};
const parseKeyString = (keyStr: string): string[] => {
const symbolMap: Record<string, string> = {
'Mod': systemStore.isMacOS ? '⌘' : 'Ctrl',
'Cmd': '⌘',
...(systemStore.isMacOS ? {
'Alt': '⌥',
'Shift': '⇧',
'Ctrl': '⌃'
} : {}),
'ArrowUp': '↑',
'ArrowDown': '↓',
'ArrowLeft': '←',
'ArrowRight': '→'
};
return keyStr
.split(/[-+]/)
.map(part => symbolMap[part] ?? part.charAt(0).toUpperCase() + part.slice(1))
.filter(Boolean);
};
// 键盘事件捕获
const SPECIAL_KEYS: Record<string, string> = {
' ': 'Space',
'ArrowUp': 'ArrowUp',
'ArrowDown': 'ArrowDown',
'ArrowLeft': 'ArrowLeft',
'ArrowRight': 'ArrowRight',
'Enter': 'Enter',
'Tab': 'Tab',
'Backspace': 'Backspace',
'Delete': 'Delete',
'Home': 'Home',
'End': 'End',
'PageUp': 'PageUp',
'PageDown': 'PageDown',
};
const MODIFIER_KEYS = ['Control', 'Alt', 'Shift', 'Meta'];
const MAX_KEY_PARTS = 3; // 最多3个键
const captureKeyBinding = (event: KeyboardEvent): string | null => {
// 忽略单独的修饰键
if (MODIFIER_KEYS.includes(event.key)) return null;
const parts: string[] = [];
// 添加修饰键
if (event.ctrlKey || event.metaKey) parts.push('Mod');
if (event.altKey) parts.push('Alt');
if (event.shiftKey) parts.push('Shift');
// 获取主键
const mainKey = SPECIAL_KEYS[event.key] ??
(event.key.length === 1 ? event.key.toLowerCase() : event.key);
if (mainKey) parts.push(mainKey);
// 限制最多3个键
if (parts.length > MAX_KEY_PARTS) return null;
return parts.join('-');
};
const cancelEdit = () => {
window.removeEventListener('keydown', handleKeyCapture, true);
editingBinding.value = null;
capturedKey.value = '';
capturedKeyDisplay.value = [];
isConflict.value = false;
};
const handleKeyCapture = (event: KeyboardEvent) => {
if (!isEditing.value) return;
event.preventDefault();
event.stopPropagation();
// ESC 取消编辑
if (event.key === 'Escape') {
cancelEdit();
return;
}
// 特殊处理重做选择快捷键的操作系统差异
if (keyBindingKey === KeyBindingKey.HistoryRedoSelectionKeyBindingKey && keyStr === 'Mod-Shift-u') {
if (systemStore.isMacOS) {
return ['⌘', '⇧', 'U']; // macOS: Cmd+Shift+U
} else {
return ['Alt', 'U']; // Windows/Linux: Alt+U
}
const key = captureKeyBinding(event);
if (key) {
capturedKey.value = key;
capturedKeyDisplay.value = parseKeyString(key);
isConflict.value = false;
}
};
const startEditBinding = (binding: any) => {
editingBinding.value = {
id: binding.id,
name: binding.name,
originalKey: binding.rawKey
};
capturedKey.value = '';
capturedKeyDisplay.value = [];
isConflict.value = false;
// 手动添加键盘监听
window.addEventListener('keydown', handleKeyCapture, true);
};
const checkConflict = (newKey: string): boolean =>
keyBindings.value.some(kb =>
kb.rawKey === newKey && kb.name !== editingBinding.value?.name
);
const confirmKeybinding = async () => {
if (!editingBinding.value || !capturedKey.value) return;
// 检查冲突
if (checkConflict(capturedKey.value)) {
isConflict.value = true;
setTimeout(cancelEdit, 600);
return;
}
// 特殊处理代码折叠快捷键的操作系统差异
if (keyBindingKey === KeyBindingKey.FoldCodeKeyBindingKey && keyStr === 'Ctrl-Shift-[') {
if (systemStore.isMacOS) {
return ['⌘', '⌥', '[']; // macOS: Cmd+Alt+[
} else {
return ['Ctrl', 'Shift', '[']; // Windows/Linux: Ctrl+Shift+[
}
try {
await keybindingStore.updateKeyBinding(
editingBinding.value.id,
capturedKey.value
);
await editorStore.applyKeymapSettings();
} catch (error) {
console.error(error);
} finally {
cancelEdit();
}
if (keyBindingKey === KeyBindingKey.UnfoldCodeKeyBindingKey && keyStr === 'Ctrl-Shift-]') {
if (systemStore.isMacOS) {
return ['⌘', '⌥', ']']; // macOS: Cmd+Alt+]
} else {
return ['Ctrl', 'Shift', ']']; // Windows/Linux: Ctrl+Shift+]
}
}
// 特殊处理编辑快捷键的操作系统差异
if (keyBindingKey === KeyBindingKey.CursorSyntaxLeftKeyBindingKey && keyStr === 'Alt-ArrowLeft') {
if (systemStore.isMacOS) {
return ['Ctrl', '←']; // macOS: Ctrl+ArrowLeft
} else {
return ['Alt', '←']; // Windows/Linux: Alt+ArrowLeft
}
}
if (keyBindingKey === KeyBindingKey.CursorSyntaxRightKeyBindingKey && keyStr === 'Alt-ArrowRight') {
if (systemStore.isMacOS) {
return ['Ctrl', '→']; // macOS: Ctrl+ArrowRight
} else {
return ['Alt', '→']; // Windows/Linux: Alt+ArrowRight
}
}
if (keyBindingKey === KeyBindingKey.InsertBlankLineKeyBindingKey && keyStr === 'Ctrl-Enter') {
if (systemStore.isMacOS) {
return ['⌘', 'Enter']; // macOS: Cmd+Enter
} else {
return ['Ctrl', 'Enter']; // Windows/Linux: Ctrl+Enter
}
}
if (keyBindingKey === KeyBindingKey.SelectLineKeyBindingKey && keyStr === 'Alt-l') {
if (systemStore.isMacOS) {
return ['Ctrl', 'L']; // macOS: Ctrl+l
} else {
return ['Alt', 'L']; // Windows/Linux: Alt+l
}
}
if (keyBindingKey === KeyBindingKey.SelectParentSyntaxKeyBindingKey && keyStr === 'Ctrl-i') {
if (systemStore.isMacOS) {
return ['⌘', 'I']; // macOS: Cmd+i
} else {
return ['Ctrl', 'I']; // Windows/Linux: Ctrl+i
}
}
if (keyBindingKey === KeyBindingKey.IndentLessKeyBindingKey && keyStr === 'Ctrl-[') {
if (systemStore.isMacOS) {
return ['⌘', '[']; // macOS: Cmd+[
} else {
return ['Ctrl', '[']; // Windows/Linux: Ctrl+[
}
}
if (keyBindingKey === KeyBindingKey.IndentMoreKeyBindingKey && keyStr === 'Ctrl-]') {
if (systemStore.isMacOS) {
return ['⌘', ']']; // macOS: Cmd+]
} else {
return ['Ctrl', ']']; // Windows/Linux: Ctrl+]
}
}
if (keyBindingKey === KeyBindingKey.IndentSelectionKeyBindingKey && keyStr === 'Ctrl-Alt-\\') {
if (systemStore.isMacOS) {
return ['⌘', '⌥', '\\']; // macOS: Cmd+Alt+\
} else {
return ['Ctrl', 'Alt', '\\']; // Windows/Linux: Ctrl+Alt+\
}
}
if (keyBindingKey === KeyBindingKey.CursorMatchingBracketKeyBindingKey && keyStr === 'Shift-Ctrl-\\') {
if (systemStore.isMacOS) {
return ['⇧', '⌘', '\\']; // macOS: Shift+Cmd+\
} else {
return ['Shift', 'Ctrl', '\\']; // Windows/Linux: Shift+Ctrl+\
}
}
if (keyBindingKey === KeyBindingKey.ToggleCommentKeyBindingKey && keyStr === 'Ctrl-/') {
if (systemStore.isMacOS) {
return ['⌘', '/']; // macOS: Cmd+/
} else {
return ['Ctrl', '/']; // Windows/Linux: Ctrl+/
}
}
// 特殊处理删除快捷键的操作系统差异
if (keyBindingKey === KeyBindingKey.DeleteGroupBackwardKeyBindingKey && keyStr === 'Ctrl-Backspace') {
if (systemStore.isMacOS) {
return ['⌘', 'Backspace']; // macOS: Cmd+Backspace
} else {
return ['Ctrl', 'Backspace']; // Windows/Linux: Ctrl+Backspace
}
}
if (keyBindingKey === KeyBindingKey.DeleteGroupForwardKeyBindingKey && keyStr === 'Ctrl-Delete') {
if (systemStore.isMacOS) {
return ['⌘', 'Delete']; // macOS: Cmd+Delete
} else {
return ['Ctrl', 'Delete']; // Windows/Linux: Ctrl+Delete
}
}
// 处理常见的快捷键格式
const parts = keyStr.split(/[-+]/);
return parts.map(part => {
// 根据操作系统将 Mod 替换为相应的键
if (part === 'Mod') {
if (systemStore.isMacOS) {
return '⌘'; // macOS 使用 Command 键符号
} else {
return 'Ctrl'; // Windows/Linux 使用 Ctrl
}
}
// 处理其他键名的操作系统差异
if (part === 'Alt' && systemStore.isMacOS) {
return '⌥'; // macOS 使用 Option 键符号
}
if (part === 'Shift') {
return systemStore.isMacOS ? '⇧' : 'Shift'; // macOS 使用符号
}
// 首字母大写
return part.charAt(0).toUpperCase() + part.slice(1).toLowerCase();
}).filter(part => part.length > 0);
};
</script>
<template>
<div class="settings-page">
<!-- 快捷键模式设置 -->
<SettingSection :title="t('keybindings.keymapMode')">
<SettingItem
:title="t('keybindings.keymapMode')">
<select
:value="configStore.config.editing.keymapMode"
@change="updateKeymapMode(($event.target as HTMLSelectElement).value as KeyBindingType)"
class="select-input"
>
<option
v-for="option in keymapModeOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</option>
</select>
</SettingItem>
</SettingSection>
<!-- 快捷键列表 -->
<SettingSection :title="t('settings.keyBindings')">
<template #title-right>
<button
:class="['reset-button', isResetConfirming('keybindings') ? 'reset-button-confirming' : '']"
@click="requestResetConfirm('keybindings')"
>
{{ isResetConfirming('keybindings') ? t('keybindings.confirmReset') : t('keybindings.resetToDefault') }}
</button>
</template>
<div class="key-bindings-container">
<div class="key-bindings-header">
<div class="keybinding-col">{{ t('keybindings.headers.shortcut') }}</div>
@@ -204,18 +260,55 @@ const parseKeyBinding = (keyStr: string, keyBindingKey?: string): string[] => {
<div
v-for="binding in keyBindings"
:key="binding.key"
:key="binding.name"
class="key-binding-row"
>
<div class="keybinding-col">
<span
v-for="(key, index) in binding.command"
:key="index"
class="key-badge"
>
{{ key }}
</span>
<!-- 快捷键列 -->
<div
class="keybinding-col"
:class="{ 'editing': editingBinding?.name === binding.name }"
@click.stop="editingBinding?.name !== binding.name && startEditBinding(binding)"
>
<!-- 编辑模式 -->
<template v-if="editingBinding?.name === binding.name">
<template v-if="!capturedKey">
<span class="key-badge waiting">waiting...</span>
</template>
<template v-else>
<span
v-for="(key, index) in capturedKeyDisplay"
:key="index"
class="key-badge captured"
:class="{ 'conflict': isConflict }"
>
{{ key }}
</span>
</template>
<button
@click.stop="confirmKeybinding"
class="btn-mini btn-confirm"
:disabled="!capturedKey"
title="Ok"
></button>
<button
@click.stop="cancelEdit"
class="btn-mini btn-cancel"
title="Cancel"
></button>
</template>
<!-- 显示模式 -->
<template v-else>
<span
v-for="(key, index) in binding.command"
:key="index"
class="key-badge"
>
{{ key }}
</span>
</template>
</div>
<div class="extension-col">{{ binding.extension }}</div>
<div class="description-col">{{ binding.description }}</div>
</div>
@@ -225,16 +318,68 @@ const parseKeyBinding = (keyStr: string, keyBindingKey?: string): string[] => {
</template>
<style scoped lang="scss">
.settings-page {
//max-width: 800px;
.select-input {
min-width: 140px;
padding: 6px 10px;
border: 1px solid var(--settings-input-border);
border-radius: 4px;
background-color: var(--settings-input-bg);
color: var(--settings-text);
font-size: 12px;
appearance: none;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23666666' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right 6px center;
background-size: 14px;
padding-right: 26px;
transition: border-color 0.2s ease;
&:focus {
outline: none;
border-color: #4a9eff;
}
option {
background-color: var(--settings-card-bg);
color: var(--settings-text);
}
}
.reset-button {
padding: 6px 12px;
font-size: 12px;
border: 1px solid var(--settings-input-border);
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
background-color: var(--settings-button-bg);
color: var(--settings-button-text);
&:hover {
border-color: #4a9eff;
background-color: var(--settings-button-hover-bg);
}
&:active {
transform: translateY(1px);
}
&.reset-button-confirming {
background-color: #e74c3c;
color: white;
border-color: #c0392b;
&:hover {
background-color: #c0392b;
}
}
}
.key-bindings-container {
padding: 10px 16px;
.key-bindings-header {
display: flex;
padding: 0 0 10px 0;
padding: 0 0 8px 0;
border-bottom: 1px solid var(--settings-border);
color: var(--text-muted);
font-size: 12px;
@@ -243,7 +388,7 @@ const parseKeyBinding = (keyStr: string, keyBindingKey?: string): string[] => {
.key-binding-row {
display: flex;
padding: 14px 0;
padding: 10px 0;
border-bottom: 1px solid var(--settings-border);
align-items: center;
transition: background-color 0.2s ease;
@@ -256,9 +401,20 @@ const parseKeyBinding = (keyStr: string, keyBindingKey?: string): string[] => {
.keybinding-col {
width: 150px;
display: flex;
gap: 5px;
gap: 4px;
padding: 0 10px 0 0;
color: var(--settings-text);
align-items: center;
cursor: pointer;
transition: all 0.2s ease;
&:hover:not(.editing) .key-badge {
border-color: #4a9eff;
}
&.editing {
cursor: default;
}
.key-badge {
background-color: var(--settings-input-bg);
@@ -267,6 +423,71 @@ const parseKeyBinding = (keyStr: string, keyBindingKey?: string): string[] => {
font-size: 11px;
border: 1px solid var(--settings-input-border);
color: var(--settings-text);
transition: border-color 0.2s ease;
white-space: nowrap;
&.waiting {
border: none;
background-color: transparent;
padding: 0;
color: #4a9eff;
font-style: italic;
animation: colorPulse 1.5s ease-in-out infinite;
}
&.captured {
background-color: #4a9eff;
color: white;
border-color: #4a9eff;
&.conflict {
background-color: #dc3545;
border-color: #dc3545;
animation: shake 0.6s ease-in-out;
}
}
}
}
.btn-mini {
width: 16px;
height: 16px;
min-width: 16px;
border: none;
border-radius: 2px;
cursor: pointer;
font-size: 10px;
transition: opacity 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
line-height: 1;
margin-left: auto;
&.btn-confirm {
background-color: #28a745;
color: white;
&:hover:not(:disabled) {
opacity: 0.85;
}
&:disabled {
background-color: var(--settings-input-border);
cursor: not-allowed;
opacity: 0.5;
}
}
&.btn-cancel {
background-color: #dc3545;
color: white;
margin-left: 2px;
&:hover {
opacity: 0.85;
}
}
}
@@ -285,6 +506,29 @@ const parseKeyBinding = (keyStr: string, keyBindingKey?: string): string[] => {
}
}
@keyframes colorPulse {
0%, 100% {
color: #4a9eff;
opacity: 1;
}
50% {
color: #2080ff;
opacity: 0.6;
}
}
@keyframes shake {
0%, 100% {
transform: translateX(0);
}
10%, 30%, 50%, 70%, 90% {
transform: translateX(-4px);
}
20%, 40%, 60%, 80% {
transform: translateX(4px);
}
}
.coming-soon-placeholder {
padding: 20px;
background-color: var(--settings-card-bg);