dsh-vscode-mode 0.1.28 → 0.1.30
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 +859 -27
- package/lib/client.js.map +1 -1
- package/lib/index.js +513 -41
- 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/monaco/diffRender.ts +7 -0
- package/src/client/rpc.ts +13 -2
- 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 +76 -15
- 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 +106 -22
- 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
|
@@ -243,3 +243,43 @@
|
|
|
243
243
|
[data-edrv-view] .edrv-side-hint-close { margin-left: auto; border: none; background: transparent; color: var(--dsw-alias-label-tertiary, #9aa5b1); cursor: pointer; font-size: 14px; line-height: 1; padding: 2px 6px; border-radius: 6px; flex-shrink: 0; }
|
|
244
244
|
[data-edrv-view] .edrv-side-hint-close:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(15,157,88,.08)); color: var(--dsw-alias-label-primary, #1f2933); }
|
|
245
245
|
|
|
246
|
+
/* ===== search panel (sidebar, VSCode search view) ===== */
|
|
247
|
+
[data-edrv-view] .edrv-search-panel { display: flex; flex-direction: column; min-height: 0; flex: 1; padding: 6px; gap: 4px; }
|
|
248
|
+
[data-edrv-view] .edrv-search-qrow { display: flex; gap: 4px; }
|
|
249
|
+
[data-edrv-view] .edrv-search-input { flex: 1; min-width: 0; height: 26px; padding: 0 8px; border: 1px solid var(--dsw-alias-border-l2, #555); border-radius: 6px; background: var(--dsw-alias-bg-base, #111); color: var(--dsw-alias-label-primary, #eee); font-size: 12px; outline: none; }
|
|
250
|
+
[data-edrv-view] .edrv-search-input:focus { border-color: var(--dsw-alias-brand-primary, #4f8cff); }
|
|
251
|
+
[data-edrv-view] .edrv-search-opts { display: flex; align-items: center; gap: 2px; flex: 0 0 auto; }
|
|
252
|
+
[data-edrv-view] .edrv-search-opt { min-width: 24px; height: 22px; padding: 0 6px; border: 1px solid transparent; border-radius: 5px; background: transparent; color: var(--dsw-alias-label-secondary, #52606d); font-size: 11px; font-family: var(--ds-font-family-code, ui-monospace, monospace); cursor: pointer; }
|
|
253
|
+
[data-edrv-view] .edrv-search-opt:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(15,157,88,.08)); }
|
|
254
|
+
[data-edrv-view] .edrv-search-opt.on { background: var(--dsw-alias-interactive-bg-hover, rgba(15,157,88,.14)); color: var(--dsw-alias-label-primary, #1f2933); border-color: var(--dsw-alias-border-l2, #d5dde3); }
|
|
255
|
+
[data-edrv-view] .edrv-search-spacer { flex: 1; }
|
|
256
|
+
[data-edrv-view] .edrv-search-summary { font-size: 11px; color: var(--dsw-alias-label-tertiary, #9aa5b1); padding: 2px 4px; }
|
|
257
|
+
[data-edrv-view] .edrv-search-results { flex: 1; min-height: 0; overflow: auto; font-size: 12px; }
|
|
258
|
+
[data-edrv-view] .edrv-search-group { margin-bottom: 2px; }
|
|
259
|
+
[data-edrv-view] .edrv-search-file { display: flex; align-items: center; gap: 4px; height: 22px; padding: 0 4px; border-radius: 5px; color: var(--dsw-alias-label-primary, #1f2933); cursor: pointer; white-space: nowrap; }
|
|
260
|
+
[data-edrv-view] .edrv-search-file:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(15,157,88,.08)); }
|
|
261
|
+
[data-edrv-view] .edrv-search-chev { width: 12px; flex-shrink: 0; color: var(--dsw-alias-label-tertiary, #9aa5b1); font-size: 10px; text-align: center; }
|
|
262
|
+
[data-edrv-view] .edrv-search-fname { font-weight: 600; overflow: hidden; text-overflow: ellipsis; }
|
|
263
|
+
[data-edrv-view] .edrv-search-fdir { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; color: var(--dsw-alias-label-tertiary, #9aa5b1); font-size: 11px; }
|
|
264
|
+
[data-edrv-view] .edrv-search-fcount { flex-shrink: 0; min-width: 16px; height: 15px; padding: 0 4px; border-radius: 999px; background: var(--dsw-alias-bg-layer-2, #f0f4f4); color: var(--dsw-alias-label-secondary, #52606d); font-size: 10px; line-height: 15px; text-align: center; }
|
|
265
|
+
[data-edrv-view] .edrv-search-line { display: flex; gap: 6px; padding: 1px 4px 1px 16px; border-radius: 4px; color: var(--dsw-alias-label-primary, #1f2933); cursor: pointer; overflow: hidden; }
|
|
266
|
+
[data-edrv-view] .edrv-search-line.alt { background: var(--dsw-alias-bg-layer-1, #ffffff); }
|
|
267
|
+
[data-edrv-view] .edrv-search-line:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(15,157,88,.08)); }
|
|
268
|
+
[data-edrv-view] .edrv-search-ln { flex-shrink: 0; color: var(--dsw-alias-label-tertiary, #9aa5b1); font-size: 11px; min-width: 26px; text-align: right; user-select: none; }
|
|
269
|
+
[data-edrv-view] .edrv-search-text { overflow: hidden; text-overflow: ellipsis; white-space: pre; font-family: var(--ds-font-family-code, ui-monospace, monospace); font-size: 11px; }
|
|
270
|
+
[data-edrv-view] .edrv-search-hit { background: var(--dsw-alias-state-warn-primary, #e5b93f); color: #1f2933; border-radius: 2px; padding: 0 1px; }
|
|
271
|
+
[data-edrv-view] .edrv-search-empty { padding: 8px 4px; font-size: 12px; color: var(--dsw-alias-label-tertiary, #9aa5b1); }
|
|
272
|
+
[data-edrv-view] .edrv-search-error { padding: 6px 8px; border: 1px solid rgba(217,83,79,.4); border-radius: 6px; background: #fdeaea; color: var(--dsw-alias-state-error-primary, #d9534f); font-size: 11px; }
|
|
273
|
+
|
|
274
|
+
/* include/exclude filter rows (collapsible) */
|
|
275
|
+
[data-edrv-view] .edrv-search-rows { display: flex; flex-direction: column; gap: 4px; padding: 2px 0 4px; }
|
|
276
|
+
[data-edrv-view] .edrv-search-row { display: flex; align-items: center; gap: 4px; }
|
|
277
|
+
[data-edrv-view] .edrv-search-field { flex: 1; min-width: 0; height: 24px; padding: 0 8px; border: 1px solid var(--dsw-alias-border-l2, #555); border-radius: 6px; background: var(--dsw-alias-bg-base, #111); color: var(--dsw-alias-label-primary, #eee); font-size: 11px; outline: none; }
|
|
278
|
+
[data-edrv-view] .edrv-search-field:focus { border-color: var(--dsw-alias-brand-primary, #4f8cff); }
|
|
279
|
+
[data-edrv-view] .edrv-search-field.dim { opacity: .45; }
|
|
280
|
+
[data-edrv-view] .edrv-search-field::placeholder { color: var(--dsw-alias-label-tertiary, #9aa5b1); }
|
|
281
|
+
[data-edrv-view] .edrv-search-toggle { min-width: 24px; height: 24px; padding: 0 5px; border: 1px solid var(--dsw-alias-border-l2, #555); border-radius: 6px; background: transparent; color: var(--dsw-alias-label-secondary, #52606d); font-size: 12px; line-height: 1; cursor: pointer; flex-shrink: 0; }
|
|
282
|
+
[data-edrv-view] .edrv-search-toggle:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover, rgba(15,157,88,.08)); }
|
|
283
|
+
[data-edrv-view] .edrv-search-toggle.on { background: var(--dsw-alias-interactive-bg-hover, rgba(15,157,88,.14)); color: var(--dsw-alias-label-primary, #1f2933); border-color: var(--dsw-alias-brand-primary, #0f9d58); }
|
|
284
|
+
[data-edrv-view] .edrv-search-toggle:disabled { opacity: .4; cursor: default; }
|
|
285
|
+
|
|
@@ -110,3 +110,21 @@
|
|
|
110
110
|
.vsm-general-row button:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,#ffffff14)}
|
|
111
111
|
.vsm-general-row button:disabled{opacity:.55;cursor:default}
|
|
112
112
|
.vsm-general-row button.vsm-devform-close{color:var(--dsw-alias-state-error-primary,#f07878);border-color:var(--dsw-alias-state-error-secondary,#d9534f55)}
|
|
113
|
+
|
|
114
|
+
/* keyboard shortcuts page */
|
|
115
|
+
.vsm-kb-list{display:flex;flex-direction:column;gap:6px;margin-top:8px}
|
|
116
|
+
.vsm-kb-row{display:flex;align-items:center;gap:10px;padding:8px 10px;border:1px solid var(--dsw-alias-border-l2,#555);border-radius:8px;background:var(--dsw-alias-bg-layer-1,#161616)}
|
|
117
|
+
.vsm-kb-row.rec{border-color:var(--dsw-alias-brand-primary,#4f8cff)}
|
|
118
|
+
.vsm-kb-label{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px;color:var(--dsw-alias-label-primary,#eee);font-size:13px}
|
|
119
|
+
.vsm-kb-warn{color:var(--dsw-alias-state-warn-primary,#e5b93f);font-size:11px}
|
|
120
|
+
.vsm-kb-key{min-width:96px;text-align:center;padding:3px 10px;border:1px solid var(--dsw-alias-border-l2,#555);border-radius:6px;background:var(--dsw-alias-bg-base,#111);color:var(--dsw-alias-label-primary,#eee);font-size:12px;font-family:var(--ds-font-family-code,ui-monospace,monospace)}
|
|
121
|
+
.vsm-kb-key.warn{border-color:var(--dsw-alias-state-warn-primary,#e5b93f);color:var(--dsw-alias-state-warn-primary,#e5b93f)}
|
|
122
|
+
.vsm-kb-key.invalid{border-color:var(--dsw-alias-state-error-primary,#d9534f);color:var(--dsw-alias-state-error-primary,#d9534f)}
|
|
123
|
+
.vsm-kb-rec{font-size:12px;color:var(--dsw-alias-label-secondary,#9aa5b1)}
|
|
124
|
+
.vsm-kb-actions{display:flex;gap:6px}
|
|
125
|
+
.vsm-kb-actions button{border:1px solid var(--dsw-alias-border-l2,#555);border-radius:6px;padding:4px 10px;background:none;color:var(--dsw-alias-label-primary,#eee);cursor:pointer;font-size:12px;white-space:nowrap}
|
|
126
|
+
.vsm-kb-actions button:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,#ffffff14)}
|
|
127
|
+
.vsm-kb-actions button:disabled{opacity:.55;cursor:default}
|
|
128
|
+
.vsm-kb-actionsbar{display:flex;align-items:center;gap:10px;margin-top:12px}
|
|
129
|
+
.vsm-kb-dirty{flex:1;font-size:12px;color:var(--dsw-alias-state-warn-primary,#e5b93f)}
|
|
130
|
+
.vsm-kb-message{font-size:12px;color:var(--dsw-alias-brand-primary,#4f8cff);margin-top:8px}
|
|
@@ -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 })
|
|
@@ -757,7 +795,9 @@ export function EditorView(props) {
|
|
|
757
795
|
fail++
|
|
758
796
|
}
|
|
759
797
|
}
|
|
760
|
-
|
|
798
|
+
// 合并结果无变化(批量全部失败/重复点击)时不 setRecords:避免 records 换引用
|
|
799
|
+
// → pendingRegions 换引用 → diff 全量重渲染(内容相同,纯空转)
|
|
800
|
+
if (Object.keys(next).length) setRecords((prev) => Object.assign({}, prev, next))
|
|
761
801
|
return { ok, fail }
|
|
762
802
|
}).catch((e) => {
|
|
763
803
|
setError('批量处理异常:' + String(e))
|
|
@@ -775,20 +815,26 @@ export function EditorView(props) {
|
|
|
775
815
|
const itemOf = (r, reject) => ({ callId: r.callId, scope: r.create ? 'call' : 'hunk', hunkIndex: r.idx, decision: reject ? 'rejected' : 'accepted' })
|
|
776
816
|
|
|
777
817
|
const acceptFile = () => {
|
|
818
|
+
if (batchBusyRef.current || !pendingRegions.length) return
|
|
819
|
+
batchBusyRef.current = true
|
|
778
820
|
actMany(pendingRegions.map((r) => itemOf(r, false))).then(({ ok, fail }) => {
|
|
821
|
+
batchBusyRef.current = false
|
|
779
822
|
reloadFile(true)
|
|
780
823
|
emitRefresh()
|
|
781
824
|
setStatus('已采纳 ' + ok + ' 处差异' + (fail ? ',' + fail + ' 处失败' : ''))
|
|
782
825
|
if (fail) setError(fail + ' 处差异处理失败(可能已被后续修改影响),可刷新后重试')
|
|
783
|
-
})
|
|
826
|
+
}).catch(() => { batchBusyRef.current = false })
|
|
784
827
|
}
|
|
785
828
|
const undoFile = () => {
|
|
829
|
+
if (batchBusyRef.current || !pendingRegions.length) return
|
|
830
|
+
batchBusyRef.current = true
|
|
786
831
|
actMany([...pendingRegions].reverse().map((r) => itemOf(r, true))).then(({ ok, fail }) => {
|
|
832
|
+
batchBusyRef.current = false
|
|
787
833
|
reloadFile(true)
|
|
788
834
|
emitRefresh()
|
|
789
835
|
setStatus('已不采纳 ' + ok + ' 处差异' + (fail ? ',' + fail + ' 处失败' : ''))
|
|
790
836
|
if (fail) setError(fail + ' 处差异处理失败(可能已被后续修改影响),可刷新后重试')
|
|
791
|
-
})
|
|
837
|
+
}).catch(() => { batchBusyRef.current = false })
|
|
792
838
|
}
|
|
793
839
|
|
|
794
840
|
// 所有差异文件的待处理 hunk 列表(二级菜单 Keep All / Undo All 用)
|
|
@@ -883,6 +929,20 @@ export function EditorView(props) {
|
|
|
883
929
|
if (focusDiff) pendingFocusRef.current = { path, region: null }
|
|
884
930
|
}
|
|
885
931
|
|
|
932
|
+
/**
|
|
933
|
+
* 打开文件并跳转到指定行列(搜索面板命中跳转)。
|
|
934
|
+
* @author ddj 2026年08月26号
|
|
935
|
+
* @param path 文件路径
|
|
936
|
+
* @param line 目标行(缺省仅打开不跳转)
|
|
937
|
+
* @param column 目标列(缺省 1)
|
|
938
|
+
*/
|
|
939
|
+
const openFileAt = (path, line, column) => {
|
|
940
|
+
if (!path) return
|
|
941
|
+
addTab(path, true)
|
|
942
|
+
pendingFocusRef.current = { path, region: null, line: line ?? null, column: column ?? 1 }
|
|
943
|
+
setFocusRequest((value) => value + 1)
|
|
944
|
+
}
|
|
945
|
+
|
|
886
946
|
const openPath = () => {
|
|
887
947
|
const p = (pathDraft || '').trim()
|
|
888
948
|
if (!p) return
|
|
@@ -916,7 +976,7 @@ export function EditorView(props) {
|
|
|
916
976
|
|
|
917
977
|
const pathBar = React.createElement('div', { className: 'edrv-pathbar', title: active || '' },
|
|
918
978
|
React.createElement('span', { className: 'edrv-pb-name' }, active ? String(active).split(/[\\/]/).pop() : '未打开文件'),
|
|
919
|
-
React.createElement('span', { className: 'edrv-pb-full' }, active || '使用右上搜索框 (Ctrl+P) 打开文件'),
|
|
979
|
+
React.createElement('span', { className: 'edrv-pb-full' }, active || '使用右上搜索框 (' + (chordOf('edrv.quickOpen') ?? 'Ctrl+P') + ') 打开文件'),
|
|
920
980
|
(active && langOf(active) ? React.createElement('span', { className: 'edrv-pb-meta' }, langOf(active)) : null),
|
|
921
981
|
(cursor ? React.createElement('span', { className: 'edrv-pb-meta' }, cursor) : null),
|
|
922
982
|
(status ? React.createElement('span', { className: 'edrv-pb-meta edrv-pb-status' }, status) : null))
|
|
@@ -926,7 +986,7 @@ export function EditorView(props) {
|
|
|
926
986
|
(sum.totalFiles > 0
|
|
927
987
|
? React.createElement('button', { className: 'edrv-diffchip', title: '打开差异总览/归档', onClick: () => setLauncherOpen((o) => !o) }, '⚠ 差异 ' + sum.totalFiles + ' 文件')
|
|
928
988
|
: null),
|
|
929
|
-
React.createElement('button', { className: 'edrv-chip-btn' + (sidebarOn ? ' edrv-chip-on' : ''), title: '切换侧边栏 (Ctrl+B)', onClick: () => setSidebarOn((v) => !v) }, '☰'),
|
|
989
|
+
React.createElement('button', { className: 'edrv-chip-btn' + (sidebarOn ? ' edrv-chip-on' : ''), title: '切换侧边栏 (' + (chordOf('edrv.toggleSidebar') ?? 'Ctrl+B') + ')', onClick: () => setSidebarOn((v) => !v) }, '☰'),
|
|
930
990
|
React.createElement('button', { className: 'edrv-chip-btn', title: '刷新', onClick: reloadFile }, '⟳'),
|
|
931
991
|
React.createElement(QuickOpen, { sessionId, onOpen: (p) => openFile(p, false) }))
|
|
932
992
|
|
|
@@ -964,7 +1024,7 @@ export function EditorView(props) {
|
|
|
964
1024
|
} else if (!active) {
|
|
965
1025
|
body = React.createElement('div', { className: 'edrv-empty' },
|
|
966
1026
|
React.createElement('div', null, '暂无打开的文件'),
|
|
967
|
-
React.createElement('div', { style: { fontSize: 12 } }, '使用右上搜索框 (Ctrl+P) 打开工作区文件;agent 修改文件后顶部会出现差异角标'))
|
|
1027
|
+
React.createElement('div', { style: { fontSize: 12 } }, '使用右上搜索框 (' + (chordOf('edrv.quickOpen') ?? 'Ctrl+P') + ') 打开工作区文件;agent 修改文件后顶部会出现差异角标'))
|
|
968
1028
|
} else if (content === null && loadError) {
|
|
969
1029
|
body = loadingBody('文件加载失败:' + loadError, 0, () => {
|
|
970
1030
|
setLoadError(null)
|
|
@@ -1073,6 +1133,7 @@ export function EditorView(props) {
|
|
|
1073
1133
|
const sidebarCtx = {
|
|
1074
1134
|
sessionId,
|
|
1075
1135
|
openFile: (p) => openFile(p, false),
|
|
1136
|
+
openFileAt,
|
|
1076
1137
|
activePath: active,
|
|
1077
1138
|
pendingByPath,
|
|
1078
1139
|
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
|
|