dsh-vscode-mode 0.1.28 → 0.1.29
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/lib/client.js +827 -24
- package/lib/client.js.map +1 -1
- package/lib/index.js +441 -27
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/compat.ts +1 -1
- package/src/client/index.ts +15 -0
- package/src/client/keybindings.ts +169 -0
- package/src/client/sidebar/panels/SearchPanel.ts +294 -0
- package/src/client/sidebar/panels/index.ts +18 -2
- package/src/client/sidebar/types.ts +2 -0
- package/src/client/styles/editor.css +40 -0
- package/src/client/styles/mcp.css +18 -0
- package/src/client/ui/EditorView.ts +65 -12
- package/src/client/ui/KeybindingsPanel.ts +187 -0
- package/src/client/ui/McpSettings.ts +7 -4
- package/src/client/ui/QuickOpen.ts +6 -3
- package/src/fileOpenSettings.ts +20 -4
- package/src/index.ts +4 -1
- package/src/rpc.ts +27 -3
- package/src/search/content.ts +319 -0
- package/src/search/fallback.ts +1 -1
- package/src/search/orchestrator.ts +17 -10
- package/src/search/ripgrep.ts +17 -2
- package/src/search/types.ts +40 -0
- package/src/shared/keybindings.ts +39 -0
- package/src/shared/rpc.ts +11 -0
|
@@ -21,6 +21,7 @@ import { editorHeight } from '../editorLayout.js'
|
|
|
21
21
|
import { revealInExplorer as revealPathInExplorer } from '../fileReveal.js'
|
|
22
22
|
import { setSidePending, SIDEBAR_INSTALL_CMD } from '../sidebarBridge.js'
|
|
23
23
|
import { upsertViewState, viewStatesLoad, viewStatesSave } from '../state/viewStateCache.js'
|
|
24
|
+
import { bindingOf, chordOf, matchEvent, useKeybindingsVersion } from '../keybindings.js'
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* 中央编辑区:文件页签(脏点/关闭/打开路径)+ Ctrl+P 搜索 + Monaco 编辑器 +
|
|
@@ -61,6 +62,7 @@ export function EditorView(props) {
|
|
|
61
62
|
const [sidebarW, setSidebarW] = React.useState(240) // 侧边栏宽度
|
|
62
63
|
const [activePanel, setActivePanel] = React.useState('explorer') // 激活面板 id
|
|
63
64
|
const [focusRequest, setFocusRequest] = React.useState(0) // 外部差异聚焦请求版本
|
|
65
|
+
const kbVersion = useKeybindingsVersion() // 快捷键配置版本(tooltip 文案随键位刷新)
|
|
64
66
|
const [hintDismissed, setHintDismissed] = React.useState(() => {
|
|
65
67
|
try { return localStorage.getItem('edrv.side-hint-dismissed') === '1' } catch { return false }
|
|
66
68
|
}) // 侧边栏引导条是否已关闭
|
|
@@ -85,6 +87,7 @@ export function EditorView(props) {
|
|
|
85
87
|
const hoverPanelRef = React.useRef(false) // 鼠标是否已进入 Keep/Undo 浮层
|
|
86
88
|
const batchBusyRef = React.useRef(false) // 批量 Keep All/Undo All 防重入
|
|
87
89
|
const menuHandlersRef = React.useRef(null) // 右键菜单动作的最新闭包(Monaco addAction 空依赖回调读取)
|
|
90
|
+
const doSaveRef = React.useRef(null) // 保存动作的最新闭包(窗口级保存监听读取)
|
|
88
91
|
const diffRendererRef = React.useRef(null)
|
|
89
92
|
const layoutRef = React.useRef(layout) // ensureEditor 空依赖闭包读取的稳定布局
|
|
90
93
|
layoutRef.current = layout
|
|
@@ -392,13 +395,38 @@ export function EditorView(props) {
|
|
|
392
395
|
catch (e) { /* 忽略 */ }
|
|
393
396
|
}, [sidebarOn, sidebarW, activePanel, sessionId, sidebarKey])
|
|
394
397
|
|
|
395
|
-
//
|
|
398
|
+
// 切换侧边栏(capture 抢占,避免与 DSH 全局冲突;键位随快捷键配置)
|
|
396
399
|
React.useEffect(() => {
|
|
397
400
|
const onKey = (e) => {
|
|
398
|
-
if ((e
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
401
|
+
if (!matchEvent(e, bindingOf('edrv.toggleSidebar'))) return
|
|
402
|
+
e.preventDefault(); e.stopPropagation()
|
|
403
|
+
setSidebarOn((v) => !v)
|
|
404
|
+
}
|
|
405
|
+
window.addEventListener('keydown', onKey, true)
|
|
406
|
+
return () => window.removeEventListener('keydown', onKey, true)
|
|
407
|
+
}, [])
|
|
408
|
+
|
|
409
|
+
// 保存文件(窗口级):编辑器有活动模型才拦截执行,无文件时放行(浏览器默认行为)
|
|
410
|
+
React.useEffect(() => {
|
|
411
|
+
const onKey = (e) => {
|
|
412
|
+
if (!matchEvent(e, bindingOf('edrv.save'))) return
|
|
413
|
+
if (!editorRef.current?.getModel?.()) return
|
|
414
|
+
e.preventDefault(); e.stopPropagation()
|
|
415
|
+
flushSave()
|
|
416
|
+
doSaveRef.current(false)
|
|
417
|
+
}
|
|
418
|
+
window.addEventListener('keydown', onKey, true)
|
|
419
|
+
return () => window.removeEventListener('keydown', onKey, true)
|
|
420
|
+
}, [])
|
|
421
|
+
|
|
422
|
+
// Ctrl+Shift+F 全局搜索:展开侧边栏 + 激活搜索页签,随后聚焦搜索输入框
|
|
423
|
+
React.useEffect(() => {
|
|
424
|
+
const onKey = (e) => {
|
|
425
|
+
if (!matchEvent(e, bindingOf('edrv.searchInFiles'))) return
|
|
426
|
+
e.preventDefault(); e.stopPropagation()
|
|
427
|
+
setSidebarOn(true)
|
|
428
|
+
setActivePanel('search')
|
|
429
|
+
setTimeout(() => window.dispatchEvent(new CustomEvent('edrv:search-focus')), 0)
|
|
402
430
|
}
|
|
403
431
|
window.addEventListener('keydown', onKey, true)
|
|
404
432
|
return () => window.removeEventListener('keydown', onKey, true)
|
|
@@ -482,6 +510,7 @@ export function EditorView(props) {
|
|
|
482
510
|
} else { setStatus('保存失败'); setError(res?.error ? String(res.error) : '保存失败') }
|
|
483
511
|
}).catch((e) => { setStatus('保存失败'); setError('保存异常:' + String(e)) })
|
|
484
512
|
}
|
|
513
|
+
doSaveRef.current = doSave
|
|
485
514
|
|
|
486
515
|
const onEdit = () => {
|
|
487
516
|
const ed = editorRef.current
|
|
@@ -551,7 +580,7 @@ export function EditorView(props) {
|
|
|
551
580
|
cursorBlinking: 'smooth',
|
|
552
581
|
padding: { top: side ? 6 : 8 },
|
|
553
582
|
})
|
|
554
|
-
|
|
583
|
+
// 保存改由窗口级快捷键监听执行(键位可配置;见上方 edrv.save 监听)
|
|
555
584
|
ed.onDidChangeModelContent(() => {
|
|
556
585
|
if (!ed.getModel() || programmaticRef.current) return
|
|
557
586
|
onEdit()
|
|
@@ -634,13 +663,22 @@ export function EditorView(props) {
|
|
|
634
663
|
editorRef.current = ed
|
|
635
664
|
}, [])
|
|
636
665
|
|
|
637
|
-
//
|
|
666
|
+
// 打开文件后的跳转(差异聚焦/搜索行列跳转,内容就绪后执行一次)
|
|
638
667
|
React.useEffect(() => {
|
|
639
668
|
const pf = pendingFocusRef.current
|
|
640
669
|
if (!pf || pf.path !== active || content === null) return
|
|
641
|
-
const target = pf.region || pendingRegions[0]
|
|
642
670
|
const ed = editorRef.current
|
|
643
|
-
if (!
|
|
671
|
+
if (!ed) return
|
|
672
|
+
if (pf.line != null) {
|
|
673
|
+
// 搜索命中跳转:直接定位到行/列(不依赖差异区域)
|
|
674
|
+
pendingFocusRef.current = null
|
|
675
|
+
ed.revealLineInCenter(Math.max(1, pf.line))
|
|
676
|
+
ed.setPosition({ lineNumber: Math.max(1, pf.line), column: Math.max(1, pf.column ?? 1) })
|
|
677
|
+
ed.focus()
|
|
678
|
+
return
|
|
679
|
+
}
|
|
680
|
+
const target = pf.region || pendingRegions[0]
|
|
681
|
+
if (!target) return
|
|
644
682
|
pendingFocusRef.current = null
|
|
645
683
|
ed.revealLineInCenter(Math.max(1, target.start ?? 1))
|
|
646
684
|
ed.setPosition({ lineNumber: Math.max(1, target.start ?? 1), column: 1 })
|
|
@@ -883,6 +921,20 @@ export function EditorView(props) {
|
|
|
883
921
|
if (focusDiff) pendingFocusRef.current = { path, region: null }
|
|
884
922
|
}
|
|
885
923
|
|
|
924
|
+
/**
|
|
925
|
+
* 打开文件并跳转到指定行列(搜索面板命中跳转)。
|
|
926
|
+
* @author ddj 2026年08月26号
|
|
927
|
+
* @param path 文件路径
|
|
928
|
+
* @param line 目标行(缺省仅打开不跳转)
|
|
929
|
+
* @param column 目标列(缺省 1)
|
|
930
|
+
*/
|
|
931
|
+
const openFileAt = (path, line, column) => {
|
|
932
|
+
if (!path) return
|
|
933
|
+
addTab(path, true)
|
|
934
|
+
pendingFocusRef.current = { path, region: null, line: line ?? null, column: column ?? 1 }
|
|
935
|
+
setFocusRequest((value) => value + 1)
|
|
936
|
+
}
|
|
937
|
+
|
|
886
938
|
const openPath = () => {
|
|
887
939
|
const p = (pathDraft || '').trim()
|
|
888
940
|
if (!p) return
|
|
@@ -916,7 +968,7 @@ export function EditorView(props) {
|
|
|
916
968
|
|
|
917
969
|
const pathBar = React.createElement('div', { className: 'edrv-pathbar', title: active || '' },
|
|
918
970
|
React.createElement('span', { className: 'edrv-pb-name' }, active ? String(active).split(/[\\/]/).pop() : '未打开文件'),
|
|
919
|
-
React.createElement('span', { className: 'edrv-pb-full' }, active || '使用右上搜索框 (Ctrl+P) 打开文件'),
|
|
971
|
+
React.createElement('span', { className: 'edrv-pb-full' }, active || '使用右上搜索框 (' + (chordOf('edrv.quickOpen') ?? 'Ctrl+P') + ') 打开文件'),
|
|
920
972
|
(active && langOf(active) ? React.createElement('span', { className: 'edrv-pb-meta' }, langOf(active)) : null),
|
|
921
973
|
(cursor ? React.createElement('span', { className: 'edrv-pb-meta' }, cursor) : null),
|
|
922
974
|
(status ? React.createElement('span', { className: 'edrv-pb-meta edrv-pb-status' }, status) : null))
|
|
@@ -926,7 +978,7 @@ export function EditorView(props) {
|
|
|
926
978
|
(sum.totalFiles > 0
|
|
927
979
|
? React.createElement('button', { className: 'edrv-diffchip', title: '打开差异总览/归档', onClick: () => setLauncherOpen((o) => !o) }, '⚠ 差异 ' + sum.totalFiles + ' 文件')
|
|
928
980
|
: null),
|
|
929
|
-
React.createElement('button', { className: 'edrv-chip-btn' + (sidebarOn ? ' edrv-chip-on' : ''), title: '切换侧边栏 (Ctrl+B)', onClick: () => setSidebarOn((v) => !v) }, '☰'),
|
|
981
|
+
React.createElement('button', { className: 'edrv-chip-btn' + (sidebarOn ? ' edrv-chip-on' : ''), title: '切换侧边栏 (' + (chordOf('edrv.toggleSidebar') ?? 'Ctrl+B') + ')', onClick: () => setSidebarOn((v) => !v) }, '☰'),
|
|
930
982
|
React.createElement('button', { className: 'edrv-chip-btn', title: '刷新', onClick: reloadFile }, '⟳'),
|
|
931
983
|
React.createElement(QuickOpen, { sessionId, onOpen: (p) => openFile(p, false) }))
|
|
932
984
|
|
|
@@ -964,7 +1016,7 @@ export function EditorView(props) {
|
|
|
964
1016
|
} else if (!active) {
|
|
965
1017
|
body = React.createElement('div', { className: 'edrv-empty' },
|
|
966
1018
|
React.createElement('div', null, '暂无打开的文件'),
|
|
967
|
-
React.createElement('div', { style: { fontSize: 12 } }, '使用右上搜索框 (Ctrl+P) 打开工作区文件;agent 修改文件后顶部会出现差异角标'))
|
|
1019
|
+
React.createElement('div', { style: { fontSize: 12 } }, '使用右上搜索框 (' + (chordOf('edrv.quickOpen') ?? 'Ctrl+P') + ') 打开工作区文件;agent 修改文件后顶部会出现差异角标'))
|
|
968
1020
|
} else if (content === null && loadError) {
|
|
969
1021
|
body = loadingBody('文件加载失败:' + loadError, 0, () => {
|
|
970
1022
|
setLoadError(null)
|
|
@@ -1073,6 +1125,7 @@ export function EditorView(props) {
|
|
|
1073
1125
|
const sidebarCtx = {
|
|
1074
1126
|
sessionId,
|
|
1075
1127
|
openFile: (p) => openFile(p, false),
|
|
1128
|
+
openFileAt,
|
|
1076
1129
|
activePath: active,
|
|
1077
1130
|
pendingByPath,
|
|
1078
1131
|
sum: { totalFiles: sum.totalFiles, files: sum.files },
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* dsh-vscode-mode client — VSCodeMode 设置区「快捷键」管理页。
|
|
4
|
+
* 命令目录来自 client/keybindings 的 COMMANDS;当前键位读 SettingsContext scope;
|
|
5
|
+
* 编辑:行内录制(捕获下一次组合键),Esc 取消、Backspace/Delete 清空(未绑定);
|
|
6
|
+
* 草稿 + 显式保存(scope.set('keybindings', draft));行级恢复 + 页级「恢复默认」;
|
|
7
|
+
* 冲突键位(两命令同键位)标红警告;scope 外部变更且无未保存修改时回同步草稿。
|
|
8
|
+
* 作者 ddj 2026-08-26
|
|
9
|
+
*/
|
|
10
|
+
import React from 'react'
|
|
11
|
+
import { SettingsContext } from '../settingsContext.js'
|
|
12
|
+
import { COMMANDS, chordFromEvent, parseChord } from '../keybindings.js'
|
|
13
|
+
import { defaultKeybindings, KEYBINDING_DEFAULTS, normalizeKeybindings } from '../../shared/keybindings.js'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 草稿化:默认值 + 设置值合并,空串归一为 null(未绑定)。
|
|
17
|
+
* @param raw 设置 scope 的 keybindings 字段
|
|
18
|
+
* @returns 草稿映射(id → 键位弦或 null)
|
|
19
|
+
*/
|
|
20
|
+
export function draftOf(raw) {
|
|
21
|
+
const merged = { ...defaultKeybindings(), ...normalizeKeybindings(raw) }
|
|
22
|
+
const out = {}
|
|
23
|
+
for (const [id, chord] of Object.entries(merged)) out[id] = chord === '' ? null : chord
|
|
24
|
+
return out
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 存库化:null(未绑定)转空串。
|
|
29
|
+
* @param draft 草稿映射
|
|
30
|
+
* @returns 可存储的键位映射
|
|
31
|
+
*/
|
|
32
|
+
export function storeOf(draft) {
|
|
33
|
+
const out = {}
|
|
34
|
+
for (const [id, chord] of Object.entries(draft)) out[id] = chord === null ? '' : chord
|
|
35
|
+
return out
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 冲突检测:同一键位被多个命令占用 → 返回冲突命令 id 集合。
|
|
40
|
+
* @param draft 草稿映射
|
|
41
|
+
* @returns 冲突命令 id 集合
|
|
42
|
+
*/
|
|
43
|
+
export function conflictsOf(draft) {
|
|
44
|
+
const byChord = new Map()
|
|
45
|
+
for (const [id, chord] of Object.entries(draft)) {
|
|
46
|
+
if (!chord) continue
|
|
47
|
+
const list = byChord.get(chord) ?? []
|
|
48
|
+
list.push(id)
|
|
49
|
+
byChord.set(chord, list)
|
|
50
|
+
}
|
|
51
|
+
const out = new Set()
|
|
52
|
+
for (const list of byChord.values()) {
|
|
53
|
+
if (list.length > 1) for (const id of list) out.add(id)
|
|
54
|
+
}
|
|
55
|
+
return out
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 快捷键管理页主体。
|
|
60
|
+
*/
|
|
61
|
+
export function KeybindingsPanel() {
|
|
62
|
+
const settings = React.useContext(SettingsContext)
|
|
63
|
+
const snapshot = settings?.getSnapshot?.()
|
|
64
|
+
const unavailable = !settings || snapshot?.status === 'unavailable'
|
|
65
|
+
const notReady = snapshot?.status !== 'ready'
|
|
66
|
+
const [draft, setDraft] = React.useState(() => draftOf(snapshot?.value?.keybindings))
|
|
67
|
+
const [recording, setRecording] = React.useState(null) // 正在录制的命令 id
|
|
68
|
+
const [dirty, setDirty] = React.useState(false)
|
|
69
|
+
const [busy, setBusy] = React.useState(false)
|
|
70
|
+
const [error, setError] = React.useState('')
|
|
71
|
+
const [message, setMessage] = React.useState('')
|
|
72
|
+
const dirtyRef = React.useRef(false)
|
|
73
|
+
dirtyRef.current = dirty
|
|
74
|
+
const lastKeyRef = React.useRef(JSON.stringify(draft)) // 草稿内容指纹(内容未变不 setState,防外部快照引用不稳导致循环)
|
|
75
|
+
const snapshotValue = snapshot?.value?.keybindings
|
|
76
|
+
|
|
77
|
+
// scope 外部变更(如重置/其他端写入)且无未保存修改时回同步草稿
|
|
78
|
+
React.useEffect(() => {
|
|
79
|
+
if (dirtyRef.current) return
|
|
80
|
+
const next = draftOf(snapshotValue)
|
|
81
|
+
const key = JSON.stringify(next)
|
|
82
|
+
if (key === lastKeyRef.current) return
|
|
83
|
+
lastKeyRef.current = key
|
|
84
|
+
setDraft(next)
|
|
85
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
86
|
+
}, [snapshotValue])
|
|
87
|
+
|
|
88
|
+
// 录制态:窗口捕获下一次组合键(preventDefault 防浏览器默认动作)
|
|
89
|
+
React.useEffect(() => {
|
|
90
|
+
if (!recording) return
|
|
91
|
+
const onKey = (e) => {
|
|
92
|
+
e.preventDefault()
|
|
93
|
+
e.stopPropagation()
|
|
94
|
+
if (e.key === 'Escape') { setRecording(null); return }
|
|
95
|
+
if (e.key === 'Backspace' || e.key === 'Delete') {
|
|
96
|
+
setDraft((prev) => Object.assign({}, prev, { [recording]: null }))
|
|
97
|
+
setRecording(null)
|
|
98
|
+
setDirty(true)
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
const chord = chordFromEvent(e)
|
|
102
|
+
if (!chord) return
|
|
103
|
+
setDraft((prev) => Object.assign({}, prev, { [recording]: chord }))
|
|
104
|
+
setRecording(null)
|
|
105
|
+
setDirty(true)
|
|
106
|
+
}
|
|
107
|
+
window.addEventListener('keydown', onKey, true)
|
|
108
|
+
return () => window.removeEventListener('keydown', onKey, true)
|
|
109
|
+
}, [recording])
|
|
110
|
+
|
|
111
|
+
const conflicts = conflictsOf(draft)
|
|
112
|
+
const save = () => {
|
|
113
|
+
if (!settings?.set) { setError('设置服务不可用'); return }
|
|
114
|
+
setBusy(true)
|
|
115
|
+
setError('')
|
|
116
|
+
setMessage('')
|
|
117
|
+
settings.set('keybindings', storeOf(draft)).then(() => {
|
|
118
|
+
setDirty(false)
|
|
119
|
+
lastKeyRef.current = JSON.stringify(draft)
|
|
120
|
+
setMessage('已保存')
|
|
121
|
+
}).catch((e) => setError(String(e))).finally(() => setBusy(false))
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 恢复全部默认(确认后直接保存,一步到位)。
|
|
126
|
+
*/
|
|
127
|
+
const resetAll = () => {
|
|
128
|
+
if (!settings?.set) { setError('设置服务不可用'); return }
|
|
129
|
+
if (!window.confirm('恢复全部快捷键为默认值?')) return
|
|
130
|
+
setBusy(true)
|
|
131
|
+
setError('')
|
|
132
|
+
setMessage('')
|
|
133
|
+
settings.set('keybindings', { ...defaultKeybindings() }).then(() => {
|
|
134
|
+
const next = draftOf({})
|
|
135
|
+
setDraft(next)
|
|
136
|
+
lastKeyRef.current = JSON.stringify(next)
|
|
137
|
+
setDirty(false)
|
|
138
|
+
setMessage('已恢复默认')
|
|
139
|
+
}).catch((e) => setError(String(e))).finally(() => setBusy(false))
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** 行级恢复该项默认值(进草稿,随保存提交)。 */
|
|
143
|
+
const resetRow = (id) => {
|
|
144
|
+
setDraft((prev) => Object.assign({}, prev, { [id]: KEYBINDING_DEFAULTS[id] ?? null }))
|
|
145
|
+
setDirty(true)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const rows = COMMANDS.map((command) => {
|
|
149
|
+
const chord = draft[command.id] ?? null
|
|
150
|
+
const conflict = chord !== null && conflicts.has(command.id)
|
|
151
|
+
const invalid = chord !== null && parseChord(chord) === null
|
|
152
|
+
const recordingRow = recording === command.id
|
|
153
|
+
let display
|
|
154
|
+
if (recordingRow) {
|
|
155
|
+
display = React.createElement('span', { className: 'vsm-kb-rec' }, '按下组合键…(Esc 取消 / Backspace 清除)')
|
|
156
|
+
} else {
|
|
157
|
+
display = React.createElement('kbd', { className: 'vsm-kb-key' + (conflict ? ' warn' : '') + (invalid ? ' invalid' : '') },
|
|
158
|
+
chord ?? '未绑定')
|
|
159
|
+
}
|
|
160
|
+
return React.createElement('div', { key: command.id, className: 'vsm-kb-row' + (recordingRow ? ' rec' : '') },
|
|
161
|
+
React.createElement('div', { className: 'vsm-kb-label' },
|
|
162
|
+
React.createElement('span', null, command.label),
|
|
163
|
+
conflict && React.createElement('small', { className: 'vsm-kb-warn' }, '与另一命令冲突'),
|
|
164
|
+
invalid && React.createElement('small', { className: 'vsm-kb-warn' }, '键位格式无效')),
|
|
165
|
+
display,
|
|
166
|
+
React.createElement('div', { className: 'vsm-kb-actions' },
|
|
167
|
+
recordingRow
|
|
168
|
+
? React.createElement('button', { onClick: () => setRecording(null) }, '取消')
|
|
169
|
+
: React.createElement(React.Fragment, null,
|
|
170
|
+
React.createElement('button', { disabled: unavailable || notReady || busy || snapshot?.writable === false, onClick: () => { setRecording(command.id); setMessage('') } }, '编辑'),
|
|
171
|
+
React.createElement('button', { disabled: busy, onClick: () => resetRow(command.id) }, '恢复')),
|
|
172
|
+
))
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
return React.createElement('section', { className: 'vsm-general-page' },
|
|
176
|
+
React.createElement('h2', null, '快捷键'),
|
|
177
|
+
React.createElement('p', null, '配置编辑器视图的快捷键(保存 / 快速打开 / 侧边栏 / 搜索)。修改即时写入配置,重新聚焦编辑器后生效。'),
|
|
178
|
+
unavailable && React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, '设置服务暂不可用,当前使用默认键位。'),
|
|
179
|
+
error && React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, error),
|
|
180
|
+
message && React.createElement('div', { className: 'vsm-kb-message' }, message),
|
|
181
|
+
React.createElement('div', { className: 'vsm-kb-list' }, rows),
|
|
182
|
+
React.createElement('div', { className: 'vsm-kb-actionsbar' },
|
|
183
|
+
React.createElement('span', { className: 'vsm-kb-dirty' }, dirty ? '有未保存的修改' : ''),
|
|
184
|
+
React.createElement('button', { className: 'vsm-primary vsm-small', disabled: unavailable || notReady || busy || snapshot?.writable === false || !dirty, onClick: save }, busy ? '保存中…' : '保存'),
|
|
185
|
+
React.createElement('button', { className: 'vsm-small', disabled: unavailable || notReady || busy || snapshot?.writable === false, onClick: resetAll }, '恢复默认')),
|
|
186
|
+
)
|
|
187
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
2
|
/**
|
|
3
|
-
* dsh-vscode-mode client — VSCodeMode
|
|
4
|
-
* 子 Tab
|
|
5
|
-
*
|
|
3
|
+
* dsh-vscode-mode client — VSCodeMode 设置区:通用 / 快捷键 / MCP 管理。
|
|
4
|
+
* 子 Tab:通用(文件打开工具/开发形态)/ 快捷键(编辑器键位配置,可重置)/
|
|
5
|
+
* 我的 MCP(profile 全局)/ 项目 MCP(各项目 .mcp.json)/ MCP 市场(占位)/ 兼容性。
|
|
6
|
+
* 作者 ddj 2026年08月22号 / 2026年08月26号
|
|
6
7
|
*/
|
|
7
8
|
import React from 'react'
|
|
8
9
|
import { rpc } from '../rpc.js'
|
|
@@ -10,6 +11,7 @@ import type { MpcProject, MpcServer } from '../../shared/mcp.js'
|
|
|
10
11
|
import '../styles/mcp.css'
|
|
11
12
|
import { availableOpeners, AUTO_OPEN_TOOL } from '../fileOpeners.js'
|
|
12
13
|
import { SettingsContext } from '../settingsContext.js'
|
|
14
|
+
import { KeybindingsPanel } from './KeybindingsPanel.js'
|
|
13
15
|
|
|
14
16
|
const EMPTY = { serverName: '', transport: 'stdio', command: '', args: '', cwd: '', url: '', headers: '' }
|
|
15
17
|
|
|
@@ -315,6 +317,7 @@ export function McpSettings(props) {
|
|
|
315
317
|
let body
|
|
316
318
|
if (loading) body = React.createElement('div', { className: 'vsm-mcp-empty' }, '正在读取 MCP 服务…')
|
|
317
319
|
else if (tab === 'general') body = React.createElement(GeneralSettings, { registry: openerRegistry })
|
|
320
|
+
else if (tab === 'keys') body = React.createElement(KeybindingsPanel, null)
|
|
318
321
|
else if (tab === 'mine') body = React.createElement('div', null, servers.length ? servers.map((s) => React.createElement(ServerCard, { key: s.id, server: s, onRefresh: refreshGlobal, onToggle: toggleGlobal, onRemove: removeGlobal })) : React.createElement('div', { className: 'vsm-mcp-empty' }, '还没有配置全局 MCP'))
|
|
319
322
|
else if (tab === 'projects') body = projectBody
|
|
320
323
|
else if (tab === 'compat') body = React.createElement(CompatSection, { getSummary: compatSummary })
|
|
@@ -322,7 +325,7 @@ export function McpSettings(props) {
|
|
|
322
325
|
const form = showForm ? React.createElement(McpForm, { title: '添加全局 MCP', draft, busy, edit, save: saveGlobal, close: () => setShowForm(false) }) : projectForm ? React.createElement(McpForm, { title: '添加项目 MCP · ' + projectForm.title, draft, busy, edit, save: saveProject, close: () => setProjectForm(null) }) : null
|
|
323
326
|
return React.createElement('section', { className: 'vsm-mcp-page' },
|
|
324
327
|
React.createElement('header', { className: 'vsm-mcp-header' }, React.createElement('div', null, React.createElement('h2', null, 'VSCodeMode'), React.createElement('p', null, '管理当前 profile 与各项目的 Model Context Protocol 服务。')), React.createElement('button', { className: 'vsm-primary', onClick: () => { setDraft(EMPTY); setShowForm(true) } }, '+ 添加全局 MCP')),
|
|
325
|
-
React.createElement('nav', { className: 'vsm-mcp-tabs' }, React.createElement('button', { className: tab === 'general' ? 'active' : '', onClick: () => setTab('general') }, '通用'), React.createElement('button', { className: tab === 'mine' ? 'active' : '', onClick: () => setTab('mine') }, '我的 MCP'), React.createElement('button', { className: tab === 'projects' ? 'active' : '', onClick: () => setTab('projects') }, '项目 MCP'), React.createElement('button', { className: tab === 'market' ? 'active' : '', onClick: () => setTab('market') }, 'MCP 市场'), React.createElement('button', { className: tab === 'compat' ? 'active' : '', onClick: () => setTab('compat') }, '兼容性')),
|
|
328
|
+
React.createElement('nav', { className: 'vsm-mcp-tabs' }, React.createElement('button', { className: tab === 'general' ? 'active' : '', onClick: () => setTab('general') }, '通用'), React.createElement('button', { className: tab === 'keys' ? 'active' : '', onClick: () => setTab('keys') }, '快捷键'), React.createElement('button', { className: tab === 'mine' ? 'active' : '', onClick: () => setTab('mine') }, '我的 MCP'), React.createElement('button', { className: tab === 'projects' ? 'active' : '', onClick: () => setTab('projects') }, '项目 MCP'), React.createElement('button', { className: tab === 'market' ? 'active' : '', onClick: () => setTab('market') }, 'MCP 市场'), React.createElement('button', { className: tab === 'compat' ? 'active' : '', onClick: () => setTab('compat') }, '兼容性')),
|
|
326
329
|
error && React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, error),
|
|
327
330
|
body,
|
|
328
331
|
form,
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
2
|
/**
|
|
3
3
|
* dsh-vscode-mode client — QuickOpen:顶部搜索框(Ctrl+P 打开文件)。
|
|
4
|
+
* 快捷键随配置(edrv.quickOpen),占位文案同步当前键位。
|
|
4
5
|
* 迁移自原 src/client/index.ts 的 QuickOpen,语义不改。
|
|
5
|
-
* 作者 ddj 2026-08-20
|
|
6
|
+
* 作者 ddj 2026-08-20 / 2026-08-26
|
|
6
7
|
*/
|
|
7
8
|
import React from 'react'
|
|
8
9
|
import { rpc } from '../rpc.js'
|
|
10
|
+
import { bindingOf, chordOf, matchEvent, useKeybindingsVersion } from '../keybindings.js'
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* 快速打开搜索框:Ctrl+P 聚焦,输入 ≥2 字符经 edrv.searchFiles 搜索,
|
|
@@ -22,6 +24,7 @@ export function QuickOpen(props) {
|
|
|
22
24
|
const inputRef = React.useRef(null)
|
|
23
25
|
const seq = React.useRef(0)
|
|
24
26
|
const timer = React.useRef(null)
|
|
27
|
+
useKeybindingsVersion() // 键位变化时刷新占位文案
|
|
25
28
|
|
|
26
29
|
const search = (query) => {
|
|
27
30
|
const s = ++seq.current
|
|
@@ -47,7 +50,7 @@ export function QuickOpen(props) {
|
|
|
47
50
|
|
|
48
51
|
React.useEffect(() => {
|
|
49
52
|
const onKey = (e) => {
|
|
50
|
-
if ((e
|
|
53
|
+
if (matchEvent(e, bindingOf('edrv.quickOpen'))) {
|
|
51
54
|
e.preventDefault(); e.stopPropagation()
|
|
52
55
|
inputRef.current?.focus?.()
|
|
53
56
|
setOpen(true)
|
|
@@ -78,7 +81,7 @@ export function QuickOpen(props) {
|
|
|
78
81
|
|
|
79
82
|
return React.createElement('div', { className: 'edrv-search-wrap' },
|
|
80
83
|
React.createElement('input', {
|
|
81
|
-
ref: inputRef, className: 'edrv-search', placeholder: '搜索文件 (Ctrl+P)',
|
|
84
|
+
ref: inputRef, className: 'edrv-search', placeholder: '搜索文件 (' + (chordOf('edrv.quickOpen') ?? 'Ctrl+P') + ')',
|
|
82
85
|
value: q,
|
|
83
86
|
onChange: (e) => onChange(e.target.value),
|
|
84
87
|
onFocus: () => { if (q.trim().length >= 2 && !results) setOpen(true) },
|
package/src/fileOpenSettings.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* dsh-vscode-mode host —
|
|
2
|
+
* dsh-vscode-mode host — 文件链接打开工具 + 快捷键的持久化设置。
|
|
3
3
|
* 依赖守卫:@deepseek-ai/dsh-settings / schemastery 以动态加载引入,
|
|
4
4
|
* 缺失时插件仍可加载(fileOpenTool 降级为配置值,compat 报告可见)。
|
|
5
|
-
* 作者 ddj 2026年08月24号
|
|
5
|
+
* 作者 ddj 2026年08月24号 / 2026年08月26号
|
|
6
6
|
*/
|
|
7
7
|
import type { Ctx } from './store.js'
|
|
8
|
+
import { KEYBINDING_DEFAULTS } from './shared/keybindings.js'
|
|
8
9
|
|
|
9
10
|
export const FILE_OPEN_SETTINGS_NS = 'dsh-vscode-mode'
|
|
10
11
|
export const FILE_OPEN_DEFAULT = 'auto'
|
|
@@ -21,7 +22,7 @@ type SettingsProvider = {
|
|
|
21
22
|
export interface SettingsDeps {
|
|
22
23
|
installSettingsSection: (ctx: Ctx, ns: string, schema: unknown, entry: object, hooks: object) => void
|
|
23
24
|
z: {
|
|
24
|
-
object: (shape: Record<string, unknown>) => unknown
|
|
25
|
+
object: (shape: Record<string, unknown>) => { default: (value: Record<string, unknown>) => unknown }
|
|
25
26
|
string: () => { default: (value: string) => unknown }
|
|
26
27
|
}
|
|
27
28
|
}
|
|
@@ -30,6 +31,18 @@ export type SettingsDepsLoader = () => Promise<SettingsDeps | null>
|
|
|
30
31
|
|
|
31
32
|
let depsPromise: Promise<SettingsDeps | null> | undefined
|
|
32
33
|
|
|
34
|
+
/**
|
|
35
|
+
* 按命令目录构建 keybindings 显式键形状(每键独立默认值)。
|
|
36
|
+
* @author ddj 2026年08月26号
|
|
37
|
+
* @param z schemastery 命名空间
|
|
38
|
+
* @returns keybindings 字段形状
|
|
39
|
+
*/
|
|
40
|
+
function keybindingsShape(z: SettingsDeps['z']): Record<string, unknown> {
|
|
41
|
+
const shape: Record<string, unknown> = {}
|
|
42
|
+
for (const [id, chord] of Object.entries(KEYBINDING_DEFAULTS)) shape[id] = z.string().default(chord)
|
|
43
|
+
return shape
|
|
44
|
+
}
|
|
45
|
+
|
|
33
46
|
/**
|
|
34
47
|
* 动态加载设置依赖(模块级缓存;任一缺失返回 null 而非抛错)。
|
|
35
48
|
* @author ddj 2026年08月24号
|
|
@@ -75,7 +88,10 @@ export async function installOpenSettingsSection(
|
|
|
75
88
|
): Promise<boolean> {
|
|
76
89
|
const deps = await loader()
|
|
77
90
|
if (!deps) return false
|
|
78
|
-
const schema = deps.z.object({
|
|
91
|
+
const schema = deps.z.object({
|
|
92
|
+
fileOpenTool: deps.z.string().default(FILE_OPEN_DEFAULT),
|
|
93
|
+
keybindings: deps.z.object(keybindingsShape(deps.z)).default({ ...KEYBINDING_DEFAULTS }),
|
|
94
|
+
})
|
|
79
95
|
deps.installSettingsSection(ctx, ns, schema, entry, hooks)
|
|
80
96
|
return true
|
|
81
97
|
}
|
package/src/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { registerRoutes } from './routes.js'
|
|
|
10
10
|
import { captureToolResult } from './capture.js'
|
|
11
11
|
import { handleRpc } from './rpc.js'
|
|
12
12
|
import { newSearcher } from './search/orchestrator.js'
|
|
13
|
+
import { newContentSearcher } from './search/content.js'
|
|
13
14
|
import { installIsolation } from './mcpIsolation.js'
|
|
14
15
|
import { dropFileIndex } from './workspace.js'
|
|
15
16
|
import { cwdOf } from './registry.js'
|
|
@@ -30,6 +31,7 @@ export const inject = ['sessions', 'fs', 'webServer', 'loader', 'tools', 'worksp
|
|
|
30
31
|
export function apply(ctx: Ctx, config?: unknown): void {
|
|
31
32
|
const registry: Registry = new Map()
|
|
32
33
|
const searcher = newSearcher(ctx)
|
|
34
|
+
const contentSearcher = newContentSearcher(ctx)
|
|
33
35
|
/** 兼容性警告收集(route 护栏等写入,启动日志一并输出)。 */
|
|
34
36
|
const warnings: string[] = []
|
|
35
37
|
setupOpenSettings(ctx, config, () => {})
|
|
@@ -44,10 +46,11 @@ export function apply(ctx: Ctx, config?: unknown): void {
|
|
|
44
46
|
registry.delete(cwd)
|
|
45
47
|
dropFileIndex(cwd)
|
|
46
48
|
searcher.dispose(cwd)
|
|
49
|
+
contentSearcher.dispose(cwd)
|
|
47
50
|
}
|
|
48
51
|
})
|
|
49
52
|
|
|
50
|
-
registerRoutes(ctx, config, (method, args) => handleRpc(ctx, registry, method, args, searcher), (warning) => warnings.push(warning))
|
|
53
|
+
registerRoutes(ctx, config, (method, args) => handleRpc(ctx, registry, method, args, searcher, contentSearcher), (warning) => warnings.push(warning))
|
|
51
54
|
installIsolation(ctx)
|
|
52
55
|
void logCompatSummary(ctx, warnings)
|
|
53
56
|
|
package/src/rpc.ts
CHANGED
|
@@ -23,6 +23,8 @@ import type { Registry } from './registry.js'
|
|
|
23
23
|
import { bucketOf, cwdOf, sessionOf } from './registry.js'
|
|
24
24
|
import type { SearchOrchestrator } from './search/orchestrator.js'
|
|
25
25
|
import { newSearcher } from './search/orchestrator.js'
|
|
26
|
+
import type { ContentSearcher } from './search/content.js'
|
|
27
|
+
import { newContentSearcher } from './search/content.js'
|
|
26
28
|
import { restoreFile, revertCall, revertHunk } from './revert.js'
|
|
27
29
|
import { listMcp, refreshMcp, removeMcp, saveMcp, toggleMcp } from './mcp.js'
|
|
28
30
|
import { listProjects, projectRefresh, projectRemove, projectSave, projectToggle } from './mcpProject.js'
|
|
@@ -117,7 +119,7 @@ async function applyDecisions(
|
|
|
117
119
|
}
|
|
118
120
|
|
|
119
121
|
/** 各方法 handler 表(类型由 shared/rpc 的 RpcHandlerMap 约束)。 */
|
|
120
|
-
export function buildHandlers(ctx: Ctx, registry: Registry, searcher = newSearcher(ctx)): RpcHandlerMap {
|
|
122
|
+
export function buildHandlers(ctx: Ctx, registry: Registry, searcher = newSearcher(ctx), contentSearcher = newContentSearcher(ctx)): RpcHandlerMap {
|
|
121
123
|
return {
|
|
122
124
|
'edrv.list': async (args) => {
|
|
123
125
|
const sc = await requireSession(ctx, args.sessionId)
|
|
@@ -323,6 +325,27 @@ export function buildHandlers(ctx: Ctx, registry: Registry, searcher = newSearch
|
|
|
323
325
|
const result = await searcher.search({ session: sc.session, cwd: sc.cwd, query: args.query, activePaths })
|
|
324
326
|
return { ok: true, ...result }
|
|
325
327
|
},
|
|
328
|
+
'edrv.searchContent': async (args) => {
|
|
329
|
+
// 工作区内容搜索:rg --json 主路径;provider 失败转错误响应(无 fallback)。
|
|
330
|
+
const sc = await requireSession(ctx, args.sessionId)
|
|
331
|
+
if ('err' in sc) return { ok: false, error: sc.err }
|
|
332
|
+
try {
|
|
333
|
+
const result = await contentSearcher.search({
|
|
334
|
+
session: sc.session,
|
|
335
|
+
cwd: sc.cwd,
|
|
336
|
+
query: args.query,
|
|
337
|
+
matchCase: args.matchCase,
|
|
338
|
+
wholeWord: args.wholeWord,
|
|
339
|
+
regex: args.regex,
|
|
340
|
+
maxResults: args.maxResults,
|
|
341
|
+
include: args.include,
|
|
342
|
+
exclude: args.exclude,
|
|
343
|
+
})
|
|
344
|
+
return { ok: true, ...result }
|
|
345
|
+
} catch (error) {
|
|
346
|
+
return { ok: false, error: '搜索失败:' + String(error) }
|
|
347
|
+
}
|
|
348
|
+
},
|
|
326
349
|
'edrv.listDir': async (args) => {
|
|
327
350
|
// 目录树(侧边栏文件管理用):按会话 cwd 解析,与 edrv.read 同基准,树内相对路径点开即打开。
|
|
328
351
|
const sc = await requireSession(ctx, args.sessionId)
|
|
@@ -433,7 +456,7 @@ export function buildHandlers(ctx: Ctx, registry: Registry, searcher = newSearch
|
|
|
433
456
|
|
|
434
457
|
/**
|
|
435
458
|
* 统一入口:按方法分发到 handler 表。
|
|
436
|
-
* @author ddj 2026年08月20号
|
|
459
|
+
* @author ddj 2026年08月20号 / 2026年08月26号
|
|
437
460
|
*/
|
|
438
461
|
export async function handleRpc<M extends RpcMethod>(
|
|
439
462
|
ctx: Ctx,
|
|
@@ -441,8 +464,9 @@ export async function handleRpc<M extends RpcMethod>(
|
|
|
441
464
|
method: M,
|
|
442
465
|
args: RpcRequestMap[M],
|
|
443
466
|
searcher = newSearcher(ctx),
|
|
467
|
+
contentSearcher = newContentSearcher(ctx),
|
|
444
468
|
): Promise<RpcResult<M>> {
|
|
445
|
-
const handlers = buildHandlers(ctx, registry, searcher)
|
|
469
|
+
const handlers = buildHandlers(ctx, registry, searcher, contentSearcher)
|
|
446
470
|
const handler = handlers[method]
|
|
447
471
|
if (!handler) return { ok: false, error: '未知方法: ' + String(method) } as RpcResult<M>
|
|
448
472
|
return handler(args)
|