♻️ Refactor keybinding service
This commit is contained in:
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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),
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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'
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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';
|
||||
@@ -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))
|
||||
});
|
||||
|
||||
@@ -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[]
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ export interface ExtensionDefinition {
|
||||
* 扩展运行时状态
|
||||
*/
|
||||
export interface ExtensionState {
|
||||
id: string // 扩展 key
|
||||
name: string
|
||||
definition: ExtensionDefinition
|
||||
config: any
|
||||
enabled: boolean
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
<!-- 扩展主项 -->
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user