♻️ 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

@@ -21,7 +21,7 @@ export class Document {
/**
* UUID for cross-device sync (UUIDv7)
*/
"uuid": string | null;
"uuid": string;
/**
* creation time
@@ -56,7 +56,7 @@ export class Document {
/** Creates a new Document instance. */
constructor($$source: Partial<Document> = {}) {
if (!("uuid" in $$source)) {
this["uuid"] = null;
this["uuid"] = "";
}
if (!("created_at" in $$source)) {
this["created_at"] = "";
@@ -98,7 +98,7 @@ export class Extension {
/**
* UUID for cross-device sync (UUIDv7)
*/
"uuid": string | null;
"uuid": string;
/**
* creation time
@@ -116,9 +116,9 @@ export class Extension {
"deleted_at"?: string | null;
/**
* extension key
* extension name
*/
"key": string;
"name": string;
/**
* extension enabled or not
@@ -133,7 +133,7 @@ export class Extension {
/** Creates a new Extension instance. */
constructor($$source: Partial<Extension> = {}) {
if (!("uuid" in $$source)) {
this["uuid"] = null;
this["uuid"] = "";
}
if (!("created_at" in $$source)) {
this["created_at"] = "";
@@ -141,8 +141,8 @@ export class Extension {
if (!("updated_at" in $$source)) {
this["updated_at"] = "";
}
if (!("key" in $$source)) {
this["key"] = "";
if (!("name" in $$source)) {
this["name"] = "";
}
if (!("enabled" in $$source)) {
this["enabled"] = false;
@@ -179,7 +179,7 @@ export class KeyBinding {
/**
* UUID for cross-device sync (UUIDv7)
*/
"uuid": string | null;
"uuid": string;
/**
* creation time
@@ -197,29 +197,59 @@ export class KeyBinding {
"deleted_at"?: string | null;
/**
* key binding key
* command identifier
*/
"key": string;
"name": string;
/**
* key binding command
* keybinding type: standard or emacs
*/
"command": string;
"type": string;
/**
* key binding extension
* universal keybinding (cross-platform)
*/
"extension"?: string;
"key"?: string;
/**
* key binding enabled
* macOS specific keybinding
*/
"macos"?: string;
/**
* Windows specific keybinding
*/
"windows"?: string;
/**
* Linux specific keybinding
*/
"linux"?: string;
/**
* extension name (functional category)
*/
"extension": string;
/**
* whether this keybinding is enabled
*/
"enabled": boolean;
/**
* prevent browser default behavior
*/
"preventDefault": boolean;
/**
* keybinding scope (default: editor)
*/
"scope"?: string;
/** Creates a new KeyBinding instance. */
constructor($$source: Partial<KeyBinding> = {}) {
if (!("uuid" in $$source)) {
this["uuid"] = null;
this["uuid"] = "";
}
if (!("created_at" in $$source)) {
this["created_at"] = "";
@@ -227,15 +257,21 @@ export class KeyBinding {
if (!("updated_at" in $$source)) {
this["updated_at"] = "";
}
if (!("key" in $$source)) {
this["key"] = "";
if (!("name" in $$source)) {
this["name"] = "";
}
if (!("command" in $$source)) {
this["command"] = "";
if (!("type" in $$source)) {
this["type"] = "";
}
if (!("extension" in $$source)) {
this["extension"] = "";
}
if (!("enabled" in $$source)) {
this["enabled"] = false;
}
if (!("preventDefault" in $$source)) {
this["preventDefault"] = false;
}
Object.assign(this, $$source);
}
@@ -261,7 +297,7 @@ export class Theme {
/**
* UUID for cross-device sync (UUIDv7)
*/
"uuid": string | null;
"uuid": string;
/**
* creation time
@@ -279,9 +315,9 @@ export class Theme {
"deleted_at"?: string | null;
/**
* theme key
* theme name
*/
"key": string;
"name": string;
/**
* theme type
@@ -296,7 +332,7 @@ export class Theme {
/** Creates a new Theme instance. */
constructor($$source: Partial<Theme> = {}) {
if (!("uuid" in $$source)) {
this["uuid"] = null;
this["uuid"] = "";
}
if (!("created_at" in $$source)) {
this["created_at"] = "";
@@ -304,8 +340,8 @@ export class Theme {
if (!("updated_at" in $$source)) {
this["updated_at"] = "";
}
if (!("key" in $$source)) {
this["key"] = "";
if (!("name" in $$source)) {
this["name"] = "";
}
if (!("type" in $$source)) {
this["type"] = ("" as theme$0.Type);

View File

@@ -234,6 +234,12 @@ export class EditingConfig {
*/
"tabType": TabType;
/**
* 快捷键模式
* 快捷键模式standard 或 emacs
*/
"keymapMode": KeyBindingType;
/**
* 保存选项
* 自动保存延迟(毫秒)
@@ -263,6 +269,9 @@ export class EditingConfig {
if (!("tabType" in $$source)) {
this["tabType"] = ("" as TabType);
}
if (!("keymapMode" in $$source)) {
this["keymapMode"] = ("" as KeyBindingType);
}
if (!("autoSaveDelay" in $$source)) {
this["autoSaveDelay"] = 0;
}
@@ -283,14 +292,14 @@ export class EditingConfig {
* Extension 扩展配置
*/
export class Extension {
"key": ExtensionKey;
"key": ExtensionName;
"enabled": boolean;
"config": ExtensionConfig;
/** Creates a new Extension instance. */
constructor($$source: Partial<Extension> = {}) {
if (!("key" in $$source)) {
this["key"] = ("" as ExtensionKey);
this["key"] = ("" as ExtensionName);
}
if (!("enabled" in $$source)) {
this["enabled"] = false;
@@ -321,79 +330,78 @@ export class Extension {
export type ExtensionConfig = { [_: string]: any };
/**
* ExtensionKey 扩展标识符
* ExtensionName 扩展标识符
*/
export enum ExtensionKey {
export enum ExtensionName {
/**
* The Go zero value for the underlying type of the enum.
*/
$zero = "",
/**
* 编辑增强扩展
* 彩虹括号
*/
ExtensionRainbowBrackets = "rainbowBrackets",
RainbowBrackets = "rainbowBrackets",
/**
* 超链接
*/
ExtensionHyperlink = "hyperlink",
Hyperlink = "hyperlink",
/**
* 颜色选择器
*/
ExtensionColorSelector = "colorSelector",
ColorSelector = "colorSelector",
/**
* 代码折叠
*/
ExtensionFold = "fold",
Fold = "fold",
/**
* 划词翻译
*/
ExtensionTranslator = "translator",
Translator = "translator",
/**
* Markdown渲染
*/
ExtensionMarkdown = "markdown",
Markdown = "markdown",
/**
* 显示空白字符
*/
ExtensionHighlightWhitespace = "highlightWhitespace",
HighlightWhitespace = "highlightWhitespace",
/**
* 高亮行尾空白
*/
ExtensionHighlightTrailingWhitespace = "highlightTrailingWhitespace",
HighlightTrailingWhitespace = "highlightTrailingWhitespace",
/**
* 小地图
*/
ExtensionMinimap = "minimap",
Minimap = "minimap",
/**
* 行号显示
*/
ExtensionLineNumbers = "lineNumbers",
LineNumbers = "lineNumbers",
/**
* 上下文菜单
*/
ExtensionContextMenu = "contextMenu",
ContextMenu = "contextMenu",
/**
* 搜索功能
*/
ExtensionSearch = "search",
Search = "search",
/**
* HTTP 客户端
*/
ExtensionHttpClient = "httpClient",
HttpClient = "httpClient",
};
/**
@@ -684,25 +692,73 @@ export class HotkeyCombo {
* KeyBinding 单个快捷键绑定
*/
export class KeyBinding {
"key": KeyBindingKey;
"command": string;
"extension": ExtensionKey;
/**
* 命令唯一标识符
*/
"name": KeyBindingName;
/**
* 快捷键类型standard 或 "emacs"
*/
"type": KeyBindingType;
/**
* 通用快捷键(跨平台)
*/
"key"?: string;
/**
* macOS 专用快捷键
*/
"macos"?: string;
/**
* windows 专用快捷键
*/
"win"?: string;
/**
* Linux 专用快捷键
*/
"linux"?: string;
/**
* 所属扩展
*/
"extension": ExtensionName;
/**
* 是否启用
*/
"enabled": boolean;
/**
* 阻止浏览器默认行为
*/
"preventDefault": boolean;
/**
* 作用域(默认 "editor"
*/
"scope"?: string;
/** Creates a new KeyBinding instance. */
constructor($$source: Partial<KeyBinding> = {}) {
if (!("key" in $$source)) {
this["key"] = ("" as KeyBindingKey);
if (!("name" in $$source)) {
this["name"] = ("" as KeyBindingName);
}
if (!("command" in $$source)) {
this["command"] = "";
if (!("type" in $$source)) {
this["type"] = ("" as KeyBindingType);
}
if (!("extension" in $$source)) {
this["extension"] = ("" as ExtensionKey);
this["extension"] = ("" as ExtensionName);
}
if (!("enabled" in $$source)) {
this["enabled"] = false;
}
if (!("preventDefault" in $$source)) {
this["preventDefault"] = false;
}
Object.assign(this, $$source);
}
@@ -717,9 +773,9 @@ export class KeyBinding {
}
/**
* KeyBindingKey 快捷键命令
* KeyBindingName 快捷键命令标识符
*/
export enum KeyBindingKey {
export enum KeyBindingName {
/**
* The Go zero value for the underlying type of the enum.
*/
@@ -728,247 +784,409 @@ export enum KeyBindingKey {
/**
* 显示搜索
*/
ShowSearchKeyBindingKey = "showSearch",
ShowSearch = "showSearch",
/**
* 隐藏搜索
*/
HideSearchKeyBindingKey = "hideSearch",
HideSearch = "hideSearch",
/**
* 块内选择全部
*/
BlockSelectAllKeyBindingKey = "blockSelectAll",
BlockSelectAll = "blockSelectAll",
/**
* 在当前块后添加新块
*/
BlockAddAfterCurrentKeyBindingKey = "blockAddAfterCurrent",
BlockAddAfterCurrent = "blockAddAfterCurrent",
/**
* 在最后添加新块
*/
BlockAddAfterLastKeyBindingKey = "blockAddAfterLast",
BlockAddAfterLast = "blockAddAfterLast",
/**
* 在当前块前添加新块
*/
BlockAddBeforeCurrentKeyBindingKey = "blockAddBeforeCurrent",
BlockAddBeforeCurrent = "blockAddBeforeCurrent",
/**
* 跳转到上一个块
*/
BlockGotoPreviousKeyBindingKey = "blockGotoPrevious",
BlockGotoPrevious = "blockGotoPrevious",
/**
* 跳转到下一个块
*/
BlockGotoNextKeyBindingKey = "blockGotoNext",
BlockGotoNext = "blockGotoNext",
/**
* 选择上一个块
*/
BlockSelectPreviousKeyBindingKey = "blockSelectPrevious",
BlockSelectPrevious = "blockSelectPrevious",
/**
* 选择下一个块
*/
BlockSelectNextKeyBindingKey = "blockSelectNext",
BlockSelectNext = "blockSelectNext",
/**
* 删除当前块
*/
BlockDeleteKeyBindingKey = "blockDelete",
BlockDelete = "blockDelete",
/**
* 向上移动当前块
*/
BlockMoveUpKeyBindingKey = "blockMoveUp",
BlockMoveUp = "blockMoveUp",
/**
* 向下移动当前块
*/
BlockMoveDownKeyBindingKey = "blockMoveDown",
BlockMoveDown = "blockMoveDown",
/**
* 删除行
*/
BlockDeleteLineKeyBindingKey = "blockDeleteLine",
BlockDeleteLine = "blockDeleteLine",
/**
* 向上移动行
*/
BlockMoveLineUpKeyBindingKey = "blockMoveLineUp",
BlockMoveLineUp = "blockMoveLineUp",
/**
* 向下移动行
*/
BlockMoveLineDownKeyBindingKey = "blockMoveLineDown",
BlockMoveLineDown = "blockMoveLineDown",
/**
* 字符转置
*/
BlockTransposeCharsKeyBindingKey = "blockTransposeChars",
BlockTransposeChars = "blockTransposeChars",
/**
* 格式化代码块
*/
BlockFormatKeyBindingKey = "blockFormat",
BlockFormat = "blockFormat",
/**
* 复制
*/
BlockCopyKeyBindingKey = "blockCopy",
BlockCopy = "blockCopy",
/**
* 剪切
*/
BlockCutKeyBindingKey = "blockCut",
BlockCut = "blockCut",
/**
* 粘贴
*/
BlockPasteKeyBindingKey = "blockPaste",
BlockPaste = "blockPaste",
/**
* 折叠代码
*/
FoldCodeKeyBindingKey = "foldCode",
FoldCode = "foldCode",
/**
* 展开代码
*/
UnfoldCodeKeyBindingKey = "unfoldCode",
UnfoldCode = "unfoldCode",
/**
* 折叠全部
*/
FoldAllKeyBindingKey = "foldAll",
FoldAll = "foldAll",
/**
* 展开全部
*/
UnfoldAllKeyBindingKey = "unfoldAll",
UnfoldAll = "unfoldAll",
/**
* 光标按语法左移
*/
CursorSyntaxLeftKeyBindingKey = "cursorSyntaxLeft",
CursorSyntaxLeft = "cursorSyntaxLeft",
/**
* 光标按语法右移
*/
CursorSyntaxRightKeyBindingKey = "cursorSyntaxRight",
CursorSyntaxRight = "cursorSyntaxRight",
/**
* 按语法选择左侧
*/
SelectSyntaxLeftKeyBindingKey = "selectSyntaxLeft",
SelectSyntaxLeft = "selectSyntaxLeft",
/**
* 按语法选择右侧
*/
SelectSyntaxRightKeyBindingKey = "selectSyntaxRight",
SelectSyntaxRight = "selectSyntaxRight",
/**
* 向上复制行
*/
CopyLineUpKeyBindingKey = "copyLineUp",
CopyLineUp = "copyLineUp",
/**
* 向下复制行
*/
CopyLineDownKeyBindingKey = "copyLineDown",
CopyLineDown = "copyLineDown",
/**
* 插入空行
*/
InsertBlankLineKeyBindingKey = "insertBlankLine",
InsertBlankLine = "insertBlankLine",
/**
* 选择行
*/
SelectLineKeyBindingKey = "selectLine",
SelectLine = "selectLine",
/**
* 选择父级语法
*/
SelectParentSyntaxKeyBindingKey = "selectParentSyntax",
SelectParentSyntax = "selectParentSyntax",
/**
* 简化选择
*/
SimplifySelection = "simplifySelection",
/**
* 在上方添加光标
*/
AddCursorAbove = "addCursorAbove",
/**
* 在下方添加光标
*/
AddCursorBelow = "addCursorBelow",
/**
* 光标按单词左移
*/
CursorGroupLeft = "cursorGroupLeft",
/**
* 光标按单词右移
*/
CursorGroupRight = "cursorGroupRight",
/**
* 按单词选择左侧
*/
SelectGroupLeft = "selectGroupLeft",
/**
* 按单词选择右侧
*/
SelectGroupRight = "selectGroupRight",
/**
* 删除到行尾
*/
DeleteToLineEnd = "deleteToLineEnd",
/**
* 删除到行首
*/
DeleteToLineStart = "deleteToLineStart",
/**
* 移动到行首
*/
CursorLineStart = "cursorLineStart",
/**
* 移动到行尾
*/
CursorLineEnd = "cursorLineEnd",
/**
* 选择到行首
*/
SelectLineStart = "selectLineStart",
/**
* 选择到行尾
*/
SelectLineEnd = "selectLineEnd",
/**
* 跳转到文档开头
*/
CursorDocStart = "cursorDocStart",
/**
* 跳转到文档结尾
*/
CursorDocEnd = "cursorDocEnd",
/**
* 选择到文档开头
*/
SelectDocStart = "selectDocStart",
/**
* 选择到文档结尾
*/
SelectDocEnd = "selectDocEnd",
/**
* 选择到匹配括号
*/
SelectMatchingBracket = "selectMatchingBracket",
/**
* 分割行
*/
SplitLine = "splitLine",
/**
* 光标左移一个字符
*/
CursorCharLeft = "cursorCharLeft",
/**
* 光标右移一个字符
*/
CursorCharRight = "cursorCharRight",
/**
* 光标上移一行
*/
CursorLineUp = "cursorLineUp",
/**
* 光标下移一行
*/
CursorLineDown = "cursorLineDown",
/**
* 向上翻页
*/
CursorPageUp = "cursorPageUp",
/**
* 向下翻页
*/
CursorPageDown = "cursorPageDown",
/**
* 选择左移一个字符
*/
SelectCharLeft = "selectCharLeft",
/**
* 选择右移一个字符
*/
SelectCharRight = "selectCharRight",
/**
* 选择上移一行
*/
SelectLineUp = "selectLineUp",
/**
* 选择下移一行
*/
SelectLineDown = "selectLineDown",
/**
* 减少缩进
*/
IndentLessKeyBindingKey = "indentLess",
IndentLess = "indentLess",
/**
* 增加缩进
*/
IndentMoreKeyBindingKey = "indentMore",
IndentMore = "indentMore",
/**
* 缩进选择
*/
IndentSelectionKeyBindingKey = "indentSelection",
IndentSelection = "indentSelection",
/**
* 光标到匹配括号
*/
CursorMatchingBracketKeyBindingKey = "cursorMatchingBracket",
CursorMatchingBracket = "cursorMatchingBracket",
/**
* 切换注释
*/
ToggleCommentKeyBindingKey = "toggleComment",
ToggleComment = "toggleComment",
/**
* 切换块注释
*/
ToggleBlockCommentKeyBindingKey = "toggleBlockComment",
ToggleBlockComment = "toggleBlockComment",
/**
* 插入新行并缩进
*/
InsertNewlineAndIndentKeyBindingKey = "insertNewlineAndIndent",
InsertNewlineAndIndent = "insertNewlineAndIndent",
/**
* 向后删除字符
*/
DeleteCharBackwardKeyBindingKey = "deleteCharBackward",
DeleteCharBackward = "deleteCharBackward",
/**
* 向前删除字符
*/
DeleteCharForwardKeyBindingKey = "deleteCharForward",
DeleteCharForward = "deleteCharForward",
/**
* 向后删除组
*/
DeleteGroupBackwardKeyBindingKey = "deleteGroupBackward",
DeleteGroupBackward = "deleteGroupBackward",
/**
* 向前删除组
*/
DeleteGroupForwardKeyBindingKey = "deleteGroupForward",
DeleteGroupForward = "deleteGroupForward",
/**
* 撤销
*/
HistoryUndoKeyBindingKey = "historyUndo",
HistoryUndo = "historyUndo",
/**
* 重做
*/
HistoryRedoKeyBindingKey = "historyRedo",
HistoryRedo = "historyRedo",
/**
* 撤销选择
*/
HistoryUndoSelectionKeyBindingKey = "historyUndoSelection",
HistoryUndoSelection = "historyUndoSelection",
/**
* 重做选择
*/
HistoryRedoSelectionKeyBindingKey = "historyRedoSelection",
HistoryRedoSelection = "historyRedoSelection",
};
export enum KeyBindingType {
/**
* The Go zero value for the underlying type of the enum.
*/
$zero = "",
/**
* standard 标准快捷键
*/
Standard = "standard",
/**
* emacs 快捷键
*/
Emacs = "emacs",
};
/**

View File

@@ -20,35 +20,11 @@ import * as models$0 from "../models/models.js";
// @ts-ignore: Unused imports
import * as ent$0 from "../models/ent/models.js";
/**
* GetAllExtensions 获取所有扩展
*/
export function GetAllExtensions(): Promise<(ent$0.Extension | null)[]> & { cancel(): void } {
let $resultPromise = $Call.ByID(3094292124) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType2($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* GetDefaultExtensions 获取默认扩展配置(用于前端绑定生成)
*/
export function GetDefaultExtensions(): Promise<models$0.Extension[]> & { cancel(): void } {
let $resultPromise = $Call.ByID(4036328166) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType4($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* GetExtensionByKey 根据Key获取扩展
*/
export function GetExtensionByKey(key: string): Promise<ent$0.Extension | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(2551065776, key) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType1($result);
}) as any;
@@ -56,11 +32,47 @@ export function GetExtensionByKey(key: string): Promise<ent$0.Extension | null>
return $typingPromise;
}
/**
* GetExtensionByID 根据ID获取扩展
*/
export function GetExtensionByID(id: number): Promise<ent$0.Extension | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(1521424252, id) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType3($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* GetExtensionConfig 获取扩展配置
*/
export function GetExtensionConfig(id: number): Promise<{ [_: string]: any }> & { cancel(): void } {
let $resultPromise = $Call.ByID(1629559882, id) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType4($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* GetExtensions 获取所有扩展
*/
export function GetExtensions(): Promise<(ent$0.Extension | null)[]> & { cancel(): void } {
let $resultPromise = $Call.ByID(3179289021) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType5($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* ResetExtensionConfig 重置单个扩展到默认状态
*/
export function ResetExtensionConfig(key: string): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3990780299, key) as any;
export function ResetExtensionConfig(id: number): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3990780299, id) as any;
return $resultPromise;
}
@@ -83,22 +95,23 @@ export function SyncExtensions(): Promise<void> & { cancel(): void } {
/**
* UpdateExtensionConfig 更新扩展配置
*/
export function UpdateExtensionConfig(key: string, config: { [_: string]: any }): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3184142503, key, config) as any;
export function UpdateExtensionConfig(id: number, config: { [_: string]: any }): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3184142503, id, config) as any;
return $resultPromise;
}
/**
* UpdateExtensionEnabled 更新扩展启用状态
*/
export function UpdateExtensionEnabled(key: string, enabled: boolean): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1067300094, key, enabled) as any;
export function UpdateExtensionEnabled(id: number, enabled: boolean): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1067300094, id, enabled) as any;
return $resultPromise;
}
// Private type creation functions
const $$createType0 = ent$0.Extension.createFrom;
const $$createType1 = $Create.Nullable($$createType0);
const $$createType2 = $Create.Array($$createType1);
const $$createType3 = models$0.Extension.createFrom;
const $$createType4 = $Create.Array($$createType3);
const $$createType0 = models$0.Extension.createFrom;
const $$createType1 = $Create.Array($$createType0);
const $$createType2 = ent$0.Extension.createFrom;
const $$createType3 = $Create.Nullable($$createType2);
const $$createType4 = $Create.Map($Create.Any, $Create.Any);
const $$createType5 = $Create.Array($$createType3);

View File

@@ -21,22 +21,34 @@ import * as models$0 from "../models/models.js";
import * as ent$0 from "../models/ent/models.js";
/**
* GetAllKeyBindings 获取所有快捷键
* GetDefaultKeyBindings 获取默认快捷键配置
*/
export function GetAllKeyBindings(): Promise<(ent$0.KeyBinding | null)[]> & { cancel(): void } {
let $resultPromise = $Call.ByID(1633502882) as any;
export function GetDefaultKeyBindings(): Promise<models$0.KeyBinding[]> & { cancel(): void } {
let $resultPromise = $Call.ByID(3843471588) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType2($result);
return $$createType1($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* GetDefaultKeyBindings 获取默认快捷键配置
* GetKeyBindingByID 根据ID获取快捷键
*/
export function GetDefaultKeyBindings(): Promise<models$0.KeyBinding[]> & { cancel(): void } {
let $resultPromise = $Call.ByID(3843471588) as any;
export function GetKeyBindingByID(id: number): Promise<ent$0.KeyBinding | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(1578192526, id) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType3($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* GetKeyBindings 根据类型获取快捷键
*/
export function GetKeyBindings(kbType: models$0.KeyBindingType): Promise<(ent$0.KeyBinding | null)[]> & { cancel(): void } {
let $resultPromise = $Call.ByID(4253885163, kbType) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType4($result);
}) as any;
@@ -45,15 +57,11 @@ export function GetDefaultKeyBindings(): Promise<models$0.KeyBinding[]> & { canc
}
/**
* GetKeyBindingByKey 根据Key获取快捷键
* ResetKeyBindings 重置所有快捷键到默认值
*/
export function GetKeyBindingByKey(key: string): Promise<ent$0.KeyBinding | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(852938650, key) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType1($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
export function ResetKeyBindings(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(4251626010) as any;
return $resultPromise;
}
/**
@@ -73,24 +81,24 @@ export function SyncKeyBindings(): Promise<void> & { cancel(): void } {
}
/**
* UpdateKeyBindingCommand 更新快捷键命令
* UpdateKeyBindingEnabled 更新快捷键启用状态
*/
export function UpdateKeyBindingCommand(key: string, command: string): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1293670628, key, command) as any;
export function UpdateKeyBindingEnabled(id: number, enabled: boolean): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(843626124, id, enabled) as any;
return $resultPromise;
}
/**
* UpdateKeyBindingEnabled 更新快捷键启用状态
* UpdateKeyBindingKeys 更新快捷键绑定(根据操作系统自动判断更新哪个字段)
*/
export function UpdateKeyBindingEnabled(key: string, enabled: boolean): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(843626124, key, enabled) as any;
export function UpdateKeyBindingKeys(id: number, key: string): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3432755175, id, key) as any;
return $resultPromise;
}
// Private type creation functions
const $$createType0 = ent$0.KeyBinding.createFrom;
const $$createType1 = $Create.Nullable($$createType0);
const $$createType2 = $Create.Array($$createType1);
const $$createType3 = models$0.KeyBinding.createFrom;
const $$createType0 = models$0.KeyBinding.createFrom;
const $$createType1 = $Create.Array($$createType0);
const $$createType2 = ent$0.KeyBinding.createFrom;
const $$createType3 = $Create.Nullable($$createType2);
const $$createType4 = $Create.Array($$createType3);

View File

@@ -18,10 +18,10 @@ import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/applic
import * as ent$0 from "../models/ent/models.js";
/**
* GetThemeByKey 根据Key获取主题
* GetThemeByName 根据Key获取主题
*/
export function GetThemeByKey(key: string): Promise<ent$0.Theme | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(808794256, key) as any;
export function GetThemeByName(name: string): Promise<ent$0.Theme | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(1938954770, name) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType1($result);
}) as any;

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);