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
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
2
|
/**
|
|
3
|
-
* dsh-vscode-mode client — VSCodeMode 设置区:通用 /
|
|
3
|
+
* dsh-vscode-mode client — VSCodeMode 设置区:通用 / MCP 管理 / 语言服务器 / 性能优化 / 兼容性。
|
|
4
|
+
* (快捷键管理已迁出为独立顶层设置区 ui/ShortcutSettings.ts,与 VSCodeMode 平级。)
|
|
4
5
|
* MCP 管理 Tab 下含三个子页签:我的 MCP(profile 全局)/ 项目 MCP(各项目 .mcp.json)/ MCP 市场(占位)。
|
|
5
|
-
* 作者 ddj 2026年08月22号 / 2026年08月26号 / 2026年08月27号
|
|
6
|
+
* 作者 ddj 2026年08月22号 / 2026年08月26号 / 2026年08月27号 / 2026年10月
|
|
6
7
|
*/
|
|
7
8
|
import React from 'react'
|
|
8
9
|
import { rpc } from '../rpc.js'
|
|
@@ -14,7 +15,6 @@ import { normalizeSidebarMinWidth, SIDEBAR_MIN_DEFAULT } from '../sidebarMin.js'
|
|
|
14
15
|
import { EDITOR_LIMIT_CEIL, EDITOR_LIMIT_DEFAULT, normalizeMaxOpenEditors } from '../../shared/editorLimit.js'
|
|
15
16
|
import { TORTOISE_DIR_DEFAULT } from '../../shared/svn.js'
|
|
16
17
|
import { DEFAULT_NATIVE_CSV } from '../../shared/nativeOpen.js'
|
|
17
|
-
import { KeybindingsPanel } from './KeybindingsPanel.js'
|
|
18
18
|
import { LspSettings } from './LspSettings.js'
|
|
19
19
|
import { PerfSettings } from './PerfSettings.js'
|
|
20
20
|
import { IntegrationSettings } from './IntegrationSettings.js'
|
|
@@ -581,7 +581,6 @@ export function McpSettings(props) {
|
|
|
581
581
|
let body
|
|
582
582
|
if (loading) body = React.createElement('div', { className: 'vsm-mcp-empty' }, '正在读取 MCP 服务…')
|
|
583
583
|
else if (tab === 'general') body = React.createElement(GeneralSettings, { registry: openerRegistry })
|
|
584
|
-
else if (tab === 'keys') body = React.createElement(KeybindingsPanel, null)
|
|
585
584
|
else if (tab === 'mcp') body = React.createElement(McpManagePanel, { servers, projects, busy, draft, edit, resetDraft,
|
|
586
585
|
saveGlobal: (close) => saveGlobal(close), saveProject: (workspacePath, close) => saveProject(workspacePath, close),
|
|
587
586
|
refreshGlobal, toggleGlobal, removeGlobal, refreshProject, toggleProject, removeProject })
|
|
@@ -591,7 +590,7 @@ export function McpSettings(props) {
|
|
|
591
590
|
else body = React.createElement(PerfSettings, null)
|
|
592
591
|
return React.createElement('section', { className: 'vsm-mcp-page' },
|
|
593
592
|
React.createElement('header', { className: 'vsm-mcp-header' }, React.createElement('div', null, React.createElement('h2', null, 'VSCodeMode'), React.createElement('p', null, '管理当前 profile 与各项目的 Model Context Protocol 服务。'))),
|
|
594
|
-
React.createElement('nav', { className: 'vsm-mcp-tabs' }, React.createElement('button', { className: tab === 'general' ? 'active' : '', onClick: () => setTab('general') }, '通用'), React.createElement('button', { className: tab === '
|
|
593
|
+
React.createElement('nav', { className: 'vsm-mcp-tabs' }, React.createElement('button', { className: tab === 'general' ? 'active' : '', onClick: () => setTab('general') }, '通用'), React.createElement('button', { className: tab === 'mcp' ? 'active' : '', onClick: () => setTab('mcp') }, 'MCP 管理'), React.createElement('button', { className: tab === 'lsp' ? 'active' : '', onClick: () => setTab('lsp') }, '语言服务器'), React.createElement('button', { className: tab === 'ai' ? 'active' : '', onClick: () => setTab('ai') }, 'AI 补全'), React.createElement('button', { className: tab === 'perf' ? 'active' : '', onClick: () => setTab('perf') }, '性能优化'), React.createElement('button', { className: tab === 'compat' ? 'active' : '', onClick: () => setTab('compat') }, '兼容性')),
|
|
595
594
|
error && React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, error),
|
|
596
595
|
body,
|
|
597
596
|
)
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import React from 'react'
|
|
11
11
|
import { rpc } from '../rpc.js'
|
|
12
|
-
import {
|
|
12
|
+
import { chordOf, useKeybindingsVersion } from '../keybindings.js'
|
|
13
13
|
import { stepIndex } from './searchNav.js'
|
|
14
14
|
|
|
15
15
|
/**
|
|
@@ -64,19 +64,18 @@ export function QuickOpen(props) {
|
|
|
64
64
|
inputRef.current?.focus?.()
|
|
65
65
|
setOpen(true)
|
|
66
66
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
setOpen(false); inputRef.current?.blur?.()
|
|
73
|
-
}
|
|
67
|
+
// Esc 关闭(仅收起浮窗;历史 capture 行为保留,不参与命令派发、不吞键)
|
|
68
|
+
const onEscape = (e) => {
|
|
69
|
+
if (e.key !== 'Escape') return
|
|
70
|
+
setOpen(false)
|
|
71
|
+
inputRef.current?.blur?.()
|
|
74
72
|
}
|
|
75
|
-
|
|
76
|
-
//
|
|
73
|
+
// 快速打开键位(Ctrl+P)已归官方 shortcuts 机制派发:官方 resolve → 注册表 run →
|
|
74
|
+
// 派发 `edrv.command.quickOpen` 事件到此处(历史窗口 capture 键位自检移除,防双开)。
|
|
75
|
+
window.addEventListener('keydown', onEscape, true)
|
|
77
76
|
window.addEventListener('edrv.command.quickOpen', openBox)
|
|
78
77
|
return () => {
|
|
79
|
-
window.removeEventListener('keydown',
|
|
78
|
+
window.removeEventListener('keydown', onEscape, true)
|
|
80
79
|
window.removeEventListener('edrv.command.quickOpen', openBox)
|
|
81
80
|
}
|
|
82
81
|
}, [])
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* dsh-vscode-mode client — 独立「快捷键」设置区(与 VSCodeMode 平级,settings.section slot)。
|
|
4
|
+
* 子页一「官方快捷键」:官方 shortcuts 目录只读总览(官方 + 插件命令)+ 一键打开官方快捷键弹窗;
|
|
5
|
+
* 子页二「插件快捷键」:插件命令(edrv.*)行内录键编辑,持久化经官方 edit API
|
|
6
|
+
* (Desktop: userData/keybindings.json;Web: localStorage dsh.keybindings.v1),不再写插件设置。
|
|
7
|
+
* 录制期间调 service.recording(true):Desktop 原生键盘桥暂停拦截,否则录不到键。
|
|
8
|
+
* 样式复用 vsm-kb-* / vsm-panel-* / vsm-mcp-subtabs(styles/mcp.css 随 client bundle 注入)。
|
|
9
|
+
* 作者 ddj 2026年10月
|
|
10
|
+
*/
|
|
11
|
+
import React from 'react'
|
|
12
|
+
import { COMMANDS } from '../keybindings.js'
|
|
13
|
+
import { log } from '../log.js'
|
|
14
|
+
import { resetPluginKeys } from '../shortcutsOfficial.js'
|
|
15
|
+
import type { ShortcutsServiceLike, ShortcutCatalogRow } from '../shortcutsOfficial.js'
|
|
16
|
+
|
|
17
|
+
const settingsLog = log.child('shortcut-settings')
|
|
18
|
+
|
|
19
|
+
const MODIFIER_ONLY = new Set(['Control', 'Shift', 'Alt', 'Meta', 'AltGraph'])
|
|
20
|
+
|
|
21
|
+
/** 录制键 → 官方绑定(物理 code + 修饰符;Ctrl/Cmd 同为 primary,与官方互认语义一致)。 */
|
|
22
|
+
function bindingFromEvent(e) {
|
|
23
|
+
const modifiers = []
|
|
24
|
+
if (e.ctrlKey || e.metaKey) modifiers.push('primary')
|
|
25
|
+
if (e.altKey) modifiers.push('alt')
|
|
26
|
+
if (e.shiftKey) modifiers.push('shift')
|
|
27
|
+
return { code: e.code, modifiers }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 目录行展示键位(keys 已按平台格式化;binding null = 未绑定)。 */
|
|
31
|
+
function keysText(row) {
|
|
32
|
+
if (!row || row.binding === null) return '未绑定'
|
|
33
|
+
return row.keys.join(' ')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 插件命令行 = COMMANDS 目录序 × 官方目录行(行缺失时占位「未注册」)。 */
|
|
37
|
+
function catalogRowsOf(service) {
|
|
38
|
+
const byId = new Map(service.catalog.getSnapshot().map((row) => [row.id, row]))
|
|
39
|
+
return COMMANDS.map((command) => ({ id: command.id, label: command.label, row: byId.get(command.id) ?? null }))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 「快捷键」设置区主体:子页切换 + 服务就绪探测(晚到自愈,有界重试)。
|
|
44
|
+
* @author ddj 2026年10月
|
|
45
|
+
* @param props.getShortcuts 官方服务运行时探测函数(client/index.ts 注入)
|
|
46
|
+
*/
|
|
47
|
+
export function ShortcutSettings(props) {
|
|
48
|
+
const getShortcuts = props?.getShortcuts
|
|
49
|
+
const [service, setService] = React.useState(() => (typeof getShortcuts === 'function' ? getShortcuts() : null))
|
|
50
|
+
const [tab, setTab] = React.useState('official')
|
|
51
|
+
const [, setVersion] = React.useState(0)
|
|
52
|
+
|
|
53
|
+
// 服务就绪有界重试(服务晚到自愈;用尽保持提示文案)
|
|
54
|
+
React.useEffect(() => {
|
|
55
|
+
if (service) return undefined
|
|
56
|
+
let attempts = 0
|
|
57
|
+
let cancelled = false
|
|
58
|
+
const tick = () => {
|
|
59
|
+
if (cancelled) return
|
|
60
|
+
const found = typeof getShortcuts === 'function' ? getShortcuts() : null
|
|
61
|
+
if (found) {
|
|
62
|
+
setService(found)
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
if (attempts >= 15) return
|
|
66
|
+
attempts += 1
|
|
67
|
+
setTimeout(tick, 2000)
|
|
68
|
+
}
|
|
69
|
+
tick()
|
|
70
|
+
return () => {
|
|
71
|
+
cancelled = true
|
|
72
|
+
}
|
|
73
|
+
}, [service])
|
|
74
|
+
|
|
75
|
+
// 目录/配置变化即重渲染(官方 catalog/config 快照存储订阅)
|
|
76
|
+
React.useEffect(() => {
|
|
77
|
+
if (!service) return undefined
|
|
78
|
+
const bump = () => setVersion((v) => v + 1)
|
|
79
|
+
const offCatalog = service.catalog.subscribe(bump)
|
|
80
|
+
const offConfig = service.config.subscribe(bump)
|
|
81
|
+
return () => {
|
|
82
|
+
offCatalog()
|
|
83
|
+
offConfig()
|
|
84
|
+
}
|
|
85
|
+
}, [service])
|
|
86
|
+
|
|
87
|
+
let body
|
|
88
|
+
if (!service) {
|
|
89
|
+
body = React.createElement('div', { className: 'vsm-mcp-empty' },
|
|
90
|
+
'官方快捷键服务未就绪(正在探测;若长期无响应,说明当前 DSH 版本不含官方快捷键机制)。')
|
|
91
|
+
} else if (tab === 'plugin') {
|
|
92
|
+
body = React.createElement(PluginShortcutEditor, { service })
|
|
93
|
+
} else {
|
|
94
|
+
body = React.createElement(OfficialShortcutsPane, { service })
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return React.createElement('section', { className: 'vsm-general-page' },
|
|
98
|
+
React.createElement('header', { className: 'vsm-mcp-header' }, React.createElement('div', null,
|
|
99
|
+
React.createElement('h2', null, '快捷键'),
|
|
100
|
+
React.createElement('p', null, '基于官方 shortcuts 机制:统一注册、冲突校验、按平台差异化默认值;Desktop 持久化到 userData/keybindings.json,Web 持久化到 localStorage(dsh.keybindings.v1)。'))),
|
|
101
|
+
React.createElement('nav', { className: 'vsm-mcp-subtabs' },
|
|
102
|
+
React.createElement('button', { className: tab === 'official' ? 'active' : '', onClick: () => setTab('official') }, '官方快捷键'),
|
|
103
|
+
React.createElement('button', { className: tab === 'plugin' ? 'active' : '', onClick: () => setTab('plugin') }, '插件快捷键')),
|
|
104
|
+
body,
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** 官方目录行(只读总览行)。 */
|
|
109
|
+
function OfficialRow({ row }) {
|
|
110
|
+
return React.createElement('div', { className: 'vsm-kb-row' },
|
|
111
|
+
React.createElement('div', { className: 'vsm-kb-label' },
|
|
112
|
+
React.createElement('span', null, row.label),
|
|
113
|
+
React.createElement('small', null, row.id),
|
|
114
|
+
row.modified && React.createElement('small', { className: 'vsm-kb-warn' }, '已自定义'),
|
|
115
|
+
row.issue && React.createElement('small', { className: 'vsm-kb-warn' }, '键位不受当前环境支持'),
|
|
116
|
+
row.conflicts.length > 0 && React.createElement('small', { className: 'vsm-kb-warn' }, '与 ' + row.conflicts.join('、') + ' 冲突')),
|
|
117
|
+
React.createElement('kbd', { className: 'vsm-kb-key' + (row.conflicts.length ? ' warn' : '') }, keysText(row)),
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** 子页一:官方快捷键(弹窗入口 + 目录总览)。 */
|
|
122
|
+
function OfficialShortcutsPane({ service }) {
|
|
123
|
+
const [error, setError] = React.useState('')
|
|
124
|
+
const rows = service.catalog.getSnapshot()
|
|
125
|
+
const config = service.config.getSnapshot()
|
|
126
|
+
const openOfficial = () => {
|
|
127
|
+
setError('')
|
|
128
|
+
if (!service.registry || typeof service.registry.invoke !== 'function') {
|
|
129
|
+
setError('官方弹窗入口不可用;可按 Ctrl+/ 或到 设置 → 通用 → 快捷键 打开')
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
service.registry.invoke('shortcuts.open', {
|
|
134
|
+
region: 'page',
|
|
135
|
+
modal: null,
|
|
136
|
+
target: typeof document !== 'undefined' ? document.activeElement : null,
|
|
137
|
+
})
|
|
138
|
+
} catch (e) {
|
|
139
|
+
settingsLog.warn('打开官方快捷键弹窗失败:' + String(e))
|
|
140
|
+
setError('打开官方弹窗失败:' + String(e) + ';可按 Ctrl+/ 或到 设置 → 通用 → 快捷键 打开')
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return React.createElement(React.Fragment, null,
|
|
144
|
+
React.createElement('section', { className: 'vsm-panel' },
|
|
145
|
+
React.createElement('h3', { className: 'vsm-panel-title' }, '官方快捷键配置'),
|
|
146
|
+
React.createElement('div', { className: 'vsm-panel-body' },
|
|
147
|
+
React.createElement('p', null, '官方快捷键弹窗(Ctrl+/ 或 设置 → 通用 → 快捷键)提供全部命令的查看、录键与恢复默认;下方为当前目录只读总览(含插件注册的命令)。'),
|
|
148
|
+
React.createElement('button', { className: 'vsm-primary vsm-small', onClick: openOfficial }, '打开官方快捷键设置(Ctrl+/)'),
|
|
149
|
+
error && React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, error),
|
|
150
|
+
config?.status === 'unreadable' && React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' },
|
|
151
|
+
'快捷键配置读取失败(内容损坏或由更新版本写入),当前使用默认键位;请备份后清理配置,或升级 DSH。')),
|
|
152
|
+
),
|
|
153
|
+
React.createElement('section', { className: 'vsm-panel' },
|
|
154
|
+
React.createElement('h3', { className: 'vsm-panel-title' }, '命令目录(只读总览)'),
|
|
155
|
+
React.createElement('div', { className: 'vsm-panel-body' },
|
|
156
|
+
React.createElement('div', { className: 'vsm-kb-list' }, rows.map((row) => React.createElement(OfficialRow, { key: row.id, row }))),
|
|
157
|
+
),
|
|
158
|
+
),
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** 子页二:插件命令键位(行内录键 + 行级清除 + 恢复默认;写入走官方 edit API)。 */
|
|
163
|
+
function PluginShortcutEditor({ service }) {
|
|
164
|
+
const [recording, setRecording] = React.useState(null) // 正在录制的命令 id
|
|
165
|
+
const [busy, setBusy] = React.useState(false)
|
|
166
|
+
const [message, setMessage] = React.useState('')
|
|
167
|
+
const [error, setError] = React.useState('')
|
|
168
|
+
const rows = catalogRowsOf(service)
|
|
169
|
+
const config = service.config.getSnapshot()
|
|
170
|
+
|
|
171
|
+
const saveBinding = async (id, binding) => {
|
|
172
|
+
setBusy(true)
|
|
173
|
+
setError('')
|
|
174
|
+
setMessage('')
|
|
175
|
+
const buildEdit = () => (binding === null ? { type: 'reset', id } : { type: 'set', id, binding })
|
|
176
|
+
try {
|
|
177
|
+
const result = await service.edit(buildEdit(), service.config.getSnapshot().revision)
|
|
178
|
+
if (result.status === 'saved') {
|
|
179
|
+
setMessage('已保存')
|
|
180
|
+
} else if (result.status === 'stale') {
|
|
181
|
+
setError('快捷键配置已在其他窗口更新,请检查当前键位后重新录制')
|
|
182
|
+
} else if (result.status === 'conflict') {
|
|
183
|
+
// 官方按原因分类拒绝:issue = 键位本身不合法(如 web 白名单外),conflicts = 与其他命令撞键
|
|
184
|
+
if (result.issue) {
|
|
185
|
+
const reasons = {
|
|
186
|
+
'unsupported-browser': 'web 模式白名单外的组合(单修饰符组合在浏览器环境不可靠)',
|
|
187
|
+
'reserved': '官方保留键',
|
|
188
|
+
'unsupported-key': '官方不支持的键',
|
|
189
|
+
'modifier-required': '至少需要一个修饰键',
|
|
190
|
+
}
|
|
191
|
+
setError('键位被官方拒绝:' + (reasons[result.issue] ?? result.issue)
|
|
192
|
+
+ '。web 模式可用 Ctrl+Alt+X 风格组合;升级桌面外壳进入桌面模式后不受此限。')
|
|
193
|
+
} else {
|
|
194
|
+
setError('键位冲突:与 ' + (result.conflicts?.join('、') ?? '其他命令') + ' 冲突,请换一个组合')
|
|
195
|
+
}
|
|
196
|
+
} else if (result.status === 'not-ready') {
|
|
197
|
+
setError('快捷键配置尚未就绪,请稍后重试')
|
|
198
|
+
} else if (result.status === 'unreadable') {
|
|
199
|
+
setError('快捷键配置读取失败,编辑已禁用(见官方提示)')
|
|
200
|
+
} else {
|
|
201
|
+
setError('写入失败,请重试')
|
|
202
|
+
}
|
|
203
|
+
} catch (e) {
|
|
204
|
+
settingsLog.warn('快捷键保存失败:' + String(e))
|
|
205
|
+
setError('保存失败:' + String(e))
|
|
206
|
+
} finally {
|
|
207
|
+
setBusy(false)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const resetAll = async () => {
|
|
212
|
+
if (!window.confirm('恢复全部插件快捷键为默认值?(只影响本插件命令的键位覆盖)')) return
|
|
213
|
+
setBusy(true)
|
|
214
|
+
setError('')
|
|
215
|
+
setMessage('')
|
|
216
|
+
try {
|
|
217
|
+
const result = await resetPluginKeys(service)
|
|
218
|
+
if (result.failed) setError('已恢复 ' + result.reset + ' 条,' + result.failed + ' 恢复失败;请重试')
|
|
219
|
+
else setMessage('已恢复 ' + result.reset + ' 条插件快捷键')
|
|
220
|
+
} catch (e) {
|
|
221
|
+
settingsLog.warn('快捷键恢复默认失败:' + String(e))
|
|
222
|
+
setError('恢复失败:' + String(e))
|
|
223
|
+
} finally {
|
|
224
|
+
setBusy(false)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// 录制态:窗口捕获下一次组合键(Desktop 原生桥已经 recording(true) 暂停拦截)。
|
|
229
|
+
// capture 阶段 preventDefault → 官方 DOM 派发因 defaultPrevented 放行,不会误触发命令。
|
|
230
|
+
React.useEffect(() => {
|
|
231
|
+
if (!recording) return undefined
|
|
232
|
+
const onKey = (e) => {
|
|
233
|
+
e.preventDefault()
|
|
234
|
+
e.stopPropagation()
|
|
235
|
+
if (e.key === 'Escape') {
|
|
236
|
+
setRecording(null)
|
|
237
|
+
return
|
|
238
|
+
}
|
|
239
|
+
if (e.key === 'Backspace' || e.key === 'Delete') {
|
|
240
|
+
const id = recording
|
|
241
|
+
setRecording(null)
|
|
242
|
+
void saveBinding(id, null)
|
|
243
|
+
return
|
|
244
|
+
}
|
|
245
|
+
if (MODIFIER_ONLY.has(e.key)) return
|
|
246
|
+
const id = recording
|
|
247
|
+
setRecording(null)
|
|
248
|
+
void saveBinding(id, bindingFromEvent(e))
|
|
249
|
+
}
|
|
250
|
+
window.addEventListener('keydown', onKey, true)
|
|
251
|
+
return () => window.removeEventListener('keydown', onKey, true)
|
|
252
|
+
}, [recording])
|
|
253
|
+
|
|
254
|
+
// 录制期间通知 Desktop 原生桥暂停拦截(Web 适配器为空实现)
|
|
255
|
+
React.useEffect(() => {
|
|
256
|
+
if (!service || typeof service.recording !== 'function') return undefined
|
|
257
|
+
void service.recording(Boolean(recording)).catch(() => {})
|
|
258
|
+
return () => {
|
|
259
|
+
if (recording) void service.recording(false).catch(() => {})
|
|
260
|
+
}
|
|
261
|
+
}, [recording, service])
|
|
262
|
+
|
|
263
|
+
const rowsView = rows.map(({ id, label, row }) => {
|
|
264
|
+
const recordingRow = recording === id
|
|
265
|
+
const display = recordingRow
|
|
266
|
+
? React.createElement('span', { className: 'vsm-kb-rec' }, '按下组合键…(Esc 取消 / Backspace 清除)')
|
|
267
|
+
: React.createElement('kbd', { className: 'vsm-kb-key' + (row && row.conflicts.length ? ' warn' : '') }, keysText(row))
|
|
268
|
+
return React.createElement('div', { key: id, className: 'vsm-kb-row' + (recordingRow ? ' rec' : '') },
|
|
269
|
+
React.createElement('div', { className: 'vsm-kb-label' },
|
|
270
|
+
React.createElement('span', null, label),
|
|
271
|
+
row === null && React.createElement('small', { className: 'vsm-kb-warn' }, '未注册(官方服务未同步)'),
|
|
272
|
+
row && row.modified && React.createElement('small', { className: 'vsm-kb-warn' }, '已自定义'),
|
|
273
|
+
row && row.conflicts.length > 0 && React.createElement('small', { className: 'vsm-kb-warn' }, '与 ' + row.conflicts.join('、') + ' 冲突')),
|
|
274
|
+
display,
|
|
275
|
+
React.createElement('div', { className: 'vsm-kb-actions' },
|
|
276
|
+
recordingRow
|
|
277
|
+
? React.createElement('button', { onClick: () => setRecording(null) }, '取消')
|
|
278
|
+
: React.createElement(React.Fragment, null,
|
|
279
|
+
React.createElement('button', { disabled: busy || config?.status !== 'ready', onClick: () => { setRecording(id); setMessage('') } }, '编辑'),
|
|
280
|
+
React.createElement('button', { disabled: busy || config?.status !== 'ready', title: '清除该命令的键位(恢复默认)', onClick: () => void saveBinding(id, null) }, '清除'))),
|
|
281
|
+
)
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
return React.createElement('section', { className: 'vsm-panel' },
|
|
285
|
+
React.createElement('h3', { className: 'vsm-panel-title' }, '插件命令键位'),
|
|
286
|
+
React.createElement('div', { className: 'vsm-panel-body' },
|
|
287
|
+
React.createElement('div', { className: 'vsm-kb-list' }, rowsView)),
|
|
288
|
+
React.createElement('div', { className: 'vsm-panel-body vsm-kb-actionsbar' },
|
|
289
|
+
React.createElement('span', { className: 'vsm-kb-dirty' }, recording ? '录制中…' : ''),
|
|
290
|
+
message && React.createElement('span', { className: 'vsm-kb-message' }, message),
|
|
291
|
+
error && React.createElement('span', { className: 'vsm-kb-warn' }, error),
|
|
292
|
+
React.createElement('button', { className: 'vsm-small', disabled: busy || config?.status !== 'ready', onClick: () => void resetAll() }, '恢复默认')),
|
|
293
|
+
)
|
|
294
|
+
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-vscode-mode client — 编辑器指令目录(纯元数据)。
|
|
3
|
-
* 「一条命令 =
|
|
4
|
-
* 全部从本表读取;新增一条能力只需在 EDITOR_COMMANDS
|
|
3
|
+
* 「一条命令 = 一条注册数据」:命令栏、快捷键设置页与 Monaco 右键菜单
|
|
4
|
+
* 全部从本表读取;新增一条能力只需在 EDITOR_COMMANDS 追加一项(默认键位见 shared/keybindings SHORTCUT_PROFILES)。
|
|
5
5
|
* run 只派发 `edrv.command.*` 窗口事件,不直接触碰 React/Monaco(`edrv.command.` 前缀
|
|
6
6
|
* 与既有 `edrv:` 刷新/主题事件分属不同命名空间,互不干扰)。
|
|
7
7
|
* 作者 ddj 2026年09月10号
|
|
8
8
|
*/
|
|
9
9
|
import { hasEditorModel, hasOpenTabs } from '../editorModelState.js'
|
|
10
|
+
import { activeEditorPath } from '../activePathStore.js'
|
|
11
|
+
import { isMarkdownPath } from '../markdownPreview.js'
|
|
10
12
|
import { svnCurrentStatus } from '../svnStatus.js'
|
|
11
13
|
import { dapStore } from '../dap/store.js'
|
|
12
14
|
import { svnActionOn, svnActionsFor } from '../../shared/svnActions.js'
|
|
@@ -22,8 +24,6 @@ export interface CommandDef {
|
|
|
22
24
|
category: string
|
|
23
25
|
/** 组内排序(小者优先)。 */
|
|
24
26
|
order?: number
|
|
25
|
-
/** 默认键位弦(可选;可含 `|` 多候选)。声明后由快捷键设置页展示与录制。 */
|
|
26
|
-
keybinding?: string
|
|
27
27
|
/** 运行体:派发窗口事件或直接执行。 */
|
|
28
28
|
run: () => void
|
|
29
29
|
/** 可用性判定(缺省视为始终可用;返回 false 时命令栏隐藏且 run 被拒)。 */
|
|
@@ -62,7 +62,7 @@ function alwaysAvailable(): boolean {
|
|
|
62
62
|
function nextEditorRowDef(): CommandDef {
|
|
63
63
|
return {
|
|
64
64
|
id: 'edrv.nextEditorRow', label: '下一编辑行(光标整行下移)', category: '导航', order: 50,
|
|
65
|
-
|
|
65
|
+
available: needsModel,
|
|
66
66
|
run: () => emit('nextEditorRow'),
|
|
67
67
|
}
|
|
68
68
|
}
|
|
@@ -75,7 +75,7 @@ function nextEditorRowDef(): CommandDef {
|
|
|
75
75
|
function prevEditorRowDef(): CommandDef {
|
|
76
76
|
return {
|
|
77
77
|
id: 'edrv.prevEditorRow', label: '上一编辑行(光标整行上移)', category: '导航', order: 60,
|
|
78
|
-
|
|
78
|
+
available: needsModel,
|
|
79
79
|
run: () => emit('prevEditorRow'),
|
|
80
80
|
}
|
|
81
81
|
}
|
|
@@ -88,7 +88,7 @@ function prevEditorRowDef(): CommandDef {
|
|
|
88
88
|
function gotoLineDef(): CommandDef {
|
|
89
89
|
return {
|
|
90
90
|
id: 'edrv.goToLine', label: '转到行', category: '导航', order: 45,
|
|
91
|
-
|
|
91
|
+
available: needsModel,
|
|
92
92
|
run: () => emit('goToLine'),
|
|
93
93
|
}
|
|
94
94
|
}
|
|
@@ -102,7 +102,7 @@ function gotoLineDef(): CommandDef {
|
|
|
102
102
|
function addSelectionRefDef(): CommandDef {
|
|
103
103
|
return {
|
|
104
104
|
id: 'edrv.addSelectionRef', label: '添加选中内容为引用', category: '编辑', order: 10,
|
|
105
|
-
|
|
105
|
+
available: needsModel,
|
|
106
106
|
run: () => emit('addSelectionRef'),
|
|
107
107
|
}
|
|
108
108
|
}
|
|
@@ -117,7 +117,7 @@ function addSelectionRefDef(): CommandDef {
|
|
|
117
117
|
function closeTabDef(): CommandDef {
|
|
118
118
|
return {
|
|
119
119
|
id: 'edrv.closeTab', label: '关闭当前页签', category: '文件', order: 40,
|
|
120
|
-
|
|
120
|
+
available: hasOpenTabs,
|
|
121
121
|
run: () => emit('closeTab'),
|
|
122
122
|
}
|
|
123
123
|
}
|
|
@@ -188,34 +188,39 @@ function svnEventOf(action: SvnActionDef): string {
|
|
|
188
188
|
|
|
189
189
|
/**
|
|
190
190
|
* 编辑器内置指令目录(顺序 = 快捷键设置页展示顺序)。
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
191
|
+
* 键位派发已归官方 shortcuts 机制:默认键位表在 shared/keybindings.ts 的 SHORTCUT_PROFILES,
|
|
192
|
+
* 由 client/index.ts 经 shortcutsOfficial.registerOfficialShortcuts 注册,本目录只管
|
|
193
|
+
* 「标签/可用性/执行」。无默认键位的命令同样注册(官方 UI/插件子页内可自定义绑定)。
|
|
194
|
+
* @author ddj 2026年09月10号 / 2026年10月
|
|
194
195
|
*/
|
|
195
196
|
export const EDITOR_COMMANDS: readonly CommandDef[] = [
|
|
196
197
|
{
|
|
197
198
|
id: 'edrv.save', label: '保存文件', category: '文件', order: 10,
|
|
198
|
-
|
|
199
|
+
available: needsModel,
|
|
199
200
|
run: () => emit('save'),
|
|
200
201
|
},
|
|
201
202
|
{
|
|
202
203
|
id: 'edrv.quickOpen', label: '快速打开文件', category: '文件', order: 20,
|
|
203
|
-
|
|
204
|
+
available: alwaysAvailable,
|
|
204
205
|
run: () => emit('quickOpen'),
|
|
205
206
|
},
|
|
206
207
|
{
|
|
207
208
|
id: 'edrv.toggleSidebar', label: '切换侧边栏', category: '视图', order: 10,
|
|
208
|
-
|
|
209
|
+
available: alwaysAvailable,
|
|
209
210
|
run: () => emit('toggleSidebar'),
|
|
210
211
|
},
|
|
211
212
|
{
|
|
212
213
|
id: 'edrv.searchInFiles', label: '在工作区中搜索', category: '视图', order: 20,
|
|
213
|
-
|
|
214
|
+
available: alwaysAvailable,
|
|
214
215
|
run: () => emit('searchInFiles'),
|
|
215
216
|
},
|
|
216
217
|
{
|
|
217
218
|
id: 'edrv.toggleMarkdownPreview', label: '切换 Markdown 预览', category: '视图', order: 25,
|
|
218
|
-
|
|
219
|
+
// 仅活动文件是 .md 时可用(不可用时官方派发返回 pass 不吞键,保留浏览器「无格式粘贴」语义)
|
|
220
|
+
available: () => {
|
|
221
|
+
const path = activeEditorPath()
|
|
222
|
+
return typeof path === 'string' && isMarkdownPath(path)
|
|
223
|
+
},
|
|
219
224
|
run: () => emit('toggleMarkdownPreview'),
|
|
220
225
|
},
|
|
221
226
|
{
|
|
@@ -225,22 +230,22 @@ export const EDITOR_COMMANDS: readonly CommandDef[] = [
|
|
|
225
230
|
},
|
|
226
231
|
{
|
|
227
232
|
id: 'edrv.navigateBack', label: '后退(导航历史)', category: '导航', order: 10,
|
|
228
|
-
|
|
233
|
+
available: needsModel,
|
|
229
234
|
run: () => emit('navigateBack'),
|
|
230
235
|
},
|
|
231
236
|
{
|
|
232
237
|
id: 'edrv.navigateForward', label: '前进(导航历史)', category: '导航', order: 20,
|
|
233
|
-
|
|
238
|
+
available: needsModel,
|
|
234
239
|
run: () => emit('navigateForward'),
|
|
235
240
|
},
|
|
236
241
|
{
|
|
237
242
|
id: 'edrv.nextTab', label: '下一个页签', category: '导航', order: 30,
|
|
238
|
-
|
|
243
|
+
available: needsModel,
|
|
239
244
|
run: () => emit('nextTab'),
|
|
240
245
|
},
|
|
241
246
|
{
|
|
242
247
|
id: 'edrv.prevTab', label: '上一个页签', category: '导航', order: 40,
|
|
243
|
-
|
|
248
|
+
available: needsModel,
|
|
244
249
|
run: () => emit('prevTab'),
|
|
245
250
|
},
|
|
246
251
|
{
|
|
@@ -291,41 +296,41 @@ function debugPaletteDefs(): CommandDef[] {
|
|
|
291
296
|
return [
|
|
292
297
|
{
|
|
293
298
|
id: 'edrv.debugToggleBreakpoint', label: '调试:切换断点(光标行)', category: '调试', order: 10,
|
|
294
|
-
|
|
299
|
+
available: needsModel,
|
|
295
300
|
run: () => emit('debugToggleBreakpoint'),
|
|
296
301
|
},
|
|
297
302
|
{
|
|
298
303
|
id: 'edrv.debugStartContinue', label: '调试:启动 / 继续', category: '调试', order: 20,
|
|
299
|
-
|
|
304
|
+
available: canStart,
|
|
300
305
|
run: () => emit('debugStartContinue'),
|
|
301
306
|
},
|
|
302
307
|
{
|
|
303
308
|
id: 'edrv.debugStepOver', label: '调试:单步跳过', category: '调试', order: 30,
|
|
304
|
-
|
|
309
|
+
available: paused,
|
|
305
310
|
run: () => emit('debugStepOver'),
|
|
306
311
|
},
|
|
307
312
|
{
|
|
308
313
|
id: 'edrv.debugStepInto', label: '调试:单步步入', category: '调试', order: 40,
|
|
309
|
-
|
|
314
|
+
available: paused,
|
|
310
315
|
run: () => emit('debugStepInto'),
|
|
311
316
|
},
|
|
312
317
|
{
|
|
313
318
|
id: 'edrv.debugStepOut', label: '调试:单步步出', category: '调试', order: 50,
|
|
314
|
-
|
|
319
|
+
available: paused,
|
|
315
320
|
run: () => emit('debugStepOut'),
|
|
316
321
|
},
|
|
317
322
|
{
|
|
318
323
|
id: 'edrv.debugStop', label: '调试:停止', category: '调试', order: 60,
|
|
319
|
-
|
|
324
|
+
available: canStop,
|
|
320
325
|
run: () => emit('debugStop'),
|
|
321
326
|
},
|
|
322
327
|
]
|
|
323
328
|
}
|
|
324
329
|
|
|
325
330
|
/**
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
* @author ddj 2026年09月10号
|
|
331
|
+
* 非编辑器原生路径的指令(键位统一由官方 shortcuts 机制派发;历史「桥接」分组名保留)。
|
|
332
|
+
* 新增「只填目录」的编辑器指令一律放这里:命令栏与官方快捷键目录自动可用。
|
|
333
|
+
* @author ddj 2026年09月10号 / 2026年10月
|
|
329
334
|
*/
|
|
330
335
|
export const BRIDGE_COMMANDS: readonly CommandDef[] = [
|
|
331
336
|
nextEditorRowDef(),
|
|
@@ -345,16 +350,6 @@ export const BRIDGE_COMMANDS: readonly CommandDef[] = [
|
|
|
345
350
|
export function showCommandsDef(run: () => void): CommandDef {
|
|
346
351
|
return {
|
|
347
352
|
id: 'edrv.showCommands', label: '显示所有命令', category: '视图', order: 1,
|
|
348
|
-
|
|
353
|
+
available: alwaysAvailable, run,
|
|
349
354
|
}
|
|
350
355
|
}
|
|
351
|
-
|
|
352
|
-
/**
|
|
353
|
-
* 需要全局键位派发的指令(桥接类 + 命令栏自身;不含 EditorView/QuickOpen 原生监听的那些)。
|
|
354
|
-
* @author ddj 2026年09月10号
|
|
355
|
-
* @param showCommands 命令栏命令定义
|
|
356
|
-
* @returns 派发集合
|
|
357
|
-
*/
|
|
358
|
-
export function dispatchedCommands(showCommands: CommandDef): readonly CommandDef[] {
|
|
359
|
-
return [...BRIDGE_COMMANDS, showCommands]
|
|
360
|
-
}
|
package/src/fileOpenSettings.ts
CHANGED
|
@@ -407,6 +407,9 @@ export async function updateSection(provider: SettingsProvider | undefined, ns:
|
|
|
407
407
|
* 构建设置 section schema(install 路径与插件 Config 声明共用,保证两代形状同源)。
|
|
408
408
|
* 字段集 = fileOpenTool/keybindings/sidebarMinWidth/maxOpenEditors/integrationBaseUrl/
|
|
409
409
|
* aiInline/aiProvider/aiModel/aiEffort/svnPath/tortoisePath/nativeOpenExts,全部带默认值
|
|
410
|
+
* (keybindings 自 2026-10 起废弃停写:键位持久化归官方 shortcuts 存储
|
|
411
|
+
* userData/keybindings.json / dsh.keybindings.v1;字段保留仅为旧档迁移读取与降级回滚,
|
|
412
|
+
* 见 client/shortcutsOfficial.ts 的 migrateLegacyKeybindings。)
|
|
410
413
|
* (Config 启动校验在 undefined/空配置下自动填充,rc/alpha 两代 cordis 均通过)。
|
|
411
414
|
* options.volatile:DSH 0.1.7 线的 Config 导出专用——SettingsForms.describe 只下发
|
|
412
415
|
* 含 volatile 字段的 entry(volatileForm 门槛),不标记则 ns 永不出现在 describe、
|