dsh-vscode-mode 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -10
- package/lib/client.js +1092 -475
- package/lib/client.js.map +1 -1
- package/lib/index.js +91 -41
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/activePathStore.ts +28 -0
- package/src/client/commandBridge.ts +9 -41
- package/src/client/index.ts +91 -14
- package/src/client/keybindings.ts +19 -73
- package/src/client/shortcutsOfficial.ts +451 -0
- package/src/client/sidebar/panels/RulesPanel.ts +15 -11
- package/src/client/ui/EditorView.ts +10 -83
- package/src/client/ui/McpSettings.ts +4 -5
- package/src/client/ui/QuickOpen.ts +10 -11
- package/src/client/ui/ShortcutSettings.ts +294 -0
- package/src/client/ui/commandCatalog.ts +36 -41
- package/src/fileOpenSettings.ts +3 -0
- package/src/rules.ts +100 -44
- package/src/shared/keybindings.ts +253 -0
- package/src/shared/rules.ts +9 -1
- package/src/client/ui/KeybindingsPanel.ts +0 -190
package/lib/client.js
CHANGED
|
@@ -1921,6 +1921,33 @@ window.__ModuleLoader__.load({
|
|
|
1921
1921
|
return MARKDOWN_EXT_SET.has(base.slice(dot + 1).toLowerCase());
|
|
1922
1922
|
}
|
|
1923
1923
|
//#endregion
|
|
1924
|
+
//#region src/client/activePathStore.ts
|
|
1925
|
+
/**
|
|
1926
|
+
* dsh-vscode-mode client — 活动编辑器文件路径单点(模块级,不依赖 React)。
|
|
1927
|
+
* 命令目录(ui/commandCatalog)是模块级常量、拿不到 EditorView 实例;其可用性判定
|
|
1928
|
+
* 需要「当前活动文件是什么」(如 Markdown 预览切换仅 .md 活动文件吞键),故由
|
|
1929
|
+
* EditorView 把 active 状态镜像到本 store,目录按需读取。
|
|
1930
|
+
* 作者 ddj 2026年10月
|
|
1931
|
+
*/
|
|
1932
|
+
/** 当前活动文件路径(null = 无活动文件/编辑器未挂载)。 */
|
|
1933
|
+
let activePath = null;
|
|
1934
|
+
/**
|
|
1935
|
+
* 镜像活动文件路径(EditorView 渲染同步点调用;卸载时回写 null)。
|
|
1936
|
+
* @author ddj 2026年10月
|
|
1937
|
+
* @param path 活动文件路径或 null
|
|
1938
|
+
*/
|
|
1939
|
+
function setActiveEditorPath(path) {
|
|
1940
|
+
activePath = path;
|
|
1941
|
+
}
|
|
1942
|
+
/**
|
|
1943
|
+
* 读取当前活动文件路径。
|
|
1944
|
+
* @author ddj 2026年10月
|
|
1945
|
+
* @returns 活动文件路径或 null
|
|
1946
|
+
*/
|
|
1947
|
+
function activeEditorPath() {
|
|
1948
|
+
return activePath;
|
|
1949
|
+
}
|
|
1950
|
+
//#endregion
|
|
1924
1951
|
//#region src/client/md/componentType.ts
|
|
1925
1952
|
/**
|
|
1926
1953
|
* dsh-vscode-mode client — React 元素类型判定(纯函数,无 React/primitives 依赖)。
|
|
@@ -3976,62 +4003,6 @@ window.__ModuleLoader__.load({
|
|
|
3976
4003
|
return regions;
|
|
3977
4004
|
}
|
|
3978
4005
|
//#endregion
|
|
3979
|
-
//#region src/shared/keybindings.ts
|
|
3980
|
-
/**
|
|
3981
|
-
* dsh-vscode-mode shared — 快捷键命令目录与默认键位(双面契约)。
|
|
3982
|
-
* 纯数据模块:host(settings schema 默认值)与 client(执行匹配/设置页)共用。
|
|
3983
|
-
* 键位格式:修饰符 + 主键,`+` 连接,如 `Ctrl+Shift+F`;空串 = 未绑定;
|
|
3984
|
-
* `|` 连接多个候选(任一命中即触发),如 `Alt+ArrowLeft|Ctrl+Alt+-`。
|
|
3985
|
-
* 约束:本模块被 host 与 client 两半共用,**禁止 import client 侧模块**(React/浏览器 API)。
|
|
3986
|
-
* 与 client/ui/commandCatalog 的一致性由 tests/commands.test.ts 断言兜底。
|
|
3987
|
-
* 作者 ddj 2026年08月26号 / 2026年09月10号
|
|
3988
|
-
*/
|
|
3989
|
-
/** 命令 id → 默认键位(可为多候选)。命令目录以此为准,新增命令只需加一项。 */
|
|
3990
|
-
const KEYBINDING_DEFAULTS = {
|
|
3991
|
-
"edrv.save": "Ctrl+S",
|
|
3992
|
-
"edrv.quickOpen": "Ctrl+P",
|
|
3993
|
-
"edrv.toggleSidebar": "Ctrl+B",
|
|
3994
|
-
"edrv.searchInFiles": "Ctrl+Shift+F",
|
|
3995
|
-
"edrv.toggleMarkdownPreview": "Ctrl+Shift+V",
|
|
3996
|
-
"edrv.navigateBack": "Alt+ArrowLeft|Ctrl+Alt+-",
|
|
3997
|
-
"edrv.navigateForward": "Alt+ArrowRight|Ctrl+Shift+-",
|
|
3998
|
-
"edrv.nextTab": "Ctrl+Alt+ArrowRight|Ctrl+PageDown",
|
|
3999
|
-
"edrv.prevTab": "Ctrl+Alt+ArrowLeft|Ctrl+PageUp",
|
|
4000
|
-
"edrv.goToLine": "Ctrl+G",
|
|
4001
|
-
"edrv.showCommands": "Ctrl+Shift+P|F1",
|
|
4002
|
-
"edrv.nextEditorRow": "Ctrl+Alt+ArrowDown",
|
|
4003
|
-
"edrv.prevEditorRow": "Ctrl+Alt+ArrowUp",
|
|
4004
|
-
"edrv.addSelectionRef": "Ctrl+U",
|
|
4005
|
-
"edrv.closeTab": "Ctrl+F4",
|
|
4006
|
-
"edrv.debugToggleBreakpoint": "F9",
|
|
4007
|
-
"edrv.debugStartContinue": "F5",
|
|
4008
|
-
"edrv.debugStepOver": "F10",
|
|
4009
|
-
"edrv.debugStepInto": "F11",
|
|
4010
|
-
"edrv.debugStepOut": "Shift+F11",
|
|
4011
|
-
"edrv.debugStop": "Shift+F5"
|
|
4012
|
-
};
|
|
4013
|
-
/**
|
|
4014
|
-
* 返回默认键位的独立副本(调用方修改不污染目录)。
|
|
4015
|
-
* @author ddj 2026年08月26号
|
|
4016
|
-
* @returns 默认键位映射
|
|
4017
|
-
*/
|
|
4018
|
-
function defaultKeybindings() {
|
|
4019
|
-
return { ...KEYBINDING_DEFAULTS };
|
|
4020
|
-
}
|
|
4021
|
-
/**
|
|
4022
|
-
* 规整外部键位数据:只保留已知命令 id 且为字符串的值,未知 id 丢弃。
|
|
4023
|
-
* 防止用户文档/旧版本写入的未知命令污染执行匹配。
|
|
4024
|
-
* @author ddj 2026年08月26号
|
|
4025
|
-
* @param raw 原始设置值
|
|
4026
|
-
* @returns 规整后的键位映射(缺省项不补默认值,由调用方按需合并)
|
|
4027
|
-
*/
|
|
4028
|
-
function normalizeKeybindings(raw) {
|
|
4029
|
-
const out = {};
|
|
4030
|
-
if (!raw || typeof raw !== "object") return out;
|
|
4031
|
-
for (const [id, chord] of Object.entries(raw)) if (id in KEYBINDING_DEFAULTS && typeof chord === "string") out[id] = chord;
|
|
4032
|
-
return out;
|
|
4033
|
-
}
|
|
4034
|
-
//#endregion
|
|
4035
4006
|
//#region src/client/editorModelState.ts
|
|
4036
4007
|
/** Monaco 编辑器实例选择器(限定在插件编辑器行内,排除官方预览等其它 Monaco)。 */
|
|
4037
4008
|
const EDITOR_MODEL_SELECTOR = ".edrv-editor-row .monaco-editor";
|
|
@@ -6162,8 +6133,8 @@ window.__ModuleLoader__.load({
|
|
|
6162
6133
|
//#region src/client/ui/commandCatalog.ts
|
|
6163
6134
|
/**
|
|
6164
6135
|
* dsh-vscode-mode client — 编辑器指令目录(纯元数据)。
|
|
6165
|
-
* 「一条命令 =
|
|
6166
|
-
* 全部从本表读取;新增一条能力只需在 EDITOR_COMMANDS
|
|
6136
|
+
* 「一条命令 = 一条注册数据」:命令栏、快捷键设置页与 Monaco 右键菜单
|
|
6137
|
+
* 全部从本表读取;新增一条能力只需在 EDITOR_COMMANDS 追加一项(默认键位见 shared/keybindings SHORTCUT_PROFILES)。
|
|
6167
6138
|
* run 只派发 `edrv.command.*` 窗口事件,不直接触碰 React/Monaco(`edrv.command.` 前缀
|
|
6168
6139
|
* 与既有 `edrv:` 刷新/主题事件分属不同命名空间,互不干扰)。
|
|
6169
6140
|
* 作者 ddj 2026年09月10号
|
|
@@ -6197,7 +6168,6 @@ window.__ModuleLoader__.load({
|
|
|
6197
6168
|
label: "下一编辑行(光标整行下移)",
|
|
6198
6169
|
category: "导航",
|
|
6199
6170
|
order: 50,
|
|
6200
|
-
keybinding: "Ctrl+Alt+ArrowDown",
|
|
6201
6171
|
available: needsModel,
|
|
6202
6172
|
run: () => emit$1("nextEditorRow")
|
|
6203
6173
|
};
|
|
@@ -6213,7 +6183,6 @@ window.__ModuleLoader__.load({
|
|
|
6213
6183
|
label: "上一编辑行(光标整行上移)",
|
|
6214
6184
|
category: "导航",
|
|
6215
6185
|
order: 60,
|
|
6216
|
-
keybinding: "Ctrl+Alt+ArrowUp",
|
|
6217
6186
|
available: needsModel,
|
|
6218
6187
|
run: () => emit$1("prevEditorRow")
|
|
6219
6188
|
};
|
|
@@ -6229,7 +6198,6 @@ window.__ModuleLoader__.load({
|
|
|
6229
6198
|
label: "转到行",
|
|
6230
6199
|
category: "导航",
|
|
6231
6200
|
order: 45,
|
|
6232
|
-
keybinding: "Ctrl+G",
|
|
6233
6201
|
available: needsModel,
|
|
6234
6202
|
run: () => emit$1("goToLine")
|
|
6235
6203
|
};
|
|
@@ -6246,7 +6214,6 @@ window.__ModuleLoader__.load({
|
|
|
6246
6214
|
label: "添加选中内容为引用",
|
|
6247
6215
|
category: "编辑",
|
|
6248
6216
|
order: 10,
|
|
6249
|
-
keybinding: "Ctrl+U",
|
|
6250
6217
|
available: needsModel,
|
|
6251
6218
|
run: () => emit$1("addSelectionRef")
|
|
6252
6219
|
};
|
|
@@ -6264,7 +6231,6 @@ window.__ModuleLoader__.load({
|
|
|
6264
6231
|
label: "关闭当前页签",
|
|
6265
6232
|
category: "文件",
|
|
6266
6233
|
order: 40,
|
|
6267
|
-
keybinding: "Ctrl+F4",
|
|
6268
6234
|
available: hasOpenTabs,
|
|
6269
6235
|
run: () => emit$1("closeTab")
|
|
6270
6236
|
};
|
|
@@ -6332,9 +6298,10 @@ window.__ModuleLoader__.load({
|
|
|
6332
6298
|
}
|
|
6333
6299
|
/**
|
|
6334
6300
|
* 编辑器内置指令目录(顺序 = 快捷键设置页展示顺序)。
|
|
6335
|
-
*
|
|
6336
|
-
*
|
|
6337
|
-
*
|
|
6301
|
+
* 键位派发已归官方 shortcuts 机制:默认键位表在 shared/keybindings.ts 的 SHORTCUT_PROFILES,
|
|
6302
|
+
* 由 client/index.ts 经 shortcutsOfficial.registerOfficialShortcuts 注册,本目录只管
|
|
6303
|
+
* 「标签/可用性/执行」。无默认键位的命令同样注册(官方 UI/插件子页内可自定义绑定)。
|
|
6304
|
+
* @author ddj 2026年09月10号 / 2026年10月
|
|
6338
6305
|
*/
|
|
6339
6306
|
const EDITOR_COMMANDS = [
|
|
6340
6307
|
{
|
|
@@ -6342,7 +6309,6 @@ window.__ModuleLoader__.load({
|
|
|
6342
6309
|
label: "保存文件",
|
|
6343
6310
|
category: "文件",
|
|
6344
6311
|
order: 10,
|
|
6345
|
-
keybinding: "Ctrl+S",
|
|
6346
6312
|
available: needsModel,
|
|
6347
6313
|
run: () => emit$1("save")
|
|
6348
6314
|
},
|
|
@@ -6351,7 +6317,6 @@ window.__ModuleLoader__.load({
|
|
|
6351
6317
|
label: "快速打开文件",
|
|
6352
6318
|
category: "文件",
|
|
6353
6319
|
order: 20,
|
|
6354
|
-
keybinding: "Ctrl+P",
|
|
6355
6320
|
available: alwaysAvailable,
|
|
6356
6321
|
run: () => emit$1("quickOpen")
|
|
6357
6322
|
},
|
|
@@ -6360,7 +6325,6 @@ window.__ModuleLoader__.load({
|
|
|
6360
6325
|
label: "切换侧边栏",
|
|
6361
6326
|
category: "视图",
|
|
6362
6327
|
order: 10,
|
|
6363
|
-
keybinding: "Ctrl+B",
|
|
6364
6328
|
available: alwaysAvailable,
|
|
6365
6329
|
run: () => emit$1("toggleSidebar")
|
|
6366
6330
|
},
|
|
@@ -6369,7 +6333,6 @@ window.__ModuleLoader__.load({
|
|
|
6369
6333
|
label: "在工作区中搜索",
|
|
6370
6334
|
category: "视图",
|
|
6371
6335
|
order: 20,
|
|
6372
|
-
keybinding: "Ctrl+Shift+F",
|
|
6373
6336
|
available: alwaysAvailable,
|
|
6374
6337
|
run: () => emit$1("searchInFiles")
|
|
6375
6338
|
},
|
|
@@ -6378,8 +6341,10 @@ window.__ModuleLoader__.load({
|
|
|
6378
6341
|
label: "切换 Markdown 预览",
|
|
6379
6342
|
category: "视图",
|
|
6380
6343
|
order: 25,
|
|
6381
|
-
|
|
6382
|
-
|
|
6344
|
+
available: () => {
|
|
6345
|
+
const path = activeEditorPath();
|
|
6346
|
+
return typeof path === "string" && isMarkdownPath(path);
|
|
6347
|
+
},
|
|
6383
6348
|
run: () => emit$1("toggleMarkdownPreview")
|
|
6384
6349
|
},
|
|
6385
6350
|
{
|
|
@@ -6395,7 +6360,6 @@ window.__ModuleLoader__.load({
|
|
|
6395
6360
|
label: "后退(导航历史)",
|
|
6396
6361
|
category: "导航",
|
|
6397
6362
|
order: 10,
|
|
6398
|
-
keybinding: "Alt+ArrowLeft|Ctrl+Alt+-",
|
|
6399
6363
|
available: needsModel,
|
|
6400
6364
|
run: () => emit$1("navigateBack")
|
|
6401
6365
|
},
|
|
@@ -6404,7 +6368,6 @@ window.__ModuleLoader__.load({
|
|
|
6404
6368
|
label: "前进(导航历史)",
|
|
6405
6369
|
category: "导航",
|
|
6406
6370
|
order: 20,
|
|
6407
|
-
keybinding: "Alt+ArrowRight|Ctrl+Shift+-",
|
|
6408
6371
|
available: needsModel,
|
|
6409
6372
|
run: () => emit$1("navigateForward")
|
|
6410
6373
|
},
|
|
@@ -6413,7 +6376,6 @@ window.__ModuleLoader__.load({
|
|
|
6413
6376
|
label: "下一个页签",
|
|
6414
6377
|
category: "导航",
|
|
6415
6378
|
order: 30,
|
|
6416
|
-
keybinding: "Ctrl+Alt+ArrowRight|Ctrl+PageDown",
|
|
6417
6379
|
available: needsModel,
|
|
6418
6380
|
run: () => emit$1("nextTab")
|
|
6419
6381
|
},
|
|
@@ -6422,7 +6384,6 @@ window.__ModuleLoader__.load({
|
|
|
6422
6384
|
label: "上一个页签",
|
|
6423
6385
|
category: "导航",
|
|
6424
6386
|
order: 40,
|
|
6425
|
-
keybinding: "Ctrl+Alt+ArrowLeft|Ctrl+PageUp",
|
|
6426
6387
|
available: needsModel,
|
|
6427
6388
|
run: () => emit$1("prevTab")
|
|
6428
6389
|
},
|
|
@@ -6493,7 +6454,6 @@ window.__ModuleLoader__.load({
|
|
|
6493
6454
|
label: "调试:切换断点(光标行)",
|
|
6494
6455
|
category: "调试",
|
|
6495
6456
|
order: 10,
|
|
6496
|
-
keybinding: "F9",
|
|
6497
6457
|
available: needsModel,
|
|
6498
6458
|
run: () => emit$1("debugToggleBreakpoint")
|
|
6499
6459
|
},
|
|
@@ -6502,7 +6462,6 @@ window.__ModuleLoader__.load({
|
|
|
6502
6462
|
label: "调试:启动 / 继续",
|
|
6503
6463
|
category: "调试",
|
|
6504
6464
|
order: 20,
|
|
6505
|
-
keybinding: "F5",
|
|
6506
6465
|
available: canStart,
|
|
6507
6466
|
run: () => emit$1("debugStartContinue")
|
|
6508
6467
|
},
|
|
@@ -6511,7 +6470,6 @@ window.__ModuleLoader__.load({
|
|
|
6511
6470
|
label: "调试:单步跳过",
|
|
6512
6471
|
category: "调试",
|
|
6513
6472
|
order: 30,
|
|
6514
|
-
keybinding: "F10",
|
|
6515
6473
|
available: paused,
|
|
6516
6474
|
run: () => emit$1("debugStepOver")
|
|
6517
6475
|
},
|
|
@@ -6520,7 +6478,6 @@ window.__ModuleLoader__.load({
|
|
|
6520
6478
|
label: "调试:单步步入",
|
|
6521
6479
|
category: "调试",
|
|
6522
6480
|
order: 40,
|
|
6523
|
-
keybinding: "F11",
|
|
6524
6481
|
available: paused,
|
|
6525
6482
|
run: () => emit$1("debugStepInto")
|
|
6526
6483
|
},
|
|
@@ -6529,7 +6486,6 @@ window.__ModuleLoader__.load({
|
|
|
6529
6486
|
label: "调试:单步步出",
|
|
6530
6487
|
category: "调试",
|
|
6531
6488
|
order: 50,
|
|
6532
|
-
keybinding: "Shift+F11",
|
|
6533
6489
|
available: paused,
|
|
6534
6490
|
run: () => emit$1("debugStepOut")
|
|
6535
6491
|
},
|
|
@@ -6538,16 +6494,15 @@ window.__ModuleLoader__.load({
|
|
|
6538
6494
|
label: "调试:停止",
|
|
6539
6495
|
category: "调试",
|
|
6540
6496
|
order: 60,
|
|
6541
|
-
keybinding: "Shift+F5",
|
|
6542
6497
|
available: canStop,
|
|
6543
6498
|
run: () => emit$1("debugStop")
|
|
6544
6499
|
}
|
|
6545
6500
|
];
|
|
6546
6501
|
}
|
|
6547
6502
|
/**
|
|
6548
|
-
*
|
|
6549
|
-
*
|
|
6550
|
-
* @author ddj 2026年09月10号
|
|
6503
|
+
* 非编辑器原生路径的指令(键位统一由官方 shortcuts 机制派发;历史「桥接」分组名保留)。
|
|
6504
|
+
* 新增「只填目录」的编辑器指令一律放这里:命令栏与官方快捷键目录自动可用。
|
|
6505
|
+
* @author ddj 2026年09月10号 / 2026年10月
|
|
6551
6506
|
*/
|
|
6552
6507
|
const BRIDGE_COMMANDS = [
|
|
6553
6508
|
nextEditorRowDef(),
|
|
@@ -6569,32 +6524,22 @@ window.__ModuleLoader__.load({
|
|
|
6569
6524
|
label: "显示所有命令",
|
|
6570
6525
|
category: "视图",
|
|
6571
6526
|
order: 1,
|
|
6572
|
-
keybinding: "Ctrl+Shift+P|F1",
|
|
6573
6527
|
available: alwaysAvailable,
|
|
6574
6528
|
run
|
|
6575
6529
|
};
|
|
6576
6530
|
}
|
|
6577
|
-
/**
|
|
6578
|
-
* 需要全局键位派发的指令(桥接类 + 命令栏自身;不含 EditorView/QuickOpen 原生监听的那些)。
|
|
6579
|
-
* @author ddj 2026年09月10号
|
|
6580
|
-
* @param showCommands 命令栏命令定义
|
|
6581
|
-
* @returns 派发集合
|
|
6582
|
-
*/
|
|
6583
|
-
function dispatchedCommands(showCommands) {
|
|
6584
|
-
return [...BRIDGE_COMMANDS, showCommands];
|
|
6585
|
-
}
|
|
6586
6531
|
//#endregion
|
|
6587
6532
|
//#region src/client/keybindings.ts
|
|
6588
6533
|
/**
|
|
6589
|
-
* dsh-vscode-mode client —
|
|
6534
|
+
* dsh-vscode-mode client — 快捷键配置模块(解析/匹配/官方目录同步)。
|
|
6590
6535
|
* 纯逻辑(parseChord/parseChords/formatChord/matchEvent/chordFromEvent/normalizeKey)不依赖 DOM,可单测;
|
|
6591
|
-
*
|
|
6592
|
-
*
|
|
6593
|
-
*
|
|
6594
|
-
*
|
|
6595
|
-
* 作者 ddj 2026年08月26号 / 2026年09月10号
|
|
6536
|
+
* 弦表(current)由官方 shortcuts 目录驱动:client/index.ts 经 shortcutsOfficial.bindCatalogChords
|
|
6537
|
+
* 订阅官方 catalog 后调 applyOfficialChords(键位持久化/冲突校验/派发全部归官方机制,本模块不再自派发)。
|
|
6538
|
+
* 键位语义:Ctrl 与 Cmd 互认(延续历史捕获行为)。
|
|
6539
|
+
* 命令目录(COMMANDS)派生自指令目录 commandCatalog(指令 = 单一数据源)。
|
|
6540
|
+
* 作者 ddj 2026年08月26号 / 2026年09月10号 / 2026年10月
|
|
6596
6541
|
*/
|
|
6597
|
-
/**
|
|
6542
|
+
/** 命令目录(设置页展示标签;命令栏候选与键位提示共用)。 */
|
|
6598
6543
|
const COMMANDS = [
|
|
6599
6544
|
...EDITOR_COMMANDS.map((command) => ({
|
|
6600
6545
|
id: command.id,
|
|
@@ -6609,21 +6554,6 @@ window.__ModuleLoader__.load({
|
|
|
6609
6554
|
label: command.label
|
|
6610
6555
|
}))
|
|
6611
6556
|
];
|
|
6612
|
-
/** 命令栏命令定义(键位自检与目录展示共用;run 不参与键位逻辑)。 */
|
|
6613
|
-
function paletteCommand() {
|
|
6614
|
-
return showCommandsDef(() => {});
|
|
6615
|
-
}
|
|
6616
|
-
/**
|
|
6617
|
-
* 目录与默认键位表一致性自检(仅告警不中断):新指令漏写共享表时第一时间可见。
|
|
6618
|
-
* @author ddj 2026年09月10号
|
|
6619
|
-
*/
|
|
6620
|
-
function checkDefaultsDrift() {
|
|
6621
|
-
for (const command of [...EDITOR_COMMANDS, paletteCommand()]) {
|
|
6622
|
-
if (!command.keybinding) continue;
|
|
6623
|
-
if (KEYBINDING_DEFAULTS[command.id] === command.keybinding) continue;
|
|
6624
|
-
log.warn("指令默认键位与共享表不一致:" + command.id + " 目录=" + command.keybinding + " 共享表=" + String(KEYBINDING_DEFAULTS[command.id]));
|
|
6625
|
-
}
|
|
6626
|
-
}
|
|
6627
6557
|
const MODIFIERS = {
|
|
6628
6558
|
ctrl: "ctrl",
|
|
6629
6559
|
cmd: "meta",
|
|
@@ -6631,24 +6561,17 @@ window.__ModuleLoader__.load({
|
|
|
6631
6561
|
shift: "shift",
|
|
6632
6562
|
alt: "alt"
|
|
6633
6563
|
};
|
|
6634
|
-
|
|
6635
|
-
|
|
6564
|
+
/** 弦表(官方 catalog 派生:id → 显示键位弦)。官方目录就绪前为空(tooltip 走各自的兜底文案)。 */
|
|
6565
|
+
let current = {};
|
|
6636
6566
|
const listeners$2 = /* @__PURE__ */ new Set();
|
|
6637
|
-
checkDefaultsDrift();
|
|
6638
6567
|
/**
|
|
6639
|
-
*
|
|
6640
|
-
*
|
|
6641
|
-
* @
|
|
6642
|
-
* @param raw 设置 scope 的 keybindings 字段
|
|
6568
|
+
* 应用官方目录弦表(id → 显示键位弦;由 shortcutsOfficial.bindCatalogChords 全量推送)。
|
|
6569
|
+
* @author ddj 2026年10月
|
|
6570
|
+
* @param chords 弦表(官方 catalog 派生,含用户覆盖;issue 行不收录)
|
|
6643
6571
|
*/
|
|
6644
|
-
function
|
|
6645
|
-
current = {
|
|
6646
|
-
|
|
6647
|
-
...normalizeKeybindings(raw)
|
|
6648
|
-
};
|
|
6649
|
-
for (const listener of listeners$2) try {
|
|
6650
|
-
listener();
|
|
6651
|
-
} catch {}
|
|
6572
|
+
function applyOfficialChords(chords) {
|
|
6573
|
+
current = { ...chords };
|
|
6574
|
+
notifyKeybindings();
|
|
6652
6575
|
}
|
|
6653
6576
|
/**
|
|
6654
6577
|
* 订阅键位配置变化。
|
|
@@ -6660,33 +6583,23 @@ window.__ModuleLoader__.load({
|
|
|
6660
6583
|
listeners$2.add(listener);
|
|
6661
6584
|
return () => listeners$2.delete(listener);
|
|
6662
6585
|
}
|
|
6586
|
+
/** 通知键位订阅者(异常隔离)。 */
|
|
6587
|
+
function notifyKeybindings() {
|
|
6588
|
+
for (const listener of listeners$2) try {
|
|
6589
|
+
listener();
|
|
6590
|
+
} catch {}
|
|
6591
|
+
}
|
|
6663
6592
|
/**
|
|
6664
|
-
*
|
|
6665
|
-
* @author ddj 2026年08月26号 / 2026年
|
|
6593
|
+
* 当前命令的键位弦(官方目录派生;未绑定/空 → null)。
|
|
6594
|
+
* @author ddj 2026年08月26号 / 2026年10月
|
|
6666
6595
|
* @param id 命令 id
|
|
6667
6596
|
* @returns 键位弦或 null
|
|
6668
6597
|
*/
|
|
6669
6598
|
function chordOf(id) {
|
|
6670
|
-
if (runtime.has(id)) {
|
|
6671
|
-
const chords = runtime.get(id) ?? [];
|
|
6672
|
-
return chords.length ? chords.map(formatChord).join("|") : null;
|
|
6673
|
-
}
|
|
6674
6599
|
const chord = current[id];
|
|
6675
6600
|
return typeof chord === "string" && chord.trim() !== "" ? chord : null;
|
|
6676
6601
|
}
|
|
6677
6602
|
/**
|
|
6678
|
-
* 当前命令的解析键位集合(未绑定/非法 → 空数组;含多候选)。
|
|
6679
|
-
* @author ddj 2026年08月26号 / 2026年09月10号
|
|
6680
|
-
* @param id 命令 id
|
|
6681
|
-
* @returns 解析键位数组(可能为空)
|
|
6682
|
-
*/
|
|
6683
|
-
function bindingsOf(id) {
|
|
6684
|
-
const override = runtime.get(id);
|
|
6685
|
-
if (override) return override;
|
|
6686
|
-
const chord = current[id];
|
|
6687
|
-
return typeof chord === "string" ? parseChords(chord) : [];
|
|
6688
|
-
}
|
|
6689
|
-
/**
|
|
6690
6603
|
* 主键规范化:小写字母大写、空格归一为 Space,其余原样(与事件 e.key 对照)。
|
|
6691
6604
|
* @author ddj 2026年08月26号
|
|
6692
6605
|
* @param key 事件主键
|
|
@@ -6777,23 +6690,6 @@ window.__ModuleLoader__.load({
|
|
|
6777
6690
|
return false;
|
|
6778
6691
|
}
|
|
6779
6692
|
/**
|
|
6780
|
-
* 从键盘事件构造键位弦(纯修饰键 → null,供设置页录制)。
|
|
6781
|
-
* @author ddj 2026年08月26号
|
|
6782
|
-
* @param e 键盘事件
|
|
6783
|
-
* @returns 键位弦或 null
|
|
6784
|
-
*/
|
|
6785
|
-
function chordFromEvent(e) {
|
|
6786
|
-
const key = normalizeKey(e.key ?? "");
|
|
6787
|
-
if (!key || key === "Control" || key === "Shift" || key === "Alt" || key === "Meta") return null;
|
|
6788
|
-
return formatChord({
|
|
6789
|
-
ctrl: Boolean(e.ctrlKey),
|
|
6790
|
-
shift: Boolean(e.shiftKey),
|
|
6791
|
-
alt: Boolean(e.altKey),
|
|
6792
|
-
meta: Boolean(e.metaKey),
|
|
6793
|
-
key
|
|
6794
|
-
});
|
|
6795
|
-
}
|
|
6796
|
-
/**
|
|
6797
6693
|
* 键位版本 hook:配置变化时返回新版本(tooltip/占位文案随键位刷新)。
|
|
6798
6694
|
* @author ddj 2026年08月26号
|
|
6799
6695
|
* @returns 当前版本号
|
|
@@ -6895,20 +6791,15 @@ window.__ModuleLoader__.load({
|
|
|
6895
6791
|
inputRef.current?.focus?.();
|
|
6896
6792
|
setOpen(true);
|
|
6897
6793
|
};
|
|
6898
|
-
const
|
|
6899
|
-
if (
|
|
6900
|
-
|
|
6901
|
-
|
|
6902
|
-
openBox();
|
|
6903
|
-
} else if (e.key === "Escape") {
|
|
6904
|
-
setOpen(false);
|
|
6905
|
-
inputRef.current?.blur?.();
|
|
6906
|
-
}
|
|
6794
|
+
const onEscape = (e) => {
|
|
6795
|
+
if (e.key !== "Escape") return;
|
|
6796
|
+
setOpen(false);
|
|
6797
|
+
inputRef.current?.blur?.();
|
|
6907
6798
|
};
|
|
6908
|
-
window.addEventListener("keydown",
|
|
6799
|
+
window.addEventListener("keydown", onEscape, true);
|
|
6909
6800
|
window.addEventListener("edrv.command.quickOpen", openBox);
|
|
6910
6801
|
return () => {
|
|
6911
|
-
window.removeEventListener("keydown",
|
|
6802
|
+
window.removeEventListener("keydown", onEscape, true);
|
|
6912
6803
|
window.removeEventListener("edrv.command.quickOpen", openBox);
|
|
6913
6804
|
};
|
|
6914
6805
|
}, []);
|
|
@@ -18721,6 +18612,8 @@ window.__ModuleLoader__.load({
|
|
|
18721
18612
|
const rowNavMoveRef = react.default.useRef(false);
|
|
18722
18613
|
const activeRef = react.default.useRef(null);
|
|
18723
18614
|
activeRef.current = active;
|
|
18615
|
+
setActiveEditorPath(active);
|
|
18616
|
+
react.default.useEffect(() => () => setActiveEditorPath(null), []);
|
|
18724
18617
|
const tabsRef = react.default.useRef([]);
|
|
18725
18618
|
tabsRef.current = tabs;
|
|
18726
18619
|
const tabPathsRef = react.default.useRef([]);
|
|
@@ -19772,28 +19665,6 @@ window.__ModuleLoader__.load({
|
|
|
19772
19665
|
sessionId,
|
|
19773
19666
|
sidebarKey
|
|
19774
19667
|
]);
|
|
19775
|
-
react.default.useEffect(() => {
|
|
19776
|
-
const onKey = (e) => {
|
|
19777
|
-
if (!matchEvent(e, bindingsOf("edrv.toggleSidebar"))) return;
|
|
19778
|
-
e.preventDefault();
|
|
19779
|
-
e.stopPropagation();
|
|
19780
|
-
setSidebarOn((v) => !v);
|
|
19781
|
-
};
|
|
19782
|
-
window.addEventListener("keydown", onKey, true);
|
|
19783
|
-
return () => window.removeEventListener("keydown", onKey, true);
|
|
19784
|
-
}, []);
|
|
19785
|
-
react.default.useEffect(() => {
|
|
19786
|
-
const onKey = (e) => {
|
|
19787
|
-
if (!matchEvent(e, bindingsOf("edrv.save"))) return;
|
|
19788
|
-
if (!editorRef.current?.getModel?.()) return;
|
|
19789
|
-
e.preventDefault();
|
|
19790
|
-
e.stopPropagation();
|
|
19791
|
-
flushSave();
|
|
19792
|
-
doSaveRef.current(false);
|
|
19793
|
-
};
|
|
19794
|
-
window.addEventListener("keydown", onKey, true);
|
|
19795
|
-
return () => window.removeEventListener("keydown", onKey, true);
|
|
19796
|
-
}, []);
|
|
19797
19668
|
/**
|
|
19798
19669
|
* 打开搜索面板(Ctrl+Shift+F 与命令栏「在工作区中搜索」的唯一动作)。
|
|
19799
19670
|
*
|
|
@@ -19814,59 +19685,6 @@ window.__ModuleLoader__.load({
|
|
|
19814
19685
|
setActivePanel("search");
|
|
19815
19686
|
setTimeout(() => window.dispatchEvent(new CustomEvent("edrv:search-focus")), 0);
|
|
19816
19687
|
};
|
|
19817
|
-
react.default.useEffect(() => {
|
|
19818
|
-
const onKey = (e) => {
|
|
19819
|
-
if (!matchEvent(e, bindingsOf("edrv.searchInFiles"))) return;
|
|
19820
|
-
e.preventDefault();
|
|
19821
|
-
e.stopPropagation();
|
|
19822
|
-
openSearchPanel();
|
|
19823
|
-
};
|
|
19824
|
-
window.addEventListener("keydown", onKey, true);
|
|
19825
|
-
return () => window.removeEventListener("keydown", onKey, true);
|
|
19826
|
-
}, []);
|
|
19827
|
-
react.default.useEffect(() => {
|
|
19828
|
-
const onKey = (e) => {
|
|
19829
|
-
if (!matchEvent(e, bindingsOf("edrv.toggleMarkdownPreview"))) return;
|
|
19830
|
-
const path = activeRef.current;
|
|
19831
|
-
if (!isMarkdownPath(path)) return;
|
|
19832
|
-
e.preventDefault();
|
|
19833
|
-
e.stopPropagation();
|
|
19834
|
-
toggleMdPreviewRef.current?.(path);
|
|
19835
|
-
};
|
|
19836
|
-
window.addEventListener("keydown", onKey, true);
|
|
19837
|
-
return () => window.removeEventListener("keydown", onKey, true);
|
|
19838
|
-
}, []);
|
|
19839
|
-
react.default.useEffect(() => {
|
|
19840
|
-
const onKey = (e) => {
|
|
19841
|
-
if (!matchEvent(e, bindingsOf("edrv.navigateBack"))) return;
|
|
19842
|
-
e.preventDefault();
|
|
19843
|
-
e.stopPropagation();
|
|
19844
|
-
if (navBackRef.current) navBackRef.current();
|
|
19845
|
-
};
|
|
19846
|
-
window.addEventListener("keydown", onKey, true);
|
|
19847
|
-
return () => window.removeEventListener("keydown", onKey, true);
|
|
19848
|
-
}, []);
|
|
19849
|
-
react.default.useEffect(() => {
|
|
19850
|
-
const onKey = (e) => {
|
|
19851
|
-
if (!matchEvent(e, bindingsOf("edrv.navigateForward"))) return;
|
|
19852
|
-
e.preventDefault();
|
|
19853
|
-
e.stopPropagation();
|
|
19854
|
-
if (navForwardRef.current) navForwardRef.current();
|
|
19855
|
-
};
|
|
19856
|
-
window.addEventListener("keydown", onKey, true);
|
|
19857
|
-
return () => window.removeEventListener("keydown", onKey, true);
|
|
19858
|
-
}, []);
|
|
19859
|
-
react.default.useEffect(() => {
|
|
19860
|
-
const onKey = (e) => {
|
|
19861
|
-
const step = matchEvent(e, bindingsOf("edrv.nextTab")) ? 1 : matchEvent(e, bindingsOf("edrv.prevTab")) ? -1 : 0;
|
|
19862
|
-
if (step === 0) return;
|
|
19863
|
-
e.preventDefault();
|
|
19864
|
-
e.stopPropagation();
|
|
19865
|
-
if (cycleTabRef.current) cycleTabRef.current(step);
|
|
19866
|
-
};
|
|
19867
|
-
window.addEventListener("keydown", onKey, true);
|
|
19868
|
-
return () => window.removeEventListener("keydown", onKey, true);
|
|
19869
|
-
}, []);
|
|
19870
19688
|
react.default.useEffect(() => {
|
|
19871
19689
|
ensureSvnStatus(sessionId, scope);
|
|
19872
19690
|
const onSvnStatus = (event) => {
|
|
@@ -23150,179 +22968,6 @@ window.__ModuleLoader__.load({
|
|
|
23150
22968
|
*/
|
|
23151
22969
|
const SettingsContext = react.default.createContext(void 0);
|
|
23152
22970
|
//#endregion
|
|
23153
|
-
//#region src/client/ui/KeybindingsPanel.ts
|
|
23154
|
-
/**
|
|
23155
|
-
* dsh-vscode-mode client — VSCodeMode 设置区「快捷键」管理页。
|
|
23156
|
-
* 命令目录来自 client/keybindings 的 COMMANDS;当前键位读 SettingsContext scope;
|
|
23157
|
-
* 编辑:行内录制(捕获下一次组合键),Esc 取消、Backspace/Delete 清空(未绑定);
|
|
23158
|
-
* 草稿 + 显式保存(scope.set('keybindings', draft));行级恢复 + 页级「恢复默认」;
|
|
23159
|
-
* 冲突键位(两命令同键位)标红警告;scope 外部变更且无未保存修改时回同步草稿。
|
|
23160
|
-
* 作者 ddj 2026-08-26
|
|
23161
|
-
*/
|
|
23162
|
-
/**
|
|
23163
|
-
* 草稿化:默认值 + 设置值合并,空串归一为 null(未绑定)。
|
|
23164
|
-
* @param raw 设置 scope 的 keybindings 字段
|
|
23165
|
-
* @returns 草稿映射(id → 键位弦或 null)
|
|
23166
|
-
*/
|
|
23167
|
-
function draftOf(raw) {
|
|
23168
|
-
const merged = {
|
|
23169
|
-
...defaultKeybindings(),
|
|
23170
|
-
...normalizeKeybindings(raw)
|
|
23171
|
-
};
|
|
23172
|
-
const out = {};
|
|
23173
|
-
for (const [id, chord] of Object.entries(merged)) out[id] = chord === "" ? null : chord;
|
|
23174
|
-
return out;
|
|
23175
|
-
}
|
|
23176
|
-
/**
|
|
23177
|
-
* 存库化:null(未绑定)转空串。
|
|
23178
|
-
* @param draft 草稿映射
|
|
23179
|
-
* @returns 可存储的键位映射
|
|
23180
|
-
*/
|
|
23181
|
-
function storeOf(draft) {
|
|
23182
|
-
const out = {};
|
|
23183
|
-
for (const [id, chord] of Object.entries(draft)) out[id] = chord === null ? "" : chord;
|
|
23184
|
-
return out;
|
|
23185
|
-
}
|
|
23186
|
-
/**
|
|
23187
|
-
* 冲突检测:同一键位被多个命令占用 → 返回冲突命令 id 集合。
|
|
23188
|
-
* @param draft 草稿映射
|
|
23189
|
-
* @returns 冲突命令 id 集合
|
|
23190
|
-
*/
|
|
23191
|
-
function conflictsOf(draft) {
|
|
23192
|
-
const byChord = /* @__PURE__ */ new Map();
|
|
23193
|
-
for (const [id, chord] of Object.entries(draft)) {
|
|
23194
|
-
if (!chord) continue;
|
|
23195
|
-
const list = byChord.get(chord) ?? [];
|
|
23196
|
-
list.push(id);
|
|
23197
|
-
byChord.set(chord, list);
|
|
23198
|
-
}
|
|
23199
|
-
const out = /* @__PURE__ */ new Set();
|
|
23200
|
-
for (const list of byChord.values()) if (list.length > 1) for (const id of list) out.add(id);
|
|
23201
|
-
return out;
|
|
23202
|
-
}
|
|
23203
|
-
/**
|
|
23204
|
-
* 快捷键管理页主体。
|
|
23205
|
-
*/
|
|
23206
|
-
function KeybindingsPanel() {
|
|
23207
|
-
const settings = react.default.useContext(SettingsContext);
|
|
23208
|
-
const snapshot = settings?.getSnapshot?.();
|
|
23209
|
-
const unavailable = !settings || snapshot?.status === "unavailable";
|
|
23210
|
-
const notReady = snapshot?.status !== "ready";
|
|
23211
|
-
const [draft, setDraft] = react.default.useState(() => draftOf(snapshot?.value?.keybindings));
|
|
23212
|
-
const [recording, setRecording] = react.default.useState(null);
|
|
23213
|
-
const [dirty, setDirty] = react.default.useState(false);
|
|
23214
|
-
const [busy, setBusy] = react.default.useState(false);
|
|
23215
|
-
const [error, setError] = react.default.useState("");
|
|
23216
|
-
const [message, setMessage] = react.default.useState("");
|
|
23217
|
-
const dirtyRef = react.default.useRef(false);
|
|
23218
|
-
dirtyRef.current = dirty;
|
|
23219
|
-
const lastKeyRef = react.default.useRef(JSON.stringify(draft));
|
|
23220
|
-
const snapshotValue = snapshot?.value?.keybindings;
|
|
23221
|
-
react.default.useEffect(() => {
|
|
23222
|
-
if (dirtyRef.current) return;
|
|
23223
|
-
const next = draftOf(snapshotValue);
|
|
23224
|
-
const key = JSON.stringify(next);
|
|
23225
|
-
if (key === lastKeyRef.current) return;
|
|
23226
|
-
lastKeyRef.current = key;
|
|
23227
|
-
setDraft(next);
|
|
23228
|
-
}, [snapshotValue]);
|
|
23229
|
-
react.default.useEffect(() => {
|
|
23230
|
-
if (!recording) return;
|
|
23231
|
-
const onKey = (e) => {
|
|
23232
|
-
e.preventDefault();
|
|
23233
|
-
e.stopPropagation();
|
|
23234
|
-
if (e.key === "Escape") {
|
|
23235
|
-
setRecording(null);
|
|
23236
|
-
return;
|
|
23237
|
-
}
|
|
23238
|
-
if (e.key === "Backspace" || e.key === "Delete") {
|
|
23239
|
-
setDraft((prev) => Object.assign({}, prev, { [recording]: null }));
|
|
23240
|
-
setRecording(null);
|
|
23241
|
-
setDirty(true);
|
|
23242
|
-
return;
|
|
23243
|
-
}
|
|
23244
|
-
const chord = chordFromEvent(e);
|
|
23245
|
-
if (!chord) return;
|
|
23246
|
-
setDraft((prev) => Object.assign({}, prev, { [recording]: chord }));
|
|
23247
|
-
setRecording(null);
|
|
23248
|
-
setDirty(true);
|
|
23249
|
-
};
|
|
23250
|
-
window.addEventListener("keydown", onKey, true);
|
|
23251
|
-
return () => window.removeEventListener("keydown", onKey, true);
|
|
23252
|
-
}, [recording]);
|
|
23253
|
-
const conflicts = conflictsOf(draft);
|
|
23254
|
-
const save = () => {
|
|
23255
|
-
if (!settings?.set) {
|
|
23256
|
-
setError("设置服务不可用");
|
|
23257
|
-
return;
|
|
23258
|
-
}
|
|
23259
|
-
setBusy(true);
|
|
23260
|
-
setError("");
|
|
23261
|
-
setMessage("");
|
|
23262
|
-
settings.set("keybindings", storeOf(draft)).then(() => {
|
|
23263
|
-
setDirty(false);
|
|
23264
|
-
lastKeyRef.current = JSON.stringify(draft);
|
|
23265
|
-
setMessage("已保存");
|
|
23266
|
-
}).catch((e) => setError(String(e))).finally(() => setBusy(false));
|
|
23267
|
-
};
|
|
23268
|
-
/**
|
|
23269
|
-
* 恢复全部默认(确认后直接保存,一步到位)。
|
|
23270
|
-
*/
|
|
23271
|
-
const resetAll = () => {
|
|
23272
|
-
if (!settings?.set) {
|
|
23273
|
-
setError("设置服务不可用");
|
|
23274
|
-
return;
|
|
23275
|
-
}
|
|
23276
|
-
if (!window.confirm("恢复全部快捷键为默认值?")) return;
|
|
23277
|
-
setBusy(true);
|
|
23278
|
-
setError("");
|
|
23279
|
-
setMessage("");
|
|
23280
|
-
settings.set("keybindings", { ...defaultKeybindings() }).then(() => {
|
|
23281
|
-
const next = draftOf({});
|
|
23282
|
-
setDraft(next);
|
|
23283
|
-
lastKeyRef.current = JSON.stringify(next);
|
|
23284
|
-
setDirty(false);
|
|
23285
|
-
setMessage("已恢复默认");
|
|
23286
|
-
}).catch((e) => setError(String(e))).finally(() => setBusy(false));
|
|
23287
|
-
};
|
|
23288
|
-
/** 行级恢复该项默认值(进草稿,随保存提交)。 */
|
|
23289
|
-
const resetRow = (id) => {
|
|
23290
|
-
setDraft((prev) => Object.assign({}, prev, { [id]: KEYBINDING_DEFAULTS[id] ?? null }));
|
|
23291
|
-
setDirty(true);
|
|
23292
|
-
};
|
|
23293
|
-
const rows = COMMANDS.map((command) => {
|
|
23294
|
-
const chord = draft[command.id] ?? null;
|
|
23295
|
-
const conflict = chord !== null && conflicts.has(command.id);
|
|
23296
|
-
const invalid = chord !== null && parseChords(chord).length === 0;
|
|
23297
|
-
const recordingRow = recording === command.id;
|
|
23298
|
-
let display;
|
|
23299
|
-
if (recordingRow) display = react.default.createElement("span", { className: "vsm-kb-rec" }, "按下组合键…(Esc 取消 / Backspace 清除)");
|
|
23300
|
-
else display = react.default.createElement("kbd", { className: "vsm-kb-key" + (conflict ? " warn" : "") + (invalid ? " invalid" : "") }, chord ?? "未绑定");
|
|
23301
|
-
return react.default.createElement("div", {
|
|
23302
|
-
key: command.id,
|
|
23303
|
-
className: "vsm-kb-row" + (recordingRow ? " rec" : "")
|
|
23304
|
-
}, react.default.createElement("div", { className: "vsm-kb-label" }, react.default.createElement("span", null, command.label), conflict && react.default.createElement("small", { className: "vsm-kb-warn" }, "与另一命令冲突"), invalid && react.default.createElement("small", { className: "vsm-kb-warn" }, "键位格式无效")), display, react.default.createElement("div", { className: "vsm-kb-actions" }, recordingRow ? react.default.createElement("button", { onClick: () => setRecording(null) }, "取消") : react.default.createElement(react.default.Fragment, null, react.default.createElement("button", {
|
|
23305
|
-
disabled: unavailable || notReady || busy || snapshot?.writable === false,
|
|
23306
|
-
onClick: () => {
|
|
23307
|
-
setRecording(command.id);
|
|
23308
|
-
setMessage("");
|
|
23309
|
-
}
|
|
23310
|
-
}, "编辑"), react.default.createElement("button", {
|
|
23311
|
-
disabled: busy,
|
|
23312
|
-
onClick: () => resetRow(command.id)
|
|
23313
|
-
}, "恢复"))));
|
|
23314
|
-
});
|
|
23315
|
-
return react.default.createElement("section", { className: "vsm-general-page" }, react.default.createElement("h2", null, "快捷键"), react.default.createElement("p", null, "配置编辑器视图的快捷键(命令栏 / 保存 / 快速打开 / 侧边栏 / 搜索 / 后退前进 / 页签与编辑行导航)。修改即时写入配置,重新聚焦编辑器后生效;命令栏(默认 Ctrl+Shift+P 或 F1)可搜索并执行下列全部命令。"), unavailable && react.default.createElement("div", { className: "vsm-mcp-error vsm-mcp-banner" }, "设置服务暂不可用,当前使用默认键位。"), error && react.default.createElement("div", { className: "vsm-mcp-error vsm-mcp-banner" }, error), message && react.default.createElement("div", { className: "vsm-kb-message" }, message), react.default.createElement("section", { className: "vsm-panel" }, react.default.createElement("h3", { className: "vsm-panel-title" }, "编辑器键位"), react.default.createElement("div", { className: "vsm-panel-body" }, react.default.createElement("div", { className: "vsm-kb-list" }, rows)), react.default.createElement("div", { className: "vsm-panel-body vsm-kb-actionsbar" }, react.default.createElement("span", { className: "vsm-kb-dirty" }, dirty ? "有未保存的修改" : ""), react.default.createElement("button", {
|
|
23316
|
-
className: "vsm-primary vsm-small",
|
|
23317
|
-
disabled: unavailable || notReady || busy || snapshot?.writable === false || !dirty,
|
|
23318
|
-
onClick: save
|
|
23319
|
-
}, busy ? "保存中…" : "保存"), react.default.createElement("button", {
|
|
23320
|
-
className: "vsm-small",
|
|
23321
|
-
disabled: unavailable || notReady || busy || snapshot?.writable === false,
|
|
23322
|
-
onClick: resetAll
|
|
23323
|
-
}, "恢复默认"))));
|
|
23324
|
-
}
|
|
23325
|
-
//#endregion
|
|
23326
22971
|
//#region src/client/ui/LspSettings.ts
|
|
23327
22972
|
/**
|
|
23328
22973
|
* dsh-vscode-mode client — 「语言服务器」设置子 Tab。
|
|
@@ -24547,9 +24192,10 @@ window.__ModuleLoader__.load({
|
|
|
24547
24192
|
//#endregion
|
|
24548
24193
|
//#region src/client/ui/McpSettings.ts
|
|
24549
24194
|
/**
|
|
24550
|
-
* dsh-vscode-mode client — VSCodeMode 设置区:通用 /
|
|
24195
|
+
* dsh-vscode-mode client — VSCodeMode 设置区:通用 / MCP 管理 / 语言服务器 / 性能优化 / 兼容性。
|
|
24196
|
+
* (快捷键管理已迁出为独立顶层设置区 ui/ShortcutSettings.ts,与 VSCodeMode 平级。)
|
|
24551
24197
|
* MCP 管理 Tab 下含三个子页签:我的 MCP(profile 全局)/ 项目 MCP(各项目 .mcp.json)/ MCP 市场(占位)。
|
|
24552
|
-
* 作者 ddj 2026年08月22号 / 2026年08月26号 / 2026年08月27号
|
|
24198
|
+
* 作者 ddj 2026年08月22号 / 2026年08月26号 / 2026年08月27号 / 2026年10月
|
|
24553
24199
|
*/
|
|
24554
24200
|
const EMPTY = {
|
|
24555
24201
|
serverName: "",
|
|
@@ -25210,7 +24856,6 @@ window.__ModuleLoader__.load({
|
|
|
25210
24856
|
let body;
|
|
25211
24857
|
if (loading) body = react.default.createElement("div", { className: "vsm-mcp-empty" }, "正在读取 MCP 服务…");
|
|
25212
24858
|
else if (tab === "general") body = react.default.createElement(GeneralSettings, { registry: openerRegistry });
|
|
25213
|
-
else if (tab === "keys") body = react.default.createElement(KeybindingsPanel, null);
|
|
25214
24859
|
else if (tab === "mcp") body = react.default.createElement(McpManagePanel, {
|
|
25215
24860
|
servers,
|
|
25216
24861
|
projects,
|
|
@@ -25235,9 +24880,6 @@ window.__ModuleLoader__.load({
|
|
|
25235
24880
|
className: tab === "general" ? "active" : "",
|
|
25236
24881
|
onClick: () => setTab("general")
|
|
25237
24882
|
}, "通用"), react.default.createElement("button", {
|
|
25238
|
-
className: tab === "keys" ? "active" : "",
|
|
25239
|
-
onClick: () => setTab("keys")
|
|
25240
|
-
}, "快捷键"), react.default.createElement("button", {
|
|
25241
24883
|
className: tab === "mcp" ? "active" : "",
|
|
25242
24884
|
onClick: () => setTab("mcp")
|
|
25243
24885
|
}, "MCP 管理"), react.default.createElement("button", {
|
|
@@ -27656,7 +27298,10 @@ window.__ModuleLoader__.load({
|
|
|
27656
27298
|
className: "edrv-rules-main",
|
|
27657
27299
|
onClick: () => props.onEdit(rule),
|
|
27658
27300
|
title: rule.absPath
|
|
27659
|
-
}, react.default.createElement("div", { className: "edrv-rules-line1" }, react.default.createElement("span", { className: "edrv-rules-file" }, rule.file), !props.narrow ? react.default.createElement("span", { className: "edrv-rules-path" }, "[" + rule.relHint + rule.file + "]") : null,
|
|
27301
|
+
}, react.default.createElement("div", { className: "edrv-rules-line1" }, react.default.createElement("span", { className: "edrv-rules-file" }, rule.file), !props.narrow ? react.default.createElement("span", { className: "edrv-rules-path" }, "[" + rule.relHint + rule.file + "]") : null, props.narrow && rule.scope === "user" && rule.origin === "legacy" ? react.default.createElement("span", {
|
|
27302
|
+
className: "edrv-rules-path",
|
|
27303
|
+
title: rule.absPath
|
|
27304
|
+
}, "旧目录") : null, !rule.enabled ? react.default.createElement("span", { className: "edrv-rules-offtag" }, "已停用") : null), react.default.createElement("div", { className: "edrv-rules-sub" }, react.default.createElement("span", { className: "edrv-rules-sublabel" }, "类型:"), typeBadge(rule.type), desc ? react.default.createElement("span", {
|
|
27660
27305
|
className: "edrv-rules-desc",
|
|
27661
27306
|
title: desc
|
|
27662
27307
|
}, clampDesc(desc, props.descCap)) : null)), react.default.createElement("div", { className: "edrv-rules-actions" }, react.default.createElement("button", {
|
|
@@ -27801,6 +27446,7 @@ window.__ModuleLoader__.load({
|
|
|
27801
27446
|
setForm({
|
|
27802
27447
|
mode: "new",
|
|
27803
27448
|
scope,
|
|
27449
|
+
origin: scope === "user" ? "current" : void 0,
|
|
27804
27450
|
workspacePath: scope === "project" ? cwd : void 0,
|
|
27805
27451
|
relHint: scope === "project" ? ".dsh/rules/" : "rules/",
|
|
27806
27452
|
file: "",
|
|
@@ -27819,6 +27465,7 @@ window.__ModuleLoader__.load({
|
|
|
27819
27465
|
const openEdit = (rule) => {
|
|
27820
27466
|
rpc("rules.read", {
|
|
27821
27467
|
scope: rule.scope,
|
|
27468
|
+
origin: rule.origin,
|
|
27822
27469
|
workspacePath: rule.scope === "project" ? rule.workspacePath : void 0,
|
|
27823
27470
|
file: rule.file
|
|
27824
27471
|
}).then((res) => {
|
|
@@ -27830,6 +27477,7 @@ window.__ModuleLoader__.load({
|
|
|
27830
27477
|
setForm({
|
|
27831
27478
|
mode: "edit",
|
|
27832
27479
|
scope: rule.scope,
|
|
27480
|
+
origin: rule.origin,
|
|
27833
27481
|
workspacePath: rule.workspacePath,
|
|
27834
27482
|
relHint: rule.relHint,
|
|
27835
27483
|
file: rule.file,
|
|
@@ -27910,6 +27558,7 @@ window.__ModuleLoader__.load({
|
|
|
27910
27558
|
setForm(next);
|
|
27911
27559
|
rpc("rules.save", {
|
|
27912
27560
|
scope: form.scope,
|
|
27561
|
+
origin: form.origin,
|
|
27913
27562
|
workspacePath: form.workspacePath,
|
|
27914
27563
|
file,
|
|
27915
27564
|
content: form.content
|
|
@@ -27934,9 +27583,11 @@ window.__ModuleLoader__.load({
|
|
|
27934
27583
|
* @param rule 目标规则
|
|
27935
27584
|
*/
|
|
27936
27585
|
const removeRule = (rule) => {
|
|
27937
|
-
|
|
27586
|
+
const reveal = rule.scope === "user" && rule.origin !== "legacy" ? "\n若旧目录有同名规则,删除后它将重新生效。" : "";
|
|
27587
|
+
if (!window.confirm("删除规则 " + rule.absPath + "?(不可恢复)" + reveal)) return;
|
|
27938
27588
|
rpc("rules.remove", {
|
|
27939
27589
|
scope: rule.scope,
|
|
27590
|
+
origin: rule.origin,
|
|
27940
27591
|
workspacePath: rule.workspacePath,
|
|
27941
27592
|
file: rule.file
|
|
27942
27593
|
}).then((res) => {
|
|
@@ -27954,7 +27605,7 @@ window.__ModuleLoader__.load({
|
|
|
27954
27605
|
const enabled = !rule.enabled;
|
|
27955
27606
|
const applyLocal = (on) => setData((prev) => prev ? {
|
|
27956
27607
|
...prev,
|
|
27957
|
-
user: prev.user.map((r) =>
|
|
27608
|
+
user: prev.user.map((r) => rule.scope === "user" && r.file === rule.file && (r.origin ?? "current") === (rule.origin ?? "current") ? {
|
|
27958
27609
|
...r,
|
|
27959
27610
|
enabled: on
|
|
27960
27611
|
} : r),
|
|
@@ -27969,6 +27620,7 @@ window.__ModuleLoader__.load({
|
|
|
27969
27620
|
applyLocal(enabled);
|
|
27970
27621
|
rpc("rules.toggle", {
|
|
27971
27622
|
scope: rule.scope,
|
|
27623
|
+
origin: rule.origin,
|
|
27972
27624
|
workspacePath: rule.workspacePath,
|
|
27973
27625
|
file: rule.file,
|
|
27974
27626
|
enabled
|
|
@@ -28017,7 +27669,7 @@ window.__ModuleLoader__.load({
|
|
|
28017
27669
|
if (!activeProject()) return "当前工作区未注册为 DSH workspace,暂不能管理其项目规则";
|
|
28018
27670
|
return "<工作区>/.dsh/rules/ 还没有规则,点击「+ 新建规则」创建";
|
|
28019
27671
|
}
|
|
28020
|
-
return "
|
|
27672
|
+
return "还没有用户规则(当前 DSH_HOME/rules/ 或旧目录 ~/.dsh/rules/),点击「+ 新建规则」创建";
|
|
28021
27673
|
};
|
|
28022
27674
|
const renderBody = () => {
|
|
28023
27675
|
if (form) return react.default.createElement(RuleEditor, {
|
|
@@ -28050,7 +27702,7 @@ window.__ModuleLoader__.load({
|
|
|
28050
27702
|
})
|
|
28051
27703
|
};
|
|
28052
27704
|
return react.default.createElement("div", { className: "edrv-rules-list" }, rows.map((rule) => react.default.createElement(RuleRow, {
|
|
28053
|
-
key: rule.scope + ":" + rule.file,
|
|
27705
|
+
key: rule.scope + ":" + (rule.origin ?? "project") + ":" + rule.file,
|
|
28054
27706
|
rule,
|
|
28055
27707
|
...common
|
|
28056
27708
|
})));
|
|
@@ -28058,7 +27710,7 @@ window.__ModuleLoader__.load({
|
|
|
28058
27710
|
return react.default.createElement("div", {
|
|
28059
27711
|
className: "edrv-rules-panel",
|
|
28060
27712
|
ref: panelRef
|
|
28061
|
-
}, renderTabs(), renderWsLine(), react.default.createElement("div", { className: "edrv-rules-hint" }, tab === "user" ? "
|
|
27713
|
+
}, renderTabs(), renderWsLine(), react.default.createElement("div", { className: "edrv-rules-hint" }, tab === "user" ? "读取 DSH_HOME/rules/ 与 ~/.dsh/rules/;同名时当前目录优先,新建保存到当前目录" : "项目规则仅对所在工作区的会话生效(存放于 <工作区>/.dsh/rules/)"), renderBody());
|
|
28062
27714
|
}
|
|
28063
27715
|
//#endregion
|
|
28064
27716
|
//#region src/client/ui/SvnAiPlanDialog.ts
|
|
@@ -31656,26 +31308,20 @@ window.__ModuleLoader__.load({
|
|
|
31656
31308
|
//#region src/client/commandBridge.ts
|
|
31657
31309
|
/**
|
|
31658
31310
|
* dsh-vscode-mode client — 指令执行桥(装配层)。
|
|
31659
|
-
*
|
|
31660
|
-
*
|
|
31311
|
+
* 职责两件:① 把内置指令目录注册进注册表(命令栏候选源);② 注入命令栏执行器。
|
|
31312
|
+
* 键位派发全部归官方 shortcuts 机制(shortcutsOfficial.registerOfficialShortcuts 注册官方命令,
|
|
31313
|
+
* resolve 走注册表 run)——本桥不再挂窗口键位监听,避免与官方派发双执行。
|
|
31661
31314
|
* 编辑器动作本身由 EditorView / QuickOpen 监听 `edrv.command.<action>` 窗口事件完成——桥不持有
|
|
31662
31315
|
* React 状态,因此指令系统可在编辑器挂载前装配、卸载后仍安全。
|
|
31663
|
-
* 作者 ddj 2026年09月10号
|
|
31316
|
+
* 作者 ddj 2026年09月10号 / 2026年10月
|
|
31664
31317
|
*/
|
|
31665
|
-
const bridgeLog = log.child("commands");
|
|
31666
|
-
/** 解析事件目标(缺省 window;无 window 返回 null)。 */
|
|
31667
|
-
function resolveTarget(options) {
|
|
31668
|
-
if (options.target) return options.target;
|
|
31669
|
-
if (typeof window === "undefined") return null;
|
|
31670
|
-
return window;
|
|
31671
|
-
}
|
|
31318
|
+
const bridgeLog$1 = log.child("commands");
|
|
31672
31319
|
/**
|
|
31673
|
-
* 创建指令执行桥:注册内置指令 +
|
|
31320
|
+
* 创建指令执行桥:注册内置指令 + 命令栏自身,注入命令栏执行器。
|
|
31674
31321
|
* @author ddj 2026年09月10号
|
|
31675
|
-
* @param options 装配选项(事件目标注入)
|
|
31676
31322
|
* @returns 指令执行桥句柄
|
|
31677
31323
|
*/
|
|
31678
|
-
function createCommandBridge(
|
|
31324
|
+
function createCommandBridge() {
|
|
31679
31325
|
const registry = createCommandRegistry();
|
|
31680
31326
|
const disposers = [];
|
|
31681
31327
|
const palette = showCommandsDef(() => openCommandPalette("command"));
|
|
@@ -31683,21 +31329,7 @@ window.__ModuleLoader__.load({
|
|
|
31683
31329
|
disposers.push(registry.register(palette));
|
|
31684
31330
|
setPaletteRunner((id) => registry.run(id));
|
|
31685
31331
|
setRegistryRef(registry);
|
|
31686
|
-
const dispatched = dispatchedCommands(palette);
|
|
31687
|
-
const onKey = (event) => {
|
|
31688
|
-
for (const command of dispatched) {
|
|
31689
|
-
if (!matchEvent(event, bindingsOf(command.id))) continue;
|
|
31690
|
-
if (!registry.isAvailable(command.id)) continue;
|
|
31691
|
-
event.preventDefault();
|
|
31692
|
-
event.stopPropagation();
|
|
31693
|
-
registry.run(command.id);
|
|
31694
|
-
return;
|
|
31695
|
-
}
|
|
31696
|
-
};
|
|
31697
|
-
const target = resolveTarget(options);
|
|
31698
|
-
if (target) target.addEventListener("keydown", onKey, true);
|
|
31699
31332
|
const dispose = () => {
|
|
31700
|
-
if (target) target.removeEventListener("keydown", onKey, true);
|
|
31701
31333
|
while (disposers.length) {
|
|
31702
31334
|
const release = disposers.pop();
|
|
31703
31335
|
if (release) release();
|
|
@@ -31705,13 +31337,932 @@ window.__ModuleLoader__.load({
|
|
|
31705
31337
|
setPaletteRunner(null);
|
|
31706
31338
|
setRegistryRef(null);
|
|
31707
31339
|
};
|
|
31708
|
-
bridgeLog.info("指令注册表已装配(" + registry.list().length + "
|
|
31340
|
+
bridgeLog$1.info("指令注册表已装配(" + registry.list().length + " 条指令;键位派发已归官方 shortcuts 机制)");
|
|
31709
31341
|
return {
|
|
31710
31342
|
registry,
|
|
31711
31343
|
dispose
|
|
31712
31344
|
};
|
|
31713
31345
|
}
|
|
31714
31346
|
//#endregion
|
|
31347
|
+
//#region src/shared/keybindings.ts
|
|
31348
|
+
/**
|
|
31349
|
+
* dsh-vscode-mode shared — 快捷键命令目录与默认键位(双面契约)。
|
|
31350
|
+
* 纯数据模块:host(settings schema 默认值)与 client(执行匹配/设置页)共用。
|
|
31351
|
+
* 键位格式:修饰符 + 主键,`+` 连接,如 `Ctrl+Shift+F`;空串 = 未绑定;
|
|
31352
|
+
* `|` 连接多个候选(任一命中即触发),如 `Alt+ArrowLeft|Ctrl+Alt+-`。
|
|
31353
|
+
* 约束:本模块被 host 与 client 两半共用,**禁止 import client 侧模块**(React/浏览器 API)。
|
|
31354
|
+
* 与 client/ui/commandCatalog 的一致性由 tests/commands.test.ts 断言兜底。
|
|
31355
|
+
* 官方机制(2026-10):SHORTCUT_PROFILES 描述各命令在官方 ctx.shortcuts 的 profile 默认键位
|
|
31356
|
+
* (由 client 注册官方服务时使用);chordToBinding/bindingToChord 负责插件弦与官方绑定互转。
|
|
31357
|
+
* 作者 ddj 2026年08月26号 / 2026年09月10号
|
|
31358
|
+
*/
|
|
31359
|
+
/** 命令 id → 默认键位(可为多候选)。命令目录以此为准,新增命令只需加一项。 */
|
|
31360
|
+
const KEYBINDING_DEFAULTS = {
|
|
31361
|
+
"edrv.save": "Ctrl+S",
|
|
31362
|
+
"edrv.quickOpen": "Ctrl+P",
|
|
31363
|
+
"edrv.toggleSidebar": "Ctrl+B",
|
|
31364
|
+
"edrv.searchInFiles": "Ctrl+Shift+F",
|
|
31365
|
+
"edrv.toggleMarkdownPreview": "Ctrl+Shift+V",
|
|
31366
|
+
"edrv.navigateBack": "Alt+ArrowLeft|Ctrl+Alt+-",
|
|
31367
|
+
"edrv.navigateForward": "Alt+ArrowRight|Ctrl+Shift+-",
|
|
31368
|
+
"edrv.nextTab": "Ctrl+Alt+ArrowRight|Ctrl+PageDown",
|
|
31369
|
+
"edrv.prevTab": "Ctrl+Alt+ArrowLeft|Ctrl+PageUp",
|
|
31370
|
+
"edrv.goToLine": "Ctrl+G",
|
|
31371
|
+
"edrv.showCommands": "Ctrl+Shift+P|F1",
|
|
31372
|
+
"edrv.nextEditorRow": "Ctrl+Alt+ArrowDown",
|
|
31373
|
+
"edrv.prevEditorRow": "Ctrl+Alt+ArrowUp",
|
|
31374
|
+
"edrv.addSelectionRef": "Ctrl+U",
|
|
31375
|
+
"edrv.closeTab": "Ctrl+F4",
|
|
31376
|
+
"edrv.debugToggleBreakpoint": "F9",
|
|
31377
|
+
"edrv.debugStartContinue": "F5",
|
|
31378
|
+
"edrv.debugStepOver": "F10",
|
|
31379
|
+
"edrv.debugStepInto": "F11",
|
|
31380
|
+
"edrv.debugStepOut": "Shift+F11",
|
|
31381
|
+
"edrv.debugStop": "Shift+F5"
|
|
31382
|
+
};
|
|
31383
|
+
/**
|
|
31384
|
+
* 返回默认键位的独立副本(调用方修改不污染目录)。
|
|
31385
|
+
* @author ddj 2026年08月26号
|
|
31386
|
+
* @returns 默认键位映射
|
|
31387
|
+
*/
|
|
31388
|
+
function defaultKeybindings() {
|
|
31389
|
+
return { ...KEYBINDING_DEFAULTS };
|
|
31390
|
+
}
|
|
31391
|
+
/**
|
|
31392
|
+
* 规整外部键位数据:只保留已知命令 id 且为字符串的值,未知 id 丢弃。
|
|
31393
|
+
* 防止用户文档/旧版本写入的未知命令污染执行匹配。
|
|
31394
|
+
* @author ddj 2026年08月26号
|
|
31395
|
+
* @param raw 原始设置值
|
|
31396
|
+
* @returns 规整后的键位映射(缺省项不补默认值,由调用方按需合并)
|
|
31397
|
+
*/
|
|
31398
|
+
function normalizeKeybindings(raw) {
|
|
31399
|
+
const out = {};
|
|
31400
|
+
if (!raw || typeof raw !== "object") return out;
|
|
31401
|
+
for (const [id, chord] of Object.entries(raw)) if (id in KEYBINDING_DEFAULTS && typeof chord === "string") out[id] = chord;
|
|
31402
|
+
return out;
|
|
31403
|
+
}
|
|
31404
|
+
/** 显示主键 → 官方物理 code 对照(官方 code 白名单:Key[A-Z]|Digit[0-9]|F1-24|具名键)。 */
|
|
31405
|
+
const DISPLAY_KEY_TO_CODE = {
|
|
31406
|
+
"-": "Minus",
|
|
31407
|
+
"=": "Equal",
|
|
31408
|
+
"[": "BracketLeft",
|
|
31409
|
+
"]": "BracketRight",
|
|
31410
|
+
";": "Semicolon",
|
|
31411
|
+
"'": "Quote",
|
|
31412
|
+
",": "Comma",
|
|
31413
|
+
".": "Period",
|
|
31414
|
+
"/": "Slash",
|
|
31415
|
+
"\\": "Backslash",
|
|
31416
|
+
"`": "Backquote",
|
|
31417
|
+
Enter: "Enter",
|
|
31418
|
+
Tab: "Tab",
|
|
31419
|
+
Backspace: "Backspace",
|
|
31420
|
+
Delete: "Delete",
|
|
31421
|
+
Escape: "Escape",
|
|
31422
|
+
Space: "Space",
|
|
31423
|
+
ArrowUp: "ArrowUp",
|
|
31424
|
+
ArrowDown: "ArrowDown",
|
|
31425
|
+
ArrowLeft: "ArrowLeft",
|
|
31426
|
+
ArrowRight: "ArrowRight"
|
|
31427
|
+
};
|
|
31428
|
+
/** 官方具名 code(非 Key/Digit/F 前缀且可回显的部分)。 */
|
|
31429
|
+
const NAMED_CODES = [
|
|
31430
|
+
"Enter",
|
|
31431
|
+
"Tab",
|
|
31432
|
+
"Backspace",
|
|
31433
|
+
"Delete",
|
|
31434
|
+
"Escape",
|
|
31435
|
+
"Space",
|
|
31436
|
+
"ArrowUp",
|
|
31437
|
+
"ArrowDown",
|
|
31438
|
+
"ArrowLeft",
|
|
31439
|
+
"ArrowRight"
|
|
31440
|
+
];
|
|
31441
|
+
/** 官方 code → 显示主键(符号键反向对照)。 */
|
|
31442
|
+
const CODE_TO_DISPLAY_KEY = {
|
|
31443
|
+
Minus: "-",
|
|
31444
|
+
Equal: "=",
|
|
31445
|
+
BracketLeft: "[",
|
|
31446
|
+
BracketRight: "]",
|
|
31447
|
+
Semicolon: ";",
|
|
31448
|
+
Quote: "'",
|
|
31449
|
+
Comma: ",",
|
|
31450
|
+
Period: ".",
|
|
31451
|
+
Slash: "/",
|
|
31452
|
+
Backslash: "\\",
|
|
31453
|
+
Backquote: "`"
|
|
31454
|
+
};
|
|
31455
|
+
/** 显示主键 → 官方 code(PageUp/PageDown/Home/End/Insert 等官方不支持 → null)。 */
|
|
31456
|
+
function keyToCode(key) {
|
|
31457
|
+
if (Object.hasOwn(DISPLAY_KEY_TO_CODE, key)) return DISPLAY_KEY_TO_CODE[key];
|
|
31458
|
+
if (key.length === 1 && key >= "a" && key <= "z") return "Key" + key.toUpperCase();
|
|
31459
|
+
if (key.length === 1 && key >= "A" && key <= "Z") return "Key" + key;
|
|
31460
|
+
if (key.length === 1 && key >= "0" && key <= "9") return "Digit" + key;
|
|
31461
|
+
if (/^F([1-9]|1[0-9]|2[0-4])$/u.test(key)) return key;
|
|
31462
|
+
return null;
|
|
31463
|
+
}
|
|
31464
|
+
/** 官方 code → 显示主键(不可回显 → null)。 */
|
|
31465
|
+
function codeToKey(code) {
|
|
31466
|
+
if (Object.hasOwn(CODE_TO_DISPLAY_KEY, code)) return CODE_TO_DISPLAY_KEY[code];
|
|
31467
|
+
if (/^Key[A-Z]$/u.test(code)) return code.slice(3);
|
|
31468
|
+
if (/^Digit[0-9]$/u.test(code)) return code.slice(5);
|
|
31469
|
+
if (/^F([1-9]|1[0-9]|2[0-4])$/u.test(code)) return code;
|
|
31470
|
+
if (NAMED_CODES.includes(code)) return code;
|
|
31471
|
+
return null;
|
|
31472
|
+
}
|
|
31473
|
+
/** 插件弦的修饰符 token → 官方修饰符(Ctrl 与 Cmd 同为 primary,与 matchEvent 的互认语义一致)。 */
|
|
31474
|
+
const CHORD_MODIFIER_TOKENS = {
|
|
31475
|
+
ctrl: "primary",
|
|
31476
|
+
control: "primary",
|
|
31477
|
+
cmd: "primary",
|
|
31478
|
+
meta: "primary",
|
|
31479
|
+
shift: "shift",
|
|
31480
|
+
alt: "alt"
|
|
31481
|
+
};
|
|
31482
|
+
/**
|
|
31483
|
+
* 插件键位弦(单候选)→ 官方绑定。
|
|
31484
|
+
* 不可表示的主键(PageUp/PageDown/Home/End/Insert 等)返回 null,调用方跳过。
|
|
31485
|
+
* @author ddj 2026年10月
|
|
31486
|
+
* @param chord 键位弦(如 `Ctrl+Alt+-` / `F9` / `Alt+ArrowLeft`)
|
|
31487
|
+
* @returns 官方绑定或 null
|
|
31488
|
+
*/
|
|
31489
|
+
function chordToBinding(chord) {
|
|
31490
|
+
const parts = String(chord ?? "").split("+").map((part) => part.trim()).filter(Boolean);
|
|
31491
|
+
if (!parts.length) return null;
|
|
31492
|
+
const modifiers = /* @__PURE__ */ new Set();
|
|
31493
|
+
let key = "";
|
|
31494
|
+
for (const part of parts) {
|
|
31495
|
+
const modifier = CHORD_MODIFIER_TOKENS[part.toLowerCase()];
|
|
31496
|
+
if (modifier) {
|
|
31497
|
+
modifiers.add(modifier);
|
|
31498
|
+
continue;
|
|
31499
|
+
}
|
|
31500
|
+
if (key) return null;
|
|
31501
|
+
key = part;
|
|
31502
|
+
}
|
|
31503
|
+
if (!key) return null;
|
|
31504
|
+
if (key === " ") key = "Space";
|
|
31505
|
+
else if (key.length === 1 && key >= "a" && key <= "z") key = key.toUpperCase();
|
|
31506
|
+
const code = keyToCode(key);
|
|
31507
|
+
return code === null ? null : {
|
|
31508
|
+
code,
|
|
31509
|
+
modifiers: [...modifiers]
|
|
31510
|
+
};
|
|
31511
|
+
}
|
|
31512
|
+
/**
|
|
31513
|
+
* 官方绑定 → 插件显示键位弦(primary 显示为 Ctrl,与历史 tooltip 文案保持一致)。
|
|
31514
|
+
* @author ddj 2026年10月
|
|
31515
|
+
* @param binding 官方绑定
|
|
31516
|
+
* @returns 插件弦(不可表示 → null)
|
|
31517
|
+
*/
|
|
31518
|
+
function bindingToChord(binding) {
|
|
31519
|
+
if (!binding || typeof binding !== "object") return null;
|
|
31520
|
+
const key = codeToKey(binding.code);
|
|
31521
|
+
if (key === null) return null;
|
|
31522
|
+
let suffix = "";
|
|
31523
|
+
if (binding.secondCode !== void 0) {
|
|
31524
|
+
const secondKey = codeToKey(binding.secondCode);
|
|
31525
|
+
if (secondKey === null) return null;
|
|
31526
|
+
suffix = "+" + secondKey;
|
|
31527
|
+
}
|
|
31528
|
+
const parts = [];
|
|
31529
|
+
if (binding.modifiers.includes("primary")) parts.push("Ctrl");
|
|
31530
|
+
if (binding.modifiers.includes("shift")) parts.push("Shift");
|
|
31531
|
+
if (binding.modifiers.includes("alt")) parts.push("Alt");
|
|
31532
|
+
return [...parts, key + suffix].join("+");
|
|
31533
|
+
}
|
|
31534
|
+
/** 官方绑定字面量构造(表格可读性辅助)。 */
|
|
31535
|
+
function bind(code, modifiers) {
|
|
31536
|
+
return {
|
|
31537
|
+
code,
|
|
31538
|
+
modifiers
|
|
31539
|
+
};
|
|
31540
|
+
}
|
|
31541
|
+
/**
|
|
31542
|
+
* 命令 id → 官方 profile 默认键位(与 KEYBINDING_DEFAULTS 一一对应;client 注册官方服务的数据源)。
|
|
31543
|
+
* 取值约束(对齐官方 bindingIssue/isWebBindingAllowed 校验,错报会在注册时抛错):
|
|
31544
|
+
* - desktop:macos/windows:官方不做保留校验,用 VS Code 同款 chord;
|
|
31545
|
+
* - desktop:linux / web:linux:Arrow*、裸 F 键、全 Shift、Ctrl+V/C/X/Z/Y/Q/H/A、Meta 均为保留键 → 不声明;
|
|
31546
|
+
* - web:macos/windows:单修饰符组合不在白名单 → 用 primary+alt / primary+shift 变体(官方 ui-layout 同款);
|
|
31547
|
+
* - Ctrl+PageDown/PageUp 候选废弃(官方 code 白名单无 PageUp/PageDown);
|
|
31548
|
+
* - edrv.toggleSidebar 留空:Ctrl+B 与官方 sidebar.left.toggle(Mod+B)冲突,注册会抛错且冲突键互相阻断。
|
|
31549
|
+
* @author ddj 2026年10月
|
|
31550
|
+
*/
|
|
31551
|
+
const SHORTCUT_PROFILES = {
|
|
31552
|
+
"edrv.toggleSidebar": {},
|
|
31553
|
+
"edrv.save": {
|
|
31554
|
+
"desktop:macos": bind("KeyS", ["primary"]),
|
|
31555
|
+
"desktop:windows": bind("KeyS", ["primary"]),
|
|
31556
|
+
"desktop:linux": bind("KeyS", ["primary"])
|
|
31557
|
+
},
|
|
31558
|
+
"edrv.quickOpen": {
|
|
31559
|
+
"desktop:macos": bind("KeyP", ["primary", "alt"]),
|
|
31560
|
+
"desktop:windows": bind("KeyP", ["primary", "alt"]),
|
|
31561
|
+
"desktop:linux": bind("KeyP", ["primary", "alt"]),
|
|
31562
|
+
"web:macos": bind("KeyP", [
|
|
31563
|
+
"primary",
|
|
31564
|
+
"alt",
|
|
31565
|
+
"shift"
|
|
31566
|
+
]),
|
|
31567
|
+
"web:windows": bind("KeyP", [
|
|
31568
|
+
"primary",
|
|
31569
|
+
"alt",
|
|
31570
|
+
"shift"
|
|
31571
|
+
])
|
|
31572
|
+
},
|
|
31573
|
+
"edrv.searchInFiles": {
|
|
31574
|
+
"desktop:macos": bind("KeyF", ["primary", "shift"]),
|
|
31575
|
+
"desktop:windows": bind("KeyF", ["primary", "shift"]),
|
|
31576
|
+
"desktop:linux": bind("KeyF", ["primary", "shift"]),
|
|
31577
|
+
"web:macos": bind("KeyF", ["primary", "shift"]),
|
|
31578
|
+
"web:windows": bind("KeyF", ["primary", "shift"])
|
|
31579
|
+
},
|
|
31580
|
+
"edrv.toggleMarkdownPreview": {
|
|
31581
|
+
"desktop:macos": bind("KeyV", ["primary", "shift"]),
|
|
31582
|
+
"desktop:windows": bind("KeyV", ["primary", "shift"]),
|
|
31583
|
+
"web:macos": bind("KeyV", ["primary", "shift"]),
|
|
31584
|
+
"web:windows": bind("KeyV", ["primary", "shift"])
|
|
31585
|
+
},
|
|
31586
|
+
"edrv.navigateBack": {
|
|
31587
|
+
"desktop:macos": bind("ArrowLeft", ["alt"]),
|
|
31588
|
+
"desktop:windows": bind("ArrowLeft", ["alt"]),
|
|
31589
|
+
"web:macos": bind("Minus", ["primary", "alt"]),
|
|
31590
|
+
"web:windows": bind("Minus", ["primary", "alt"])
|
|
31591
|
+
},
|
|
31592
|
+
"edrv.navigateForward": {
|
|
31593
|
+
"desktop:macos": bind("ArrowRight", ["alt"]),
|
|
31594
|
+
"desktop:windows": bind("ArrowRight", ["alt"]),
|
|
31595
|
+
"web:macos": bind("Minus", ["primary", "shift"]),
|
|
31596
|
+
"web:windows": bind("Minus", ["primary", "shift"])
|
|
31597
|
+
},
|
|
31598
|
+
"edrv.nextTab": {
|
|
31599
|
+
"desktop:macos": bind("ArrowRight", ["primary", "alt"]),
|
|
31600
|
+
"desktop:windows": bind("ArrowRight", ["primary", "alt"]),
|
|
31601
|
+
"web:macos": bind("ArrowRight", ["primary", "alt"]),
|
|
31602
|
+
"web:windows": bind("ArrowRight", ["primary", "alt"])
|
|
31603
|
+
},
|
|
31604
|
+
"edrv.prevTab": {
|
|
31605
|
+
"desktop:macos": bind("ArrowLeft", ["primary", "alt"]),
|
|
31606
|
+
"desktop:windows": bind("ArrowLeft", ["primary", "alt"]),
|
|
31607
|
+
"web:macos": bind("ArrowLeft", ["primary", "alt"]),
|
|
31608
|
+
"web:windows": bind("ArrowLeft", ["primary", "alt"])
|
|
31609
|
+
},
|
|
31610
|
+
"edrv.goToLine": {
|
|
31611
|
+
"desktop:macos": bind("KeyG", ["primary"]),
|
|
31612
|
+
"desktop:windows": bind("KeyG", ["primary"]),
|
|
31613
|
+
"desktop:linux": bind("KeyG", ["primary"])
|
|
31614
|
+
},
|
|
31615
|
+
"edrv.showCommands": {
|
|
31616
|
+
"desktop:macos": bind("KeyP", ["primary", "shift"]),
|
|
31617
|
+
"desktop:windows": bind("KeyP", ["primary", "shift"]),
|
|
31618
|
+
"desktop:linux": bind("KeyP", ["primary", "shift"]),
|
|
31619
|
+
"web:macos": bind("KeyP", ["primary", "shift"]),
|
|
31620
|
+
"web:windows": bind("KeyP", ["primary", "shift"])
|
|
31621
|
+
},
|
|
31622
|
+
"edrv.nextEditorRow": {
|
|
31623
|
+
"desktop:macos": bind("ArrowDown", ["primary", "alt"]),
|
|
31624
|
+
"desktop:windows": bind("ArrowDown", ["primary", "alt"]),
|
|
31625
|
+
"web:macos": bind("ArrowDown", ["primary", "alt"]),
|
|
31626
|
+
"web:windows": bind("ArrowDown", ["primary", "alt"])
|
|
31627
|
+
},
|
|
31628
|
+
"edrv.prevEditorRow": {
|
|
31629
|
+
"desktop:macos": bind("ArrowUp", ["primary", "alt"]),
|
|
31630
|
+
"desktop:windows": bind("ArrowUp", ["primary", "alt"]),
|
|
31631
|
+
"web:macos": bind("ArrowUp", ["primary", "alt"]),
|
|
31632
|
+
"web:windows": bind("ArrowUp", ["primary", "alt"])
|
|
31633
|
+
},
|
|
31634
|
+
"edrv.addSelectionRef": {
|
|
31635
|
+
"desktop:macos": bind("KeyU", ["primary"]),
|
|
31636
|
+
"desktop:windows": bind("KeyU", ["primary"]),
|
|
31637
|
+
"desktop:linux": bind("KeyU", ["primary"])
|
|
31638
|
+
},
|
|
31639
|
+
"edrv.closeTab": {
|
|
31640
|
+
"desktop:macos": bind("F4", ["primary"]),
|
|
31641
|
+
"desktop:windows": bind("F4", ["primary"]),
|
|
31642
|
+
"desktop:linux": bind("F4", ["primary"])
|
|
31643
|
+
},
|
|
31644
|
+
"edrv.debugToggleBreakpoint": {
|
|
31645
|
+
"desktop:macos": bind("F9", []),
|
|
31646
|
+
"desktop:windows": bind("F9", [])
|
|
31647
|
+
},
|
|
31648
|
+
"edrv.debugStartContinue": {
|
|
31649
|
+
"desktop:macos": bind("F5", []),
|
|
31650
|
+
"desktop:windows": bind("F5", [])
|
|
31651
|
+
},
|
|
31652
|
+
"edrv.debugStepOver": {
|
|
31653
|
+
"desktop:macos": bind("F10", []),
|
|
31654
|
+
"desktop:windows": bind("F10", [])
|
|
31655
|
+
},
|
|
31656
|
+
"edrv.debugStepInto": {
|
|
31657
|
+
"desktop:macos": bind("F11", []),
|
|
31658
|
+
"desktop:windows": bind("F11", [])
|
|
31659
|
+
},
|
|
31660
|
+
"edrv.debugStepOut": {
|
|
31661
|
+
"desktop:macos": bind("F11", ["shift"]),
|
|
31662
|
+
"desktop:windows": bind("F11", ["shift"])
|
|
31663
|
+
},
|
|
31664
|
+
"edrv.debugStop": {
|
|
31665
|
+
"desktop:macos": bind("F5", ["shift"]),
|
|
31666
|
+
"desktop:windows": bind("F5", ["shift"])
|
|
31667
|
+
}
|
|
31668
|
+
};
|
|
31669
|
+
//#endregion
|
|
31670
|
+
//#region src/client/shortcutsOfficial.ts
|
|
31671
|
+
/**
|
|
31672
|
+
* dsh-vscode-mode client — 官方 shortcuts 服务桥接层。
|
|
31673
|
+
* 职责四件:① 运行时探测 ctx.shortcuts(可选服务,有界重试,不进 inject——旧版 DSH 无此服务
|
|
31674
|
+
* 时不阻断装配);② safeRegister 逐命令注册(官方 register 对重复 id/保留默认/组合重叠抛错,
|
|
31675
|
+
* 冲突时去掉默认键位降级注册,绝不阻断插件加载);③ 官方命令定义构建(commandCatalog → 官方
|
|
31676
|
+
* command,resolve 统一走插件注册表 run,可用性为假返回 pass 不吞键);④ 旧 keybindings
|
|
31677
|
+
* 设置一次性迁移进官方存储。
|
|
31678
|
+
* 作者 ddj 2026年10月
|
|
31679
|
+
*/
|
|
31680
|
+
const bridgeLog = log.child("shortcuts-official");
|
|
31681
|
+
/** 迁移比对用的默认键位表副本(模块态,避免每次调用重建)。 */
|
|
31682
|
+
const KEYBINDING_DEFAULTS_OF = defaultKeybindings();
|
|
31683
|
+
/** 结构探测:get('shortcuts') 命中且具备核心能力才算服务就绪(设置组件运行时自愈探测复用)。 */
|
|
31684
|
+
function detectShortcuts(ctx) {
|
|
31685
|
+
try {
|
|
31686
|
+
const service = ctx.get("shortcuts");
|
|
31687
|
+
if (service && typeof service.register === "function" && typeof service.edit === "function" && service.catalog && service.config) return service;
|
|
31688
|
+
} catch {}
|
|
31689
|
+
return null;
|
|
31690
|
+
}
|
|
31691
|
+
/**
|
|
31692
|
+
* 探测官方 shortcuts 服务:立即探测一次,未命中按节奏有界重试;命中或用尽均回调一次。
|
|
31693
|
+
* @author ddj 2026年10月
|
|
31694
|
+
* @param ctx 客户端上下文
|
|
31695
|
+
* @param options schedule/重试节奏 + onReady 回调
|
|
31696
|
+
* @returns 取消探测的注销函数
|
|
31697
|
+
*/
|
|
31698
|
+
function awaitShortcutsService(ctx, options) {
|
|
31699
|
+
const maxAttempts = options.attempts ?? 15;
|
|
31700
|
+
const interval = options.intervalMs ?? 2e3;
|
|
31701
|
+
let attempts = 0;
|
|
31702
|
+
let settled = false;
|
|
31703
|
+
const tick = () => {
|
|
31704
|
+
if (settled) return;
|
|
31705
|
+
const service = detectShortcuts(ctx);
|
|
31706
|
+
if (service !== null) {
|
|
31707
|
+
settled = true;
|
|
31708
|
+
options.onReady(service);
|
|
31709
|
+
return;
|
|
31710
|
+
}
|
|
31711
|
+
if (attempts >= maxAttempts) {
|
|
31712
|
+
settled = true;
|
|
31713
|
+
options.onReady(null);
|
|
31714
|
+
return;
|
|
31715
|
+
}
|
|
31716
|
+
attempts += 1;
|
|
31717
|
+
options.schedule(tick, interval);
|
|
31718
|
+
};
|
|
31719
|
+
tick();
|
|
31720
|
+
return () => {
|
|
31721
|
+
settled = true;
|
|
31722
|
+
};
|
|
31723
|
+
}
|
|
31724
|
+
/**
|
|
31725
|
+
* 逐命令安全注册:官方 register 抛错(重复 id/保留默认/组合重叠)时去掉默认键位重试一次,
|
|
31726
|
+
* 保证命令仍进官方目录(可被用户手动绑定),注册路径绝不抛错、绝不阻断装配。
|
|
31727
|
+
* @author ddj 2026年10月
|
|
31728
|
+
* @param service 官方服务
|
|
31729
|
+
* @param command 官方命令定义
|
|
31730
|
+
* @returns 注销函数(注册失败返回空操作)
|
|
31731
|
+
*/
|
|
31732
|
+
function safeRegister(service, command) {
|
|
31733
|
+
try {
|
|
31734
|
+
return service.register(command);
|
|
31735
|
+
} catch (error) {
|
|
31736
|
+
if (!command.defaults || Object.keys(command.defaults).length === 0) {
|
|
31737
|
+
bridgeLog.warn("官方快捷键注册失败(" + command.id + "):" + String(error));
|
|
31738
|
+
return () => {};
|
|
31739
|
+
}
|
|
31740
|
+
bridgeLog.warn("官方快捷键默认键位被拒(" + command.id + "),降级为未绑定:" + String(error));
|
|
31741
|
+
try {
|
|
31742
|
+
return service.register({
|
|
31743
|
+
...command,
|
|
31744
|
+
defaults: {}
|
|
31745
|
+
});
|
|
31746
|
+
} catch (retryError) {
|
|
31747
|
+
bridgeLog.warn("官方快捷键注册失败(" + command.id + "):" + String(retryError));
|
|
31748
|
+
return () => {};
|
|
31749
|
+
}
|
|
31750
|
+
}
|
|
31751
|
+
}
|
|
31752
|
+
/**
|
|
31753
|
+
* 官方服务未就绪时复用旧版窗口键位语义;服务出现后注销,避免双执行。
|
|
31754
|
+
* @author ddj 2026年09月28号
|
|
31755
|
+
* @param defs 按目录序排列的插件命令
|
|
31756
|
+
* @param runner 指令注册表
|
|
31757
|
+
* @param chords 当前旧设置键位获取函数
|
|
31758
|
+
* @param target 键盘事件目标(缺省 window,测试可注入)
|
|
31759
|
+
* @returns 注销监听的函数
|
|
31760
|
+
*/
|
|
31761
|
+
function installLegacyKeys(defs, runner, chords, target = typeof window === "undefined" ? null : window) {
|
|
31762
|
+
if (!target) return () => {};
|
|
31763
|
+
const cache = /* @__PURE__ */ new Map();
|
|
31764
|
+
const onKey = (event) => {
|
|
31765
|
+
const key = event;
|
|
31766
|
+
const current = chords();
|
|
31767
|
+
for (const def of defs) {
|
|
31768
|
+
const chord = current[def.id];
|
|
31769
|
+
if (!chord) continue;
|
|
31770
|
+
let bindings = cache.get(chord);
|
|
31771
|
+
if (!bindings) {
|
|
31772
|
+
bindings = parseChords(chord);
|
|
31773
|
+
cache.set(chord, bindings);
|
|
31774
|
+
}
|
|
31775
|
+
if (!matchEvent(key, bindings) || !runner.isAvailable(def.id)) continue;
|
|
31776
|
+
key.preventDefault();
|
|
31777
|
+
key.stopPropagation();
|
|
31778
|
+
runner.run(def.id);
|
|
31779
|
+
return;
|
|
31780
|
+
}
|
|
31781
|
+
};
|
|
31782
|
+
target.addEventListener("keydown", onKey, true);
|
|
31783
|
+
return () => target.removeEventListener("keydown", onKey, true);
|
|
31784
|
+
}
|
|
31785
|
+
/**
|
|
31786
|
+
* 插件命令定义 → 官方命令定义:默认键位取 SHORTCUT_PROFILES(缺省 = 不绑定);
|
|
31787
|
+
* resolve 可用性为假返回 pass(不吞键,与旧桥「不可用不执行也不吞键」语义一致),
|
|
31788
|
+
* 执行统一走注册表 run(自带可用性复核与异常捕获)。
|
|
31789
|
+
* @author ddj 2026年10月
|
|
31790
|
+
* @param def 插件命令定义
|
|
31791
|
+
* @param runner 插件指令注册表
|
|
31792
|
+
* @returns 官方命令定义
|
|
31793
|
+
*/
|
|
31794
|
+
function officialCommandOf(def, runner) {
|
|
31795
|
+
return {
|
|
31796
|
+
id: def.id,
|
|
31797
|
+
label: () => def.label,
|
|
31798
|
+
aliases: [],
|
|
31799
|
+
defaults: void 0,
|
|
31800
|
+
regions: ["page", "editable"],
|
|
31801
|
+
modals: [],
|
|
31802
|
+
resolve: () => {
|
|
31803
|
+
if (!runner.isAvailable(def.id)) return { status: "pass" };
|
|
31804
|
+
return {
|
|
31805
|
+
status: "handled",
|
|
31806
|
+
run: () => {
|
|
31807
|
+
runner.run(def.id);
|
|
31808
|
+
}
|
|
31809
|
+
};
|
|
31810
|
+
}
|
|
31811
|
+
};
|
|
31812
|
+
}
|
|
31813
|
+
/**
|
|
31814
|
+
* 注册全部命令进官方服务(逐命令 safeRegister 隔离,defaults 由调用方按 id 注入 SHORTCUT_PROFILES)。
|
|
31815
|
+
* @author ddj 2026年10月
|
|
31816
|
+
* @param service 官方服务
|
|
31817
|
+
* @param defs 插件命令定义(id/label)
|
|
31818
|
+
* @param runner 插件指令注册表
|
|
31819
|
+
* @param profiles id → 官方 profile 默认键位
|
|
31820
|
+
* @returns 聚合注销函数
|
|
31821
|
+
*/
|
|
31822
|
+
function registerOfficialShortcuts(service, defs, runner, profiles) {
|
|
31823
|
+
const disposers = [];
|
|
31824
|
+
for (const def of defs) {
|
|
31825
|
+
const command = officialCommandOf(def, runner);
|
|
31826
|
+
command.defaults = profiles[def.id] ?? {};
|
|
31827
|
+
disposers.push(safeRegister(service, command));
|
|
31828
|
+
}
|
|
31829
|
+
return () => {
|
|
31830
|
+
while (disposers.length) disposers.pop()?.();
|
|
31831
|
+
};
|
|
31832
|
+
}
|
|
31833
|
+
/**
|
|
31834
|
+
* 订阅官方目录 → 重建 edrv.* 弦表(chordOf 数据源);目录变化即通知。
|
|
31835
|
+
* 只收录 issue 为空的行(conflict 行保留——用户显式改键后 tooltip 仍显示其选择,
|
|
31836
|
+
* 冲突状态由设置页徽标呈现)。
|
|
31837
|
+
* @author ddj 2026年10月
|
|
31838
|
+
* @param service 官方服务
|
|
31839
|
+
* @param onChange 弦表回调(每次全量替换)
|
|
31840
|
+
* @returns 注销函数
|
|
31841
|
+
*/
|
|
31842
|
+
function bindCatalogChords(service, onChange) {
|
|
31843
|
+
const sync = () => {
|
|
31844
|
+
const chords = {};
|
|
31845
|
+
try {
|
|
31846
|
+
for (const row of service.catalog.getSnapshot()) {
|
|
31847
|
+
if (!row.id.startsWith("edrv.") || row.binding === null) continue;
|
|
31848
|
+
if (row.issue !== null) continue;
|
|
31849
|
+
const chord = bindingToChord(row.binding);
|
|
31850
|
+
if (chord !== null) chords[row.id] = chord;
|
|
31851
|
+
}
|
|
31852
|
+
} catch (error) {
|
|
31853
|
+
bridgeLog.warn("官方目录弦表重建失败:" + String(error));
|
|
31854
|
+
}
|
|
31855
|
+
onChange(chords);
|
|
31856
|
+
};
|
|
31857
|
+
sync();
|
|
31858
|
+
return service.catalog.subscribe(sync);
|
|
31859
|
+
}
|
|
31860
|
+
/**
|
|
31861
|
+
* 官方 edit 单次写入;stale 表示其他窗口已更新,不自动用新 revision 重放。
|
|
31862
|
+
* @author ddj 2026年09月28号
|
|
31863
|
+
* @param service 官方快捷键服务
|
|
31864
|
+
* @param edit 本次操作
|
|
31865
|
+
* @returns 是否保存成功
|
|
31866
|
+
*/
|
|
31867
|
+
async function editOnce(service, edit) {
|
|
31868
|
+
try {
|
|
31869
|
+
const result = await service.edit(edit, service.config.getSnapshot().revision);
|
|
31870
|
+
if (result.status === "saved") return true;
|
|
31871
|
+
bridgeLog.warn("官方快捷键写入未成功(" + edit.type + (edit.type === "set" ? " " + edit.id : "") + "):" + result.status);
|
|
31872
|
+
} catch (error) {
|
|
31873
|
+
bridgeLog.warn("官方快捷键写入异常(" + edit.type + "):" + String(error));
|
|
31874
|
+
}
|
|
31875
|
+
return false;
|
|
31876
|
+
}
|
|
31877
|
+
/**
|
|
31878
|
+
* 逐条恢复插件自定义键位;官方 reset-all 会清空所有产品命令的覆盖,不能使用。
|
|
31879
|
+
* @author ddj 2026年09月28号
|
|
31880
|
+
* @param service 官方快捷键服务
|
|
31881
|
+
* @returns 完成数量及首个失败命令(有失败时)
|
|
31882
|
+
*/
|
|
31883
|
+
async function resetPluginKeys(service) {
|
|
31884
|
+
const ids = service.catalog.getSnapshot().filter((row) => row.id.startsWith("edrv.") && row.modified).map((row) => row.id);
|
|
31885
|
+
let reset = 0;
|
|
31886
|
+
for (const id of ids) {
|
|
31887
|
+
if (!await editOnce(service, {
|
|
31888
|
+
type: "reset",
|
|
31889
|
+
id
|
|
31890
|
+
})) return {
|
|
31891
|
+
reset,
|
|
31892
|
+
failed: id
|
|
31893
|
+
};
|
|
31894
|
+
reset += 1;
|
|
31895
|
+
}
|
|
31896
|
+
return { reset };
|
|
31897
|
+
}
|
|
31898
|
+
const MIGRATION_KEY = "dsh-vscode-mode.shortcuts-migrated.v1.";
|
|
31899
|
+
/**
|
|
31900
|
+
* 当前快捷键运行平台的迁移标记键;不同 Web/Desktop 和操作系统分别处理。
|
|
31901
|
+
* @author ddj 2026年09月28号
|
|
31902
|
+
* @param service 官方快捷键服务
|
|
31903
|
+
* @returns 本机迁移标记键
|
|
31904
|
+
*/
|
|
31905
|
+
function migrationKey(service) {
|
|
31906
|
+
return MIGRATION_KEY + service.runtime + "." + service.platform;
|
|
31907
|
+
}
|
|
31908
|
+
/**
|
|
31909
|
+
* 检查当前端是否已处理旧设置;存储不可用时继续按目录状态判定。
|
|
31910
|
+
* @author ddj 2026年09月28号
|
|
31911
|
+
* @param service 官方快捷键服务
|
|
31912
|
+
* @returns 是否已处理
|
|
31913
|
+
*/
|
|
31914
|
+
function hasMigrated(service) {
|
|
31915
|
+
try {
|
|
31916
|
+
return typeof localStorage !== "undefined" && localStorage.getItem(migrationKey(service)) === "1";
|
|
31917
|
+
} catch {
|
|
31918
|
+
return false;
|
|
31919
|
+
}
|
|
31920
|
+
}
|
|
31921
|
+
/**
|
|
31922
|
+
* 标记当前端已处理;旧设置永不清空,供其他平台及人工恢复使用。
|
|
31923
|
+
* @author ddj 2026年09月28号
|
|
31924
|
+
* @param service 官方快捷键服务
|
|
31925
|
+
*/
|
|
31926
|
+
function markMigrated(service) {
|
|
31927
|
+
try {
|
|
31928
|
+
if (typeof localStorage !== "undefined") localStorage.setItem(migrationKey(service), "1");
|
|
31929
|
+
} catch {
|
|
31930
|
+
bridgeLog.warn("无法持久化旧快捷键迁移标记,本次配置仍保留");
|
|
31931
|
+
}
|
|
31932
|
+
}
|
|
31933
|
+
/**
|
|
31934
|
+
* 旧 keybindings 设置按当前 runtime/platform 一次性导入官方存储:
|
|
31935
|
+
* - 仅处理与 KEYBINDING_DEFAULTS 有差异的单候选键位;多候选、空键位、冲突或
|
|
31936
|
+
* 官方不可表示项不迁移,完整旧值仍保留(官方每条命令当前只允许一个绑定);
|
|
31937
|
+
* - 官方已有自定义值时不覆盖,stale 冲突也不自动重放;
|
|
31938
|
+
* - 旧配置供其他平台及人工恢复使用,绝不清空;当前端成功处理后记录本地标记,
|
|
31939
|
+
* 防止重启后把已由用户重置的官方键位重新导入。
|
|
31940
|
+
* @author ddj 2026年10月
|
|
31941
|
+
* @param service 官方服务
|
|
31942
|
+
* @param scope 旧设置 scope(settings bridge 绑定产物)
|
|
31943
|
+
* @returns 迁移结果
|
|
31944
|
+
*/
|
|
31945
|
+
async function migrateLegacyKeybindings(service, scope) {
|
|
31946
|
+
const snapshot = scope.getSnapshot();
|
|
31947
|
+
if (snapshot.status !== "ready") return {
|
|
31948
|
+
imported: 0,
|
|
31949
|
+
skipped: 0,
|
|
31950
|
+
done: false
|
|
31951
|
+
};
|
|
31952
|
+
if (service.config.getSnapshot().status !== "ready") return {
|
|
31953
|
+
imported: 0,
|
|
31954
|
+
skipped: 0,
|
|
31955
|
+
done: false
|
|
31956
|
+
};
|
|
31957
|
+
if (hasMigrated(service)) return {
|
|
31958
|
+
imported: 0,
|
|
31959
|
+
skipped: 0,
|
|
31960
|
+
done: true
|
|
31961
|
+
};
|
|
31962
|
+
const stored = normalizeKeybindings(snapshot.value?.keybindings);
|
|
31963
|
+
const overrides = Object.entries(stored).filter(([id, chord]) => chord !== KEYBINDING_DEFAULTS_OF[id]);
|
|
31964
|
+
if (overrides.length === 0) return {
|
|
31965
|
+
imported: 0,
|
|
31966
|
+
skipped: 0,
|
|
31967
|
+
done: true
|
|
31968
|
+
};
|
|
31969
|
+
let imported = 0;
|
|
31970
|
+
let skipped = 0;
|
|
31971
|
+
for (const [id, chord] of overrides) {
|
|
31972
|
+
if (!chord.trim() || chord.includes("|")) {
|
|
31973
|
+
skipped += 1;
|
|
31974
|
+
bridgeLog.warn("迁移跳过(空键位或多候选,原值保留):" + id);
|
|
31975
|
+
continue;
|
|
31976
|
+
}
|
|
31977
|
+
const binding = chordToBinding(chord);
|
|
31978
|
+
if (binding === null) {
|
|
31979
|
+
skipped += 1;
|
|
31980
|
+
bridgeLog.warn("迁移跳过(官方不可表示):" + id + " = " + chord);
|
|
31981
|
+
continue;
|
|
31982
|
+
}
|
|
31983
|
+
if (service.catalog.getSnapshot().some((row) => row.id === id && row.modified)) {
|
|
31984
|
+
skipped += 1;
|
|
31985
|
+
bridgeLog.warn("迁移跳过(官方存储已有自定义值):" + id);
|
|
31986
|
+
continue;
|
|
31987
|
+
}
|
|
31988
|
+
let probe;
|
|
31989
|
+
try {
|
|
31990
|
+
probe = service.describeBinding(binding);
|
|
31991
|
+
} catch (error) {
|
|
31992
|
+
bridgeLog.warn("迁移中止(describeBinding 异常):" + String(error));
|
|
31993
|
+
return {
|
|
31994
|
+
imported,
|
|
31995
|
+
skipped,
|
|
31996
|
+
done: false
|
|
31997
|
+
};
|
|
31998
|
+
}
|
|
31999
|
+
if (probe.conflicts.length > 0 || probe.issue !== null) {
|
|
32000
|
+
skipped += 1;
|
|
32001
|
+
bridgeLog.warn("迁移跳过(与官方键冲突/保留):" + id + " = " + chord);
|
|
32002
|
+
continue;
|
|
32003
|
+
}
|
|
32004
|
+
if (!await editOnce(service, {
|
|
32005
|
+
type: "set",
|
|
32006
|
+
id,
|
|
32007
|
+
binding
|
|
32008
|
+
})) return {
|
|
32009
|
+
imported,
|
|
32010
|
+
skipped,
|
|
32011
|
+
done: false
|
|
32012
|
+
};
|
|
32013
|
+
imported += 1;
|
|
32014
|
+
}
|
|
32015
|
+
markMigrated(service);
|
|
32016
|
+
bridgeLog.info("旧快捷键设置已处理(旧值保留):导入 " + imported + " 条,跳过 " + skipped + " 条");
|
|
32017
|
+
return {
|
|
32018
|
+
imported,
|
|
32019
|
+
skipped,
|
|
32020
|
+
done: true
|
|
32021
|
+
};
|
|
32022
|
+
}
|
|
32023
|
+
//#endregion
|
|
32024
|
+
//#region src/client/ui/ShortcutSettings.ts
|
|
32025
|
+
/**
|
|
32026
|
+
* dsh-vscode-mode client — 独立「快捷键」设置区(与 VSCodeMode 平级,settings.section slot)。
|
|
32027
|
+
* 子页一「官方快捷键」:官方 shortcuts 目录只读总览(官方 + 插件命令)+ 一键打开官方快捷键弹窗;
|
|
32028
|
+
* 子页二「插件快捷键」:插件命令(edrv.*)行内录键编辑,持久化经官方 edit API
|
|
32029
|
+
* (Desktop: userData/keybindings.json;Web: localStorage dsh.keybindings.v1),不再写插件设置。
|
|
32030
|
+
* 录制期间调 service.recording(true):Desktop 原生键盘桥暂停拦截,否则录不到键。
|
|
32031
|
+
* 样式复用 vsm-kb-* / vsm-panel-* / vsm-mcp-subtabs(styles/mcp.css 随 client bundle 注入)。
|
|
32032
|
+
* 作者 ddj 2026年10月
|
|
32033
|
+
*/
|
|
32034
|
+
const settingsLog = log.child("shortcut-settings");
|
|
32035
|
+
const MODIFIER_ONLY = /* @__PURE__ */ new Set([
|
|
32036
|
+
"Control",
|
|
32037
|
+
"Shift",
|
|
32038
|
+
"Alt",
|
|
32039
|
+
"Meta",
|
|
32040
|
+
"AltGraph"
|
|
32041
|
+
]);
|
|
32042
|
+
/** 录制键 → 官方绑定(物理 code + 修饰符;Ctrl/Cmd 同为 primary,与官方互认语义一致)。 */
|
|
32043
|
+
function bindingFromEvent(e) {
|
|
32044
|
+
const modifiers = [];
|
|
32045
|
+
if (e.ctrlKey || e.metaKey) modifiers.push("primary");
|
|
32046
|
+
if (e.altKey) modifiers.push("alt");
|
|
32047
|
+
if (e.shiftKey) modifiers.push("shift");
|
|
32048
|
+
return {
|
|
32049
|
+
code: e.code,
|
|
32050
|
+
modifiers
|
|
32051
|
+
};
|
|
32052
|
+
}
|
|
32053
|
+
/** 目录行展示键位(keys 已按平台格式化;binding null = 未绑定)。 */
|
|
32054
|
+
function keysText(row) {
|
|
32055
|
+
if (!row || row.binding === null) return "未绑定";
|
|
32056
|
+
return row.keys.join(" ");
|
|
32057
|
+
}
|
|
32058
|
+
/** 插件命令行 = COMMANDS 目录序 × 官方目录行(行缺失时占位「未注册」)。 */
|
|
32059
|
+
function catalogRowsOf(service) {
|
|
32060
|
+
const byId = new Map(service.catalog.getSnapshot().map((row) => [row.id, row]));
|
|
32061
|
+
return COMMANDS.map((command) => ({
|
|
32062
|
+
id: command.id,
|
|
32063
|
+
label: command.label,
|
|
32064
|
+
row: byId.get(command.id) ?? null
|
|
32065
|
+
}));
|
|
32066
|
+
}
|
|
32067
|
+
/**
|
|
32068
|
+
* 「快捷键」设置区主体:子页切换 + 服务就绪探测(晚到自愈,有界重试)。
|
|
32069
|
+
* @author ddj 2026年10月
|
|
32070
|
+
* @param props.getShortcuts 官方服务运行时探测函数(client/index.ts 注入)
|
|
32071
|
+
*/
|
|
32072
|
+
function ShortcutSettings(props) {
|
|
32073
|
+
const getShortcuts = props?.getShortcuts;
|
|
32074
|
+
const [service, setService] = react.default.useState(() => typeof getShortcuts === "function" ? getShortcuts() : null);
|
|
32075
|
+
const [tab, setTab] = react.default.useState("official");
|
|
32076
|
+
const [, setVersion] = react.default.useState(0);
|
|
32077
|
+
react.default.useEffect(() => {
|
|
32078
|
+
if (service) return void 0;
|
|
32079
|
+
let attempts = 0;
|
|
32080
|
+
let cancelled = false;
|
|
32081
|
+
const tick = () => {
|
|
32082
|
+
if (cancelled) return;
|
|
32083
|
+
const found = typeof getShortcuts === "function" ? getShortcuts() : null;
|
|
32084
|
+
if (found) {
|
|
32085
|
+
setService(found);
|
|
32086
|
+
return;
|
|
32087
|
+
}
|
|
32088
|
+
if (attempts >= 15) return;
|
|
32089
|
+
attempts += 1;
|
|
32090
|
+
setTimeout(tick, 2e3);
|
|
32091
|
+
};
|
|
32092
|
+
tick();
|
|
32093
|
+
return () => {
|
|
32094
|
+
cancelled = true;
|
|
32095
|
+
};
|
|
32096
|
+
}, [service]);
|
|
32097
|
+
react.default.useEffect(() => {
|
|
32098
|
+
if (!service) return void 0;
|
|
32099
|
+
const bump = () => setVersion((v) => v + 1);
|
|
32100
|
+
const offCatalog = service.catalog.subscribe(bump);
|
|
32101
|
+
const offConfig = service.config.subscribe(bump);
|
|
32102
|
+
return () => {
|
|
32103
|
+
offCatalog();
|
|
32104
|
+
offConfig();
|
|
32105
|
+
};
|
|
32106
|
+
}, [service]);
|
|
32107
|
+
let body;
|
|
32108
|
+
if (!service) body = react.default.createElement("div", { className: "vsm-mcp-empty" }, "官方快捷键服务未就绪(正在探测;若长期无响应,说明当前 DSH 版本不含官方快捷键机制)。");
|
|
32109
|
+
else if (tab === "plugin") body = react.default.createElement(PluginShortcutEditor, { service });
|
|
32110
|
+
else body = react.default.createElement(OfficialShortcutsPane, { service });
|
|
32111
|
+
return react.default.createElement("section", { className: "vsm-general-page" }, react.default.createElement("header", { className: "vsm-mcp-header" }, react.default.createElement("div", null, react.default.createElement("h2", null, "快捷键"), react.default.createElement("p", null, "基于官方 shortcuts 机制:统一注册、冲突校验、按平台差异化默认值;Desktop 持久化到 userData/keybindings.json,Web 持久化到 localStorage(dsh.keybindings.v1)。"))), react.default.createElement("nav", { className: "vsm-mcp-subtabs" }, react.default.createElement("button", {
|
|
32112
|
+
className: tab === "official" ? "active" : "",
|
|
32113
|
+
onClick: () => setTab("official")
|
|
32114
|
+
}, "官方快捷键"), react.default.createElement("button", {
|
|
32115
|
+
className: tab === "plugin" ? "active" : "",
|
|
32116
|
+
onClick: () => setTab("plugin")
|
|
32117
|
+
}, "插件快捷键")), body);
|
|
32118
|
+
}
|
|
32119
|
+
/** 官方目录行(只读总览行)。 */
|
|
32120
|
+
function OfficialRow({ row }) {
|
|
32121
|
+
return react.default.createElement("div", { className: "vsm-kb-row" }, react.default.createElement("div", { className: "vsm-kb-label" }, react.default.createElement("span", null, row.label), react.default.createElement("small", null, row.id), row.modified && react.default.createElement("small", { className: "vsm-kb-warn" }, "已自定义"), row.issue && react.default.createElement("small", { className: "vsm-kb-warn" }, "键位不受当前环境支持"), row.conflicts.length > 0 && react.default.createElement("small", { className: "vsm-kb-warn" }, "与 " + row.conflicts.join("、") + " 冲突")), react.default.createElement("kbd", { className: "vsm-kb-key" + (row.conflicts.length ? " warn" : "") }, keysText(row)));
|
|
32122
|
+
}
|
|
32123
|
+
/** 子页一:官方快捷键(弹窗入口 + 目录总览)。 */
|
|
32124
|
+
function OfficialShortcutsPane({ service }) {
|
|
32125
|
+
const [error, setError] = react.default.useState("");
|
|
32126
|
+
const rows = service.catalog.getSnapshot();
|
|
32127
|
+
const config = service.config.getSnapshot();
|
|
32128
|
+
const openOfficial = () => {
|
|
32129
|
+
setError("");
|
|
32130
|
+
if (!service.registry || typeof service.registry.invoke !== "function") {
|
|
32131
|
+
setError("官方弹窗入口不可用;可按 Ctrl+/ 或到 设置 → 通用 → 快捷键 打开");
|
|
32132
|
+
return;
|
|
32133
|
+
}
|
|
32134
|
+
try {
|
|
32135
|
+
service.registry.invoke("shortcuts.open", {
|
|
32136
|
+
region: "page",
|
|
32137
|
+
modal: null,
|
|
32138
|
+
target: typeof document !== "undefined" ? document.activeElement : null
|
|
32139
|
+
});
|
|
32140
|
+
} catch (e) {
|
|
32141
|
+
settingsLog.warn("打开官方快捷键弹窗失败:" + String(e));
|
|
32142
|
+
setError("打开官方弹窗失败:" + String(e) + ";可按 Ctrl+/ 或到 设置 → 通用 → 快捷键 打开");
|
|
32143
|
+
}
|
|
32144
|
+
};
|
|
32145
|
+
return react.default.createElement(react.default.Fragment, null, react.default.createElement("section", { className: "vsm-panel" }, react.default.createElement("h3", { className: "vsm-panel-title" }, "官方快捷键配置"), react.default.createElement("div", { className: "vsm-panel-body" }, react.default.createElement("p", null, "官方快捷键弹窗(Ctrl+/ 或 设置 → 通用 → 快捷键)提供全部命令的查看、录键与恢复默认;下方为当前目录只读总览(含插件注册的命令)。"), react.default.createElement("button", {
|
|
32146
|
+
className: "vsm-primary vsm-small",
|
|
32147
|
+
onClick: openOfficial
|
|
32148
|
+
}, "打开官方快捷键设置(Ctrl+/)"), error && react.default.createElement("div", { className: "vsm-mcp-error vsm-mcp-banner" }, error), config?.status === "unreadable" && react.default.createElement("div", { className: "vsm-mcp-error vsm-mcp-banner" }, "快捷键配置读取失败(内容损坏或由更新版本写入),当前使用默认键位;请备份后清理配置,或升级 DSH。"))), react.default.createElement("section", { className: "vsm-panel" }, react.default.createElement("h3", { className: "vsm-panel-title" }, "命令目录(只读总览)"), react.default.createElement("div", { className: "vsm-panel-body" }, react.default.createElement("div", { className: "vsm-kb-list" }, rows.map((row) => react.default.createElement(OfficialRow, {
|
|
32149
|
+
key: row.id,
|
|
32150
|
+
row
|
|
32151
|
+
}))))));
|
|
32152
|
+
}
|
|
32153
|
+
/** 子页二:插件命令键位(行内录键 + 行级清除 + 恢复默认;写入走官方 edit API)。 */
|
|
32154
|
+
function PluginShortcutEditor({ service }) {
|
|
32155
|
+
const [recording, setRecording] = react.default.useState(null);
|
|
32156
|
+
const [busy, setBusy] = react.default.useState(false);
|
|
32157
|
+
const [message, setMessage] = react.default.useState("");
|
|
32158
|
+
const [error, setError] = react.default.useState("");
|
|
32159
|
+
const rows = catalogRowsOf(service);
|
|
32160
|
+
const config = service.config.getSnapshot();
|
|
32161
|
+
const saveBinding = async (id, binding) => {
|
|
32162
|
+
setBusy(true);
|
|
32163
|
+
setError("");
|
|
32164
|
+
setMessage("");
|
|
32165
|
+
const buildEdit = () => binding === null ? {
|
|
32166
|
+
type: "reset",
|
|
32167
|
+
id
|
|
32168
|
+
} : {
|
|
32169
|
+
type: "set",
|
|
32170
|
+
id,
|
|
32171
|
+
binding
|
|
32172
|
+
};
|
|
32173
|
+
try {
|
|
32174
|
+
const result = await service.edit(buildEdit(), service.config.getSnapshot().revision);
|
|
32175
|
+
if (result.status === "saved") setMessage("已保存");
|
|
32176
|
+
else if (result.status === "stale") setError("快捷键配置已在其他窗口更新,请检查当前键位后重新录制");
|
|
32177
|
+
else if (result.status === "conflict") {
|
|
32178
|
+
if (result.issue) setError("键位被官方拒绝:" + ({
|
|
32179
|
+
"unsupported-browser": "web 模式白名单外的组合(单修饰符组合在浏览器环境不可靠)",
|
|
32180
|
+
"reserved": "官方保留键",
|
|
32181
|
+
"unsupported-key": "官方不支持的键",
|
|
32182
|
+
"modifier-required": "至少需要一个修饰键"
|
|
32183
|
+
}[result.issue] ?? result.issue) + "。web 模式可用 Ctrl+Alt+X 风格组合;升级桌面外壳进入桌面模式后不受此限。");
|
|
32184
|
+
else setError("键位冲突:与 " + (result.conflicts?.join("、") ?? "其他命令") + " 冲突,请换一个组合");
|
|
32185
|
+
} else if (result.status === "not-ready") setError("快捷键配置尚未就绪,请稍后重试");
|
|
32186
|
+
else if (result.status === "unreadable") setError("快捷键配置读取失败,编辑已禁用(见官方提示)");
|
|
32187
|
+
else setError("写入失败,请重试");
|
|
32188
|
+
} catch (e) {
|
|
32189
|
+
settingsLog.warn("快捷键保存失败:" + String(e));
|
|
32190
|
+
setError("保存失败:" + String(e));
|
|
32191
|
+
} finally {
|
|
32192
|
+
setBusy(false);
|
|
32193
|
+
}
|
|
32194
|
+
};
|
|
32195
|
+
const resetAll = async () => {
|
|
32196
|
+
if (!window.confirm("恢复全部插件快捷键为默认值?(只影响本插件命令的键位覆盖)")) return;
|
|
32197
|
+
setBusy(true);
|
|
32198
|
+
setError("");
|
|
32199
|
+
setMessage("");
|
|
32200
|
+
try {
|
|
32201
|
+
const result = await resetPluginKeys(service);
|
|
32202
|
+
if (result.failed) setError("已恢复 " + result.reset + " 条," + result.failed + " 恢复失败;请重试");
|
|
32203
|
+
else setMessage("已恢复 " + result.reset + " 条插件快捷键");
|
|
32204
|
+
} catch (e) {
|
|
32205
|
+
settingsLog.warn("快捷键恢复默认失败:" + String(e));
|
|
32206
|
+
setError("恢复失败:" + String(e));
|
|
32207
|
+
} finally {
|
|
32208
|
+
setBusy(false);
|
|
32209
|
+
}
|
|
32210
|
+
};
|
|
32211
|
+
react.default.useEffect(() => {
|
|
32212
|
+
if (!recording) return void 0;
|
|
32213
|
+
const onKey = (e) => {
|
|
32214
|
+
e.preventDefault();
|
|
32215
|
+
e.stopPropagation();
|
|
32216
|
+
if (e.key === "Escape") {
|
|
32217
|
+
setRecording(null);
|
|
32218
|
+
return;
|
|
32219
|
+
}
|
|
32220
|
+
if (e.key === "Backspace" || e.key === "Delete") {
|
|
32221
|
+
const id = recording;
|
|
32222
|
+
setRecording(null);
|
|
32223
|
+
saveBinding(id, null);
|
|
32224
|
+
return;
|
|
32225
|
+
}
|
|
32226
|
+
if (MODIFIER_ONLY.has(e.key)) return;
|
|
32227
|
+
const id = recording;
|
|
32228
|
+
setRecording(null);
|
|
32229
|
+
saveBinding(id, bindingFromEvent(e));
|
|
32230
|
+
};
|
|
32231
|
+
window.addEventListener("keydown", onKey, true);
|
|
32232
|
+
return () => window.removeEventListener("keydown", onKey, true);
|
|
32233
|
+
}, [recording]);
|
|
32234
|
+
react.default.useEffect(() => {
|
|
32235
|
+
if (!service || typeof service.recording !== "function") return void 0;
|
|
32236
|
+
service.recording(Boolean(recording)).catch(() => {});
|
|
32237
|
+
return () => {
|
|
32238
|
+
if (recording) service.recording(false).catch(() => {});
|
|
32239
|
+
};
|
|
32240
|
+
}, [recording, service]);
|
|
32241
|
+
const rowsView = rows.map(({ id, label, row }) => {
|
|
32242
|
+
const recordingRow = recording === id;
|
|
32243
|
+
const display = recordingRow ? react.default.createElement("span", { className: "vsm-kb-rec" }, "按下组合键…(Esc 取消 / Backspace 清除)") : react.default.createElement("kbd", { className: "vsm-kb-key" + (row && row.conflicts.length ? " warn" : "") }, keysText(row));
|
|
32244
|
+
return react.default.createElement("div", {
|
|
32245
|
+
key: id,
|
|
32246
|
+
className: "vsm-kb-row" + (recordingRow ? " rec" : "")
|
|
32247
|
+
}, react.default.createElement("div", { className: "vsm-kb-label" }, react.default.createElement("span", null, label), row === null && react.default.createElement("small", { className: "vsm-kb-warn" }, "未注册(官方服务未同步)"), row && row.modified && react.default.createElement("small", { className: "vsm-kb-warn" }, "已自定义"), row && row.conflicts.length > 0 && react.default.createElement("small", { className: "vsm-kb-warn" }, "与 " + row.conflicts.join("、") + " 冲突")), display, react.default.createElement("div", { className: "vsm-kb-actions" }, recordingRow ? react.default.createElement("button", { onClick: () => setRecording(null) }, "取消") : react.default.createElement(react.default.Fragment, null, react.default.createElement("button", {
|
|
32248
|
+
disabled: busy || config?.status !== "ready",
|
|
32249
|
+
onClick: () => {
|
|
32250
|
+
setRecording(id);
|
|
32251
|
+
setMessage("");
|
|
32252
|
+
}
|
|
32253
|
+
}, "编辑"), react.default.createElement("button", {
|
|
32254
|
+
disabled: busy || config?.status !== "ready",
|
|
32255
|
+
title: "清除该命令的键位(恢复默认)",
|
|
32256
|
+
onClick: () => void saveBinding(id, null)
|
|
32257
|
+
}, "清除"))));
|
|
32258
|
+
});
|
|
32259
|
+
return react.default.createElement("section", { className: "vsm-panel" }, react.default.createElement("h3", { className: "vsm-panel-title" }, "插件命令键位"), react.default.createElement("div", { className: "vsm-panel-body" }, react.default.createElement("div", { className: "vsm-kb-list" }, rowsView)), react.default.createElement("div", { className: "vsm-panel-body vsm-kb-actionsbar" }, react.default.createElement("span", { className: "vsm-kb-dirty" }, recording ? "录制中…" : ""), message && react.default.createElement("span", { className: "vsm-kb-message" }, message), error && react.default.createElement("span", { className: "vsm-kb-warn" }, error), react.default.createElement("button", {
|
|
32260
|
+
className: "vsm-small",
|
|
32261
|
+
disabled: busy || config?.status !== "ready",
|
|
32262
|
+
onClick: () => void resetAll()
|
|
32263
|
+
}, "恢复默认")));
|
|
32264
|
+
}
|
|
32265
|
+
//#endregion
|
|
31715
32266
|
//#region src/client/sessionScope.ts
|
|
31716
32267
|
/** 对象窄化(非对象返回 undefined,供后续安全取字段)。 */
|
|
31717
32268
|
function asRecord(value) {
|
|
@@ -31869,29 +32420,30 @@ window.__ModuleLoader__.load({
|
|
|
31869
32420
|
"sessions",
|
|
31870
32421
|
"conversation"
|
|
31871
32422
|
];
|
|
31872
|
-
/**
|
|
31873
|
-
let
|
|
32423
|
+
/** 指令桥单例(同一 document 重复 apply 只装配一次;HMR 重挂不重复注册)。 */
|
|
32424
|
+
let commandsBridge = null;
|
|
31874
32425
|
/**
|
|
31875
32426
|
* 装配指令注册表(指令桥 + 命令栏)。
|
|
31876
32427
|
* 指令桥不依赖已挂载编辑器(run 只派发窗口事件),故可在启动期装配;
|
|
31877
32428
|
* 命令栏浮层的宿主由 CommandPalette 自己在编辑区树内认领。
|
|
31878
|
-
* @author ddj 2026年09月10号
|
|
32429
|
+
* @author ddj 2026年09月10号 / 2026年10月
|
|
31879
32430
|
* @param ctx 客户端根上下文
|
|
31880
|
-
* @returns
|
|
32431
|
+
* @returns 指令桥(官方 shortcuts 注册的执行器来源)
|
|
31881
32432
|
*/
|
|
31882
32433
|
function setupCommands(ctx) {
|
|
31883
|
-
if (
|
|
31884
|
-
commandsMounted = true;
|
|
32434
|
+
if (commandsBridge) return commandsBridge;
|
|
31885
32435
|
const bridge = createCommandBridge();
|
|
32436
|
+
commandsBridge = bridge;
|
|
31886
32437
|
ctx.provide(REGISTRY_GLOBAL, bridge.registry);
|
|
31887
32438
|
const host = window;
|
|
31888
32439
|
host[REGISTRY_GLOBAL] = bridge.registry;
|
|
31889
32440
|
ctx.effect(() => () => {
|
|
31890
32441
|
bridge.dispose();
|
|
31891
|
-
|
|
32442
|
+
commandsBridge = null;
|
|
31892
32443
|
if (host["__edrvCommands__"] === bridge.registry) delete host[REGISTRY_GLOBAL];
|
|
31893
32444
|
}, "vscode-mode: command registry");
|
|
31894
32445
|
log.info("指令系统已装配:" + bridge.registry.list().length + " 条指令,Ctrl+Shift+P 打开命令栏");
|
|
32446
|
+
return bridge;
|
|
31895
32447
|
}
|
|
31896
32448
|
/**
|
|
31897
32449
|
* 装配客户端:注册中央编辑区视图与 header 差异角标。
|
|
@@ -31929,7 +32481,7 @@ window.__ModuleLoader__.load({
|
|
|
31929
32481
|
restore?.();
|
|
31930
32482
|
};
|
|
31931
32483
|
}, "vscode-mode: settings document editor route");
|
|
31932
|
-
setupCommands(ctx);
|
|
32484
|
+
const commands = setupCommands(ctx);
|
|
31933
32485
|
const registry = createFileOpenerRegistry();
|
|
31934
32486
|
const workspaces = ctx.get("workspaces");
|
|
31935
32487
|
const sessions = ctx.get("sessions");
|
|
@@ -32084,6 +32636,18 @@ window.__ModuleLoader__.load({
|
|
|
32084
32636
|
claimDisposer = null;
|
|
32085
32637
|
}
|
|
32086
32638
|
};
|
|
32639
|
+
const legacyDefaults = defaultKeybindings();
|
|
32640
|
+
let officialReady = false;
|
|
32641
|
+
/**
|
|
32642
|
+
* 获取旧版快捷键覆盖;设置桥尚未就绪时回退默认键位。
|
|
32643
|
+
* @author ddj 2026年09月28号
|
|
32644
|
+
* @returns 旧版键位弦表
|
|
32645
|
+
*/
|
|
32646
|
+
const legacyChords = () => ({
|
|
32647
|
+
...legacyDefaults,
|
|
32648
|
+
...normalizeKeybindings(bridge.scope()?.getSnapshot().value?.keybindings)
|
|
32649
|
+
});
|
|
32650
|
+
applyOfficialChords(legacyChords());
|
|
32087
32651
|
let settingsSyncMounted = false;
|
|
32088
32652
|
const mountSettingsSyncs = () => {
|
|
32089
32653
|
if (settingsSyncMounted) return;
|
|
@@ -32105,7 +32669,7 @@ window.__ModuleLoader__.load({
|
|
|
32105
32669
|
const sync = () => {
|
|
32106
32670
|
const snapshot = scope.getSnapshot();
|
|
32107
32671
|
if (snapshot.status === "loading") return;
|
|
32108
|
-
|
|
32672
|
+
if (!officialReady) applyOfficialChords(legacyChords());
|
|
32109
32673
|
const minW = sidebarMinApply(snapshot.value?.sidebarMinWidth);
|
|
32110
32674
|
window.dispatchEvent(new CustomEvent("edrv:sidebar-min-width", { detail: { value: minW } }));
|
|
32111
32675
|
const limit = editorLimitApply(snapshot.value?.maxOpenEditors);
|
|
@@ -32118,6 +32682,53 @@ window.__ModuleLoader__.load({
|
|
|
32118
32682
|
};
|
|
32119
32683
|
mountSettingsSyncs();
|
|
32120
32684
|
if (!settingsSyncMounted) ctx.effect(() => bridge.whenReady(mountSettingsSyncs), "vscode-mode: settings bridge wait");
|
|
32685
|
+
ctx.effect(() => {
|
|
32686
|
+
let cancelled = false;
|
|
32687
|
+
let disposeRegistration = null;
|
|
32688
|
+
let disposeCatalog = null;
|
|
32689
|
+
const disposeLegacy = installLegacyKeys(commands.registry.list(), commands.registry, legacyChords);
|
|
32690
|
+
const cancelProbe = awaitShortcutsService(ctx, {
|
|
32691
|
+
schedule,
|
|
32692
|
+
onReady: (service) => {
|
|
32693
|
+
if (cancelled) return;
|
|
32694
|
+
if (service === null) {
|
|
32695
|
+
log.warn("官方 shortcuts 服务不可用(DSH 版本过旧?),继续使用旧版快捷键派发");
|
|
32696
|
+
return;
|
|
32697
|
+
}
|
|
32698
|
+
disposeLegacy();
|
|
32699
|
+
officialReady = true;
|
|
32700
|
+
disposeRegistration = registerOfficialShortcuts(service, commands.registry.list(), commands.registry, SHORTCUT_PROFILES);
|
|
32701
|
+
disposeCatalog = bindCatalogChords(service, applyOfficialChords);
|
|
32702
|
+
let attempts = 0;
|
|
32703
|
+
const attemptMigration = () => {
|
|
32704
|
+
if (cancelled) return;
|
|
32705
|
+
const scope = bridge.scope();
|
|
32706
|
+
if (!scope) {
|
|
32707
|
+
if (attempts < 15) {
|
|
32708
|
+
attempts += 1;
|
|
32709
|
+
schedule(attemptMigration, 2e3);
|
|
32710
|
+
}
|
|
32711
|
+
return;
|
|
32712
|
+
}
|
|
32713
|
+
migrateLegacyKeybindings(service, scope).then((result) => {
|
|
32714
|
+
if (cancelled || result.done) return;
|
|
32715
|
+
if (attempts < 15) {
|
|
32716
|
+
attempts += 1;
|
|
32717
|
+
schedule(attemptMigration, 2e3);
|
|
32718
|
+
}
|
|
32719
|
+
});
|
|
32720
|
+
};
|
|
32721
|
+
attemptMigration();
|
|
32722
|
+
}
|
|
32723
|
+
});
|
|
32724
|
+
return () => {
|
|
32725
|
+
cancelled = true;
|
|
32726
|
+
cancelProbe();
|
|
32727
|
+
disposeLegacy();
|
|
32728
|
+
disposeRegistration?.();
|
|
32729
|
+
disposeCatalog?.();
|
|
32730
|
+
};
|
|
32731
|
+
}, "vscode-mode: official shortcuts");
|
|
32121
32732
|
if (workspaces?.openPath) ctx.effect(() => installOpenPathRouter({
|
|
32122
32733
|
workspaces,
|
|
32123
32734
|
registry,
|
|
@@ -32332,6 +32943,12 @@ window.__ModuleLoader__.load({
|
|
|
32332
32943
|
openerRegistry: registry,
|
|
32333
32944
|
compatSummary
|
|
32334
32945
|
})));
|
|
32946
|
+
registerSlotSafely(ctx, {
|
|
32947
|
+
name: "settings.section",
|
|
32948
|
+
id: "vscode-mode-shortcuts",
|
|
32949
|
+
order: 31,
|
|
32950
|
+
label: "快捷键"
|
|
32951
|
+
}, () => react.default.createElement(ShortcutSettings, { getShortcuts: () => detectShortcuts(ctx) }));
|
|
32335
32952
|
ctx.effect(() => () => {
|
|
32336
32953
|
try {
|
|
32337
32954
|
disposeSnippets();
|