dsh-vscode-mode 0.1.19 → 0.1.21
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 +17 -7
- package/lib/client.js +1649 -272
- package/lib/client.js.map +1 -1
- package/lib/index.js +87 -0
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/diffDock.ts +52 -0
- package/src/client/diffDockStore.ts +87 -0
- package/src/client/editorLayout.ts +19 -0
- package/src/client/events.ts +40 -11
- package/src/client/index.ts +49 -13
- package/src/client/outline/OutlinePanel.tsx +213 -0
- package/src/client/outline/index.ts +24 -0
- package/src/client/outline/parse.ts +271 -0
- package/src/client/outline/sources.ts +134 -0
- package/src/client/outline/types.ts +51 -0
- package/src/client/sidebar/SidebarView.ts +73 -0
- package/src/client/sidebar/panels/FileExplorer.ts +161 -0
- package/src/client/sidebar/panels/index.ts +24 -0
- package/src/client/sidebar/registry.ts +52 -0
- package/src/client/sidebar/types.ts +36 -0
- package/src/client/styles/editor.css +130 -5
- package/src/client/ui/ConversationDiffDock.ts +84 -0
- package/src/client/ui/DiffBox.ts +78 -29
- package/src/client/ui/EditorView.ts +209 -52
- package/src/rpc.ts +21 -0
- package/src/shared/rpc.ts +10 -0
- package/src/tree.ts +62 -0
|
@@ -13,9 +13,11 @@ import { createDiffRenderer } from '../monaco/diffRender.js'
|
|
|
13
13
|
import { ST, callIdAttr, noopHunk, summarize } from '../state/records.js'
|
|
14
14
|
import { diffRegions } from '../state/regions.js'
|
|
15
15
|
import { QuickOpen } from './QuickOpen.js'
|
|
16
|
-
import { DiffBox } from './DiffBox.js'
|
|
17
|
-
import { DiffBarEmpty } from './DiffBarEmpty.js'
|
|
18
16
|
import { DiffLauncher } from './DiffLauncher.js'
|
|
17
|
+
import { SidebarView } from '../sidebar/SidebarView.js'
|
|
18
|
+
import { clearDiffDock, publishDiffDock } from '../diffDockStore.js'
|
|
19
|
+
import { displayDiffTotal, editorDockMode } from '../diffDock.js'
|
|
20
|
+
import { editorHeight } from '../editorLayout.js'
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
23
|
* 中央编辑区:文件页签(脏点/关闭/打开路径)+ Ctrl+P 搜索 + Monaco 编辑器 +
|
|
@@ -34,6 +36,7 @@ export function EditorView(props) {
|
|
|
34
36
|
const [active, setActive] = React.useState(null)
|
|
35
37
|
const [dirtyMap, setDirtyMap] = React.useState({})
|
|
36
38
|
const [content, setContent] = React.useState(null)
|
|
39
|
+
const [contentPath, setContentPath] = React.useState(null)
|
|
37
40
|
const [status, setStatus] = React.useState('')
|
|
38
41
|
const [error, setError] = React.useState(null)
|
|
39
42
|
const [loadError, setLoadError] = React.useState(null)
|
|
@@ -47,7 +50,12 @@ export function EditorView(props) {
|
|
|
47
50
|
const [diffIdx, setDiffIdx] = React.useState(0) // 当前文件内差异位置(x/x 显示)
|
|
48
51
|
const [fileIdx, setFileIdx] = React.useState(0) // 全局差异文件位置(x/x 文件 显示)
|
|
49
52
|
const [tabMenu, setTabMenu] = React.useState(null) // Tab 右键菜单 { x,y,path }(编辑区右键走 Monaco 原生菜单,无此浮层)
|
|
53
|
+
const [sidebarOn, setSidebarOn] = React.useState(true) // 侧边栏显隐
|
|
54
|
+
const [sidebarW, setSidebarW] = React.useState(240) // 侧边栏宽度
|
|
55
|
+
const [activePanel, setActivePanel] = React.useState('explorer') // 激活面板 id
|
|
56
|
+
const [focusRequest, setFocusRequest] = React.useState(0) // 外部差异聚焦请求版本
|
|
50
57
|
const editorRef = React.useRef(null)
|
|
58
|
+
const viewRootRef = React.useRef(null)
|
|
51
59
|
const monacoRef = React.useRef(null)
|
|
52
60
|
const modelsRef = React.useRef(new Map())
|
|
53
61
|
const saveTimerRef = React.useRef(null)
|
|
@@ -74,7 +82,8 @@ export function EditorView(props) {
|
|
|
74
82
|
return list
|
|
75
83
|
}, [records, active])
|
|
76
84
|
|
|
77
|
-
const
|
|
85
|
+
const contentReady = content !== null && contentPath === active
|
|
86
|
+
const regions = React.useMemo(() => diffRegions(currentRecords, contentReady ? content : null).filter((r) => !r.superseded), [currentRecords, content, contentPath, active])
|
|
78
87
|
// useMemo 稳定引用:否则 hover 等重渲染会让 view zone effect 反复重建(- 号闪烁)
|
|
79
88
|
const pendingRegions = React.useMemo(() => regions.filter((r) => r.status === ST.PENDING && !r.stale), [regions])
|
|
80
89
|
const staleRegions = React.useMemo(() => regions.filter((r) => r.status === ST.PENDING && r.stale), [regions])
|
|
@@ -132,6 +141,7 @@ export function EditorView(props) {
|
|
|
132
141
|
if (seq !== loadSeqRef.current || path !== active) return
|
|
133
142
|
if (res && res.ok) {
|
|
134
143
|
setContent(res.content)
|
|
144
|
+
setContentPath(path)
|
|
135
145
|
setLoadStage((prev) => ({ progress: Math.max(84, prev.progress), message: '文件已读取,准备创建编辑器…' }))
|
|
136
146
|
setStatus('已加载')
|
|
137
147
|
} else {
|
|
@@ -162,9 +172,18 @@ export function EditorView(props) {
|
|
|
162
172
|
React.useEffect(() => {
|
|
163
173
|
const onOpen = (e) => {
|
|
164
174
|
const p = e?.detail?.path
|
|
165
|
-
if (p)
|
|
175
|
+
if (!p) return
|
|
176
|
+
if (e?.detail?.focusDiff === true) {
|
|
177
|
+
pendingFocusRef.current = { path: p, region: null }
|
|
178
|
+
setFocusRequest((value) => value + 1)
|
|
179
|
+
}
|
|
180
|
+
addTab(p, true)
|
|
181
|
+
}
|
|
182
|
+
const onShowLauncher = (event) => {
|
|
183
|
+
const tab = event?.detail?.tab
|
|
184
|
+
if (tab === 'pending' || tab === 'archive') setLauncherTab(tab)
|
|
185
|
+
setLauncherOpen(true)
|
|
166
186
|
}
|
|
167
|
-
const onShowLauncher = () => setLauncherOpen(true)
|
|
168
187
|
window.addEventListener('edrv:open-editor', onOpen)
|
|
169
188
|
window.addEventListener('edrv:show-launcher', onShowLauncher)
|
|
170
189
|
return () => {
|
|
@@ -173,6 +192,64 @@ export function EditorView(props) {
|
|
|
173
192
|
}
|
|
174
193
|
}, [sessionId])
|
|
175
194
|
|
|
195
|
+
// 对话输入区的唯一差异 dock 读取此会话快照;编辑器卸载时只清理自己的发布者令牌。
|
|
196
|
+
const dockSourceRef = React.useRef({})
|
|
197
|
+
React.useEffect(() => () => clearDiffDock(sessionId, dockSourceRef.current), [sessionId])
|
|
198
|
+
|
|
199
|
+
// DSH composer 保持原生布局;编辑区只读取几何边界并同步自己的高度。
|
|
200
|
+
React.useLayoutEffect(() => {
|
|
201
|
+
const root = viewRootRef.current
|
|
202
|
+
const scroll = root?.closest?.('[data-conversation-scroll]')
|
|
203
|
+
if (!root || !scroll) return
|
|
204
|
+
let seat = null
|
|
205
|
+
let frame = null
|
|
206
|
+
let resizeObserver = null
|
|
207
|
+
const update = () => {
|
|
208
|
+
frame = null
|
|
209
|
+
const rootRect = root.getBoundingClientRect()
|
|
210
|
+
const scrollRect = scroll.getBoundingClientRect()
|
|
211
|
+
const seatRect = seat?.getBoundingClientRect?.()
|
|
212
|
+
const height = editorHeight(rootRect.top, scrollRect.bottom, seatRect?.top)
|
|
213
|
+
root.style.setProperty('--edrv-editor-height', height + 'px')
|
|
214
|
+
}
|
|
215
|
+
const scheduleUpdate = () => {
|
|
216
|
+
if (frame !== null) return
|
|
217
|
+
frame = requestAnimationFrame(update)
|
|
218
|
+
}
|
|
219
|
+
const attachSeat = () => {
|
|
220
|
+
const next = scroll.querySelector?.('[data-composer-seat]')
|
|
221
|
+
if (next === seat) {
|
|
222
|
+
scheduleUpdate()
|
|
223
|
+
return
|
|
224
|
+
}
|
|
225
|
+
resizeObserver?.disconnect()
|
|
226
|
+
seat = next
|
|
227
|
+
resizeObserver = null
|
|
228
|
+
if (seat && typeof ResizeObserver === 'function') {
|
|
229
|
+
resizeObserver = new ResizeObserver(scheduleUpdate)
|
|
230
|
+
resizeObserver.observe(seat)
|
|
231
|
+
}
|
|
232
|
+
scheduleUpdate()
|
|
233
|
+
}
|
|
234
|
+
attachSeat()
|
|
235
|
+
const mutationObserver = typeof MutationObserver === 'function'
|
|
236
|
+
? new MutationObserver(attachSeat)
|
|
237
|
+
: null
|
|
238
|
+
mutationObserver?.observe(scroll, { childList: true })
|
|
239
|
+
const scrollObserver = typeof ResizeObserver === 'function' ? new ResizeObserver(scheduleUpdate) : null
|
|
240
|
+
scrollObserver?.observe(scroll)
|
|
241
|
+
window.addEventListener('resize', scheduleUpdate)
|
|
242
|
+
scheduleUpdate()
|
|
243
|
+
return () => {
|
|
244
|
+
if (frame !== null) cancelAnimationFrame(frame)
|
|
245
|
+
resizeObserver?.disconnect()
|
|
246
|
+
scrollObserver?.disconnect()
|
|
247
|
+
mutationObserver?.disconnect()
|
|
248
|
+
window.removeEventListener('resize', scheduleUpdate)
|
|
249
|
+
root.style.removeProperty('--edrv-editor-height')
|
|
250
|
+
}
|
|
251
|
+
}, [sessionId])
|
|
252
|
+
|
|
176
253
|
// localStorage v2 恢复页签
|
|
177
254
|
React.useEffect(() => {
|
|
178
255
|
if (bootRef.current || !sessionId) return
|
|
@@ -195,9 +272,50 @@ export function EditorView(props) {
|
|
|
195
272
|
catch (e) { /* 忽略 */ }
|
|
196
273
|
}, [tabs, active, sessionId])
|
|
197
274
|
|
|
275
|
+
// 侧边栏状态:恢复(显隐/宽度/激活面板)
|
|
276
|
+
React.useEffect(() => {
|
|
277
|
+
if (!sessionId) return
|
|
278
|
+
try {
|
|
279
|
+
const raw = localStorage.getItem('edrv.sidebar.' + String(sessionId))
|
|
280
|
+
if (raw) {
|
|
281
|
+
const saved = JSON.parse(raw)
|
|
282
|
+
if (typeof saved.on === 'boolean') setSidebarOn(saved.on)
|
|
283
|
+
if (typeof saved.width === 'number') setSidebarW(Math.max(180, Math.min(560, saved.width)))
|
|
284
|
+
if (typeof saved.panel === 'string') setActivePanel(saved.panel)
|
|
285
|
+
}
|
|
286
|
+
} catch (e) { /* 损坏忽略 */ }
|
|
287
|
+
}, [sessionId])
|
|
288
|
+
|
|
289
|
+
// 侧边栏状态持久化
|
|
290
|
+
React.useEffect(() => {
|
|
291
|
+
if (!sessionId) return
|
|
292
|
+
try { localStorage.setItem('edrv.sidebar.' + String(sessionId), JSON.stringify({ on: sidebarOn, width: sidebarW, panel: activePanel })) }
|
|
293
|
+
catch (e) { /* 忽略 */ }
|
|
294
|
+
}, [sidebarOn, sidebarW, activePanel, sessionId])
|
|
295
|
+
|
|
296
|
+
// Ctrl+B 切换侧边栏(capture 抢占,避免与 DSH 全局冲突)
|
|
297
|
+
React.useEffect(() => {
|
|
298
|
+
const onKey = (e) => {
|
|
299
|
+
if ((e.ctrlKey || e.metaKey) && String(e.key).toLowerCase() === 'b') {
|
|
300
|
+
e.preventDefault(); e.stopPropagation()
|
|
301
|
+
setSidebarOn((v) => !v)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
window.addEventListener('keydown', onKey, true)
|
|
305
|
+
return () => window.removeEventListener('keydown', onKey, true)
|
|
306
|
+
}, [])
|
|
307
|
+
|
|
308
|
+
// 文件 → 待处理差异数 映射(侧边栏角标)
|
|
309
|
+
const pendingByPath = React.useMemo(() => {
|
|
310
|
+
const out = {}
|
|
311
|
+
for (const f of sum.files) if (f.pending > 0) out[f.path] = f.pending
|
|
312
|
+
return out
|
|
313
|
+
}, [sum])
|
|
314
|
+
|
|
198
315
|
React.useEffect(() => {
|
|
199
316
|
if (!active) return
|
|
200
317
|
setContent(null)
|
|
318
|
+
setContentPath(null)
|
|
201
319
|
setLoadError(null)
|
|
202
320
|
setLoadStage({ progress: 10, message: '准备读取文件…' })
|
|
203
321
|
setStatus('加载中…')
|
|
@@ -258,6 +376,7 @@ export function EditorView(props) {
|
|
|
258
376
|
if (res && res.ok) {
|
|
259
377
|
setStatus('已保存 ' + new Date().toTimeString().slice(0, 8))
|
|
260
378
|
setContent(text)
|
|
379
|
+
setContentPath(active)
|
|
261
380
|
setDirtyMap((d) => Object.assign({}, d, { [active]: false }))
|
|
262
381
|
refreshRecords()
|
|
263
382
|
emitRefresh()
|
|
@@ -418,7 +537,7 @@ export function EditorView(props) {
|
|
|
418
537
|
ed.revealLineInCenter(Math.max(1, target.start ?? 1))
|
|
419
538
|
ed.setPosition({ lineNumber: Math.max(1, target.start ?? 1), column: 1 })
|
|
420
539
|
ed.focus()
|
|
421
|
-
}, [active, content, pendingRegions])
|
|
540
|
+
}, [active, content, pendingRegions, focusRequest])
|
|
422
541
|
|
|
423
542
|
const jumpTo = (region) => {
|
|
424
543
|
if (editorRef.current) {
|
|
@@ -455,6 +574,12 @@ export function EditorView(props) {
|
|
|
455
574
|
openFile(sum.pendingFiles[next].path, true)
|
|
456
575
|
}
|
|
457
576
|
|
|
577
|
+
const openNextFile = () => {
|
|
578
|
+
if (!sum.pendingFiles.length) return
|
|
579
|
+
if (sum.pendingFiles.some((file) => file.path === active)) gotoFile(1)
|
|
580
|
+
else openFile(sum.pendingFiles[0].path, true)
|
|
581
|
+
}
|
|
582
|
+
|
|
458
583
|
const reloadFile = (skipStale) => {
|
|
459
584
|
if (!active) return
|
|
460
585
|
loadContent(active, sessionId)
|
|
@@ -668,22 +793,19 @@ export function EditorView(props) {
|
|
|
668
793
|
|
|
669
794
|
const pathBar = React.createElement('div', { className: 'edrv-pathbar', title: active || '' },
|
|
670
795
|
React.createElement('span', { className: 'edrv-pb-name' }, active ? String(active).split(/[\\/]/).pop() : '未打开文件'),
|
|
671
|
-
React.createElement('span', { className: 'edrv-pb-full' }, active || '使用右上搜索框 (Ctrl+P) 打开文件')
|
|
796
|
+
React.createElement('span', { className: 'edrv-pb-full' }, active || '使用右上搜索框 (Ctrl+P) 打开文件'),
|
|
797
|
+
(active && langOf(active) ? React.createElement('span', { className: 'edrv-pb-meta' }, langOf(active)) : null),
|
|
798
|
+
(cursor ? React.createElement('span', { className: 'edrv-pb-meta' }, cursor) : null),
|
|
799
|
+
(status ? React.createElement('span', { className: 'edrv-pb-meta edrv-pb-status' }, status) : null))
|
|
672
800
|
|
|
673
801
|
const tabRow = React.createElement('div', { style: { display: 'flex', alignItems: 'center', background: 'var(--dsw-alias-bg-layer-1,transparent)', flexShrink: 0 } },
|
|
674
802
|
tabsEl,
|
|
675
|
-
React.createElement(QuickOpen, { sessionId, onOpen: (p) => openFile(p, false) }))
|
|
676
|
-
|
|
677
|
-
const statusBar = React.createElement('div', { className: 'edrv-statusbar' },
|
|
678
|
-
React.createElement('span', null, '编辑'),
|
|
679
|
-
React.createElement('span', null, active ? langOf(active) : ''),
|
|
680
|
-
React.createElement('span', null, cursor),
|
|
681
|
-
(status ? React.createElement('span', null, status) : null),
|
|
682
|
-
React.createElement('span', { style: { flex: 1 } }),
|
|
683
803
|
(sum.totalFiles > 0
|
|
684
804
|
? React.createElement('button', { className: 'edrv-diffchip', title: '打开差异总览/归档', onClick: () => setLauncherOpen((o) => !o) }, '⚠ 差异 ' + sum.totalFiles + ' 文件')
|
|
685
805
|
: null),
|
|
686
|
-
React.createElement('button', { className: 'edrv-chip-btn', title: '
|
|
806
|
+
React.createElement('button', { className: 'edrv-chip-btn' + (sidebarOn ? ' edrv-chip-on' : ''), title: '切换侧边栏 (Ctrl+B)', onClick: () => setSidebarOn((v) => !v) }, '☰'),
|
|
807
|
+
React.createElement('button', { className: 'edrv-chip-btn', title: '刷新', onClick: reloadFile }, '⟳'),
|
|
808
|
+
React.createElement(QuickOpen, { sessionId, onOpen: (p) => openFile(p, false) }))
|
|
687
809
|
|
|
688
810
|
const otherFiles = sum.pendingFiles.filter((f) => f.path !== active)
|
|
689
811
|
|
|
@@ -724,6 +846,7 @@ export function EditorView(props) {
|
|
|
724
846
|
body = loadingBody('文件加载失败:' + loadError, 0, () => {
|
|
725
847
|
setLoadError(null)
|
|
726
848
|
setContent(null)
|
|
849
|
+
setContentPath(null)
|
|
727
850
|
setLoadStage({ progress: 10, message: '重新读取文件…' })
|
|
728
851
|
loadContent(active, sessionId)
|
|
729
852
|
})
|
|
@@ -756,39 +879,43 @@ export function EditorView(props) {
|
|
|
756
879
|
React.createElement('button', { className: 'edrv-pill edrv-pill-undo', onClick: () => { dismissHover(); actHunk(hoverAct.region, true) } }, '↩ Undo'))
|
|
757
880
|
: null
|
|
758
881
|
|
|
882
|
+
React.useEffect(() => {
|
|
883
|
+
if (!sessionId) return
|
|
884
|
+
const mode = editorDockMode(active)
|
|
885
|
+
publishDiffDock(sessionId, {
|
|
886
|
+
mode,
|
|
887
|
+
pendingRegions,
|
|
888
|
+
staleRegions,
|
|
889
|
+
onAct: actHunk,
|
|
890
|
+
onAcceptFile: acceptFile,
|
|
891
|
+
onUndoFile: undoFile,
|
|
892
|
+
onAcceptAllFiles: acceptAllFiles,
|
|
893
|
+
onUndoAllFiles: undoAllFiles,
|
|
894
|
+
allPendingCount: allPending.length,
|
|
895
|
+
onRollback: rollbackFile,
|
|
896
|
+
onJump: jumpTo,
|
|
897
|
+
otherFiles,
|
|
898
|
+
onOpenOther: (path) => openFile(path, true),
|
|
899
|
+
onOpenLauncher: (tab) => { setLauncherTab(tab || 'pending'); setLauncherOpen(true) },
|
|
900
|
+
onRefresh: reloadFile,
|
|
901
|
+
activePath: active,
|
|
902
|
+
diffIdx: contentReady ? diffIdx : 0,
|
|
903
|
+
diffTotal: displayDiffTotal(contentReady, pendingRegions.length, sum.files.find((file) => file.path === active)?.pending ?? 0),
|
|
904
|
+
fileIdx,
|
|
905
|
+
fileTotal: sum.pendingFiles.length,
|
|
906
|
+
onPrevDiff: () => gotoDiff(-1),
|
|
907
|
+
onNextDiff: () => gotoDiff(1),
|
|
908
|
+
onPrevFile: () => gotoFile(-1),
|
|
909
|
+
onNextFile: () => gotoFile(1),
|
|
910
|
+
onOpenNextFile: openNextFile,
|
|
911
|
+
}, dockSourceRef.current)
|
|
912
|
+
})
|
|
913
|
+
|
|
759
914
|
const overlay = launcherOpen
|
|
760
915
|
? React.createElement(React.Fragment, null,
|
|
761
916
|
React.createElement('div', { style: { position: 'fixed', inset: 0, zIndex: 30 }, onClick: () => setLauncherOpen(false) }),
|
|
762
917
|
React.createElement(DiffLauncher, { sessionId, sum, tab: launcherTab, onClose: () => setLauncherOpen(false), onOpenFile: (p) => { openFile(p, true); setLauncherOpen(false) } }))
|
|
763
|
-
:
|
|
764
|
-
? React.createElement(DiffBox, {
|
|
765
|
-
pendingRegions, staleRegions,
|
|
766
|
-
onAct: actHunk,
|
|
767
|
-
onAcceptFile: acceptFile,
|
|
768
|
-
onUndoFile: undoFile,
|
|
769
|
-
onAcceptAllFiles: acceptAllFiles,
|
|
770
|
-
onUndoAllFiles: undoAllFiles,
|
|
771
|
-
allPendingCount: allPending.length,
|
|
772
|
-
onRollback: rollbackFile,
|
|
773
|
-
onJump: jumpTo,
|
|
774
|
-
otherFiles,
|
|
775
|
-
onOpenOther: (p) => openFile(p, true),
|
|
776
|
-
onOpenLauncher: (tab) => { setLauncherTab(tab || 'pending'); setLauncherOpen(true) },
|
|
777
|
-
onRefresh: reloadFile,
|
|
778
|
-
activePath: active,
|
|
779
|
-
diffIdx, diffTotal: pendingRegions.length,
|
|
780
|
-
fileIdx, fileTotal: sum.pendingFiles.length,
|
|
781
|
-
onPrevDiff: () => gotoDiff(-1), onNextDiff: () => gotoDiff(1),
|
|
782
|
-
onPrevFile: () => gotoFile(-1), onNextFile: () => gotoFile(1),
|
|
783
|
-
})
|
|
784
|
-
: (sum.pendingFiles.length > 0
|
|
785
|
-
? React.createElement(DiffBarEmpty, {
|
|
786
|
-
sum, active, staleCount: staleRegions.length,
|
|
787
|
-
onNextFile: () => { if (sum.pendingFiles.length) openFile(sum.pendingFiles[0].path, true) },
|
|
788
|
-
onOpenLauncher: (tab) => { setLauncherTab(tab || 'pending'); setLauncherOpen(true) },
|
|
789
|
-
onRefresh: reloadFile,
|
|
790
|
-
})
|
|
791
|
-
: null))
|
|
918
|
+
: null
|
|
792
919
|
|
|
793
920
|
// Tab 右键菜单浮层:全屏遮罩(点击/右键关闭)+ 菜单(固定定位,clamp 防越界)。
|
|
794
921
|
// 编辑区右键走 Monaco 原生 context menu(editor.addAction),不在此渲染浮层。
|
|
@@ -814,14 +941,44 @@ export function EditorView(props) {
|
|
|
814
941
|
React.createElement('button', { className: 'edrv-ctxmenu-item edrv-ctxmenu-danger', onClick: () => { closeTab(tabMenu.path); dismissMenus() } }, '关闭标签页'))
|
|
815
942
|
: null
|
|
816
943
|
|
|
817
|
-
|
|
944
|
+
const sidebarPanels = props.sidebarPanels
|
|
945
|
+
const sidebarCtx = {
|
|
946
|
+
sessionId,
|
|
947
|
+
openFile: (p) => openFile(p, false),
|
|
948
|
+
activePath: active,
|
|
949
|
+
pendingByPath,
|
|
950
|
+
sum: { totalFiles: sum.totalFiles, files: sum.files },
|
|
951
|
+
refreshRecords: () => refreshRecords(),
|
|
952
|
+
editor: () => editorRef.current,
|
|
953
|
+
outlineSources: props.outlineSources,
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// 主编辑列(侧边栏右侧):pathBar + tabRow + 编辑/差异区(底部整条留给 DSH 对话输入栏)
|
|
957
|
+
const editorArea = React.createElement('div', { className: 'edrv-editor-area' },
|
|
958
|
+
body,
|
|
959
|
+
hoverEl,
|
|
960
|
+
overlay)
|
|
961
|
+
const mainCol = React.createElement('div', { className: 'edrv-main-col', style: { flex: 1, minWidth: 0, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' } },
|
|
818
962
|
pathBar,
|
|
819
963
|
tabRow,
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
964
|
+
editorArea)
|
|
965
|
+
// 编辑器根节点按 composer 顶部边界动态限高,底部对话区域继续由 DSH 原生渲染。
|
|
966
|
+
const editorRow = React.createElement('div', { className: 'edrv-editor-row' },
|
|
967
|
+
(sidebarOn && sidebarPanels
|
|
968
|
+
? React.createElement(SidebarView, {
|
|
969
|
+
registry: sidebarPanels,
|
|
970
|
+
ctx: sidebarCtx,
|
|
971
|
+
activePanel,
|
|
972
|
+
onActive: setActivePanel,
|
|
973
|
+
width: sidebarW,
|
|
974
|
+
onWidth: setSidebarW,
|
|
975
|
+
onHide: () => setSidebarOn(false),
|
|
976
|
+
})
|
|
977
|
+
: null),
|
|
978
|
+
mainCol)
|
|
979
|
+
|
|
980
|
+
return React.createElement('div', { ref: viewRootRef, 'data-edrv-view': '1', style: { height: 'var(--edrv-editor-height, 100%)', maxHeight: 'var(--edrv-editor-height, 100%)', minHeight: 0, display: 'flex', flexDirection: 'column', background: 'var(--dsw-alias-bg-base,transparent)', overflow: 'hidden' } },
|
|
981
|
+
editorRow,
|
|
824
982
|
menuBackdrop,
|
|
825
|
-
tabMenuEl
|
|
826
|
-
statusBar)
|
|
983
|
+
tabMenuEl)
|
|
827
984
|
}
|
package/src/rpc.ts
CHANGED
|
@@ -29,6 +29,7 @@ import { listProjects, projectRefresh, projectRemove, projectSave, projectToggle
|
|
|
29
29
|
import { normalizeFileOpenTool, FILE_OPEN_DEFAULT, FILE_OPEN_SETTINGS_NS } from './fileOpenSettings.js'
|
|
30
30
|
import { buildReport } from './compat.js'
|
|
31
31
|
import { readDevForm, setDevForm } from './devForm.js'
|
|
32
|
+
import { normalizeRel, toTreeEntries } from './tree.js'
|
|
32
33
|
|
|
33
34
|
/** cwd → Promise 链:串行化 debug 日志追加(fs read+write 非原子,避免并发丢行)。 */
|
|
34
35
|
const debugWriteQueues = new Map<string, Promise<void>>()
|
|
@@ -315,6 +316,26 @@ export function buildHandlers(ctx: Ctx, registry: Registry, searcher = newSearch
|
|
|
315
316
|
const result = await searcher.search({ session: sc.session, cwd: sc.cwd, query: args.query, activePaths })
|
|
316
317
|
return { ok: true, ...result }
|
|
317
318
|
},
|
|
319
|
+
'edrv.listDir': async (args) => {
|
|
320
|
+
// 目录树(侧边栏文件管理用):按会话 cwd 解析,与 edrv.read 同基准,树内相对路径点开即打开。
|
|
321
|
+
const sc = await requireSession(ctx, args.sessionId)
|
|
322
|
+
if ('err' in sc) return { ok: false, error: sc.err }
|
|
323
|
+
const fs = ctx.get('fs')
|
|
324
|
+
if (!fs) return { ok: false, error: '缺少 fs' }
|
|
325
|
+
try {
|
|
326
|
+
const rel = normalizeRel(args.path)
|
|
327
|
+
if (rel === null) return { ok: false, error: '路径不合法' }
|
|
328
|
+
const target = await resolveTarget(ctx, sc.session, rel || '.')
|
|
329
|
+
const info = await fs.stat(target)
|
|
330
|
+
if (!info) return { ok: false, error: '目录不存在' }
|
|
331
|
+
if (info.type !== 'directory') return { ok: false, error: '不是目录' }
|
|
332
|
+
const children = await fs.listDir(target)
|
|
333
|
+
const root = fs.processPath(await fs.resolve('.', { cwd: sc.cwd }))
|
|
334
|
+
return { ok: true, root, path: rel, entries: toTreeEntries(rel, children) }
|
|
335
|
+
} catch (error) {
|
|
336
|
+
return { ok: false, error: '读取目录失败:' + String(error) }
|
|
337
|
+
}
|
|
338
|
+
},
|
|
318
339
|
'mcp.list': async () => ({ ok: true, ...listMcp(ctx) }),
|
|
319
340
|
'mcp.save': async (args) => {
|
|
320
341
|
try { return { ok: true, server: await saveMcp(ctx, args.config) } }
|
package/src/shared/rpc.ts
CHANGED
|
@@ -21,6 +21,14 @@ export const RPC_PATH = '/edrv/rpc'
|
|
|
21
21
|
/** 决策作用域:call=整条记录,hunk=单个差异块。 */
|
|
22
22
|
export type RpcScope = 'call' | 'hunk'
|
|
23
23
|
|
|
24
|
+
/** 目录树单项(edrv.listDir 返回;path 相对工作区根,'/' 分隔)。 */
|
|
25
|
+
export interface TreeEntry {
|
|
26
|
+
name: string
|
|
27
|
+
path: string
|
|
28
|
+
type: 'file' | 'directory' | 'other'
|
|
29
|
+
size?: number
|
|
30
|
+
}
|
|
31
|
+
|
|
24
32
|
/** 一条批量决策项(decideBatch 的 items 元素,与 accept/reject 单条参数同构)。 */
|
|
25
33
|
export interface DecideItem {
|
|
26
34
|
callId: string
|
|
@@ -51,6 +59,7 @@ export interface RpcRequestMap {
|
|
|
51
59
|
'edrv.rollback': { sessionId?: string; path: string; batch?: number }
|
|
52
60
|
'edrv.debug': { sessionId?: string; text: string }
|
|
53
61
|
'edrv.searchFiles': { sessionId?: string; query: string }
|
|
62
|
+
'edrv.listDir': { sessionId?: string; path: string }
|
|
54
63
|
'mcp.list': {}
|
|
55
64
|
'mcp.save': { config: MpcConfig }
|
|
56
65
|
'mcp.remove': { id: string }
|
|
@@ -84,6 +93,7 @@ export interface RpcOkMap {
|
|
|
84
93
|
'edrv.rollback': { path: string; batch: number | null }
|
|
85
94
|
'edrv.debug': object
|
|
86
95
|
'edrv.searchFiles': { files: string[]; truncated: boolean }
|
|
96
|
+
'edrv.listDir': { root: string; path: string; entries: TreeEntry[] }
|
|
87
97
|
'mcp.list': { servers: MpcServer[] }
|
|
88
98
|
'mcp.save': { server: MpcServer }
|
|
89
99
|
'mcp.remove': object
|
package/src/tree.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode host — 目录树纯函数(edrv.listDir 的数据整形与守卫)。
|
|
3
|
+
* 不触 fs/node,仅结构类型,可单测;RPC handler 只做装配,判定都收在这里。
|
|
4
|
+
* 作者 ddj 2026-08-26
|
|
5
|
+
*/
|
|
6
|
+
import type { TreeEntry } from './shared/rpc.js'
|
|
7
|
+
|
|
8
|
+
/** fs.listDir 返回项的结构最小视图(不引 dsh-fs,保持纯函数零依赖)。 */
|
|
9
|
+
export interface DirChildLike {
|
|
10
|
+
name: string
|
|
11
|
+
type: 'file' | 'directory' | 'other'
|
|
12
|
+
size?: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** 目录在文件前、名称按小写字母序的比较器(对齐 VSCode 资源管理器习惯)。 */
|
|
16
|
+
function entryOrder(a: { name: string; type: string }, b: { name: string; type: string }): number {
|
|
17
|
+
const aDir = a.type === 'directory' ? 0 : 1
|
|
18
|
+
const bDir = b.type === 'directory' ? 0 : 1
|
|
19
|
+
if (aDir !== bDir) return aDir - bDir
|
|
20
|
+
const la = a.name.toLocaleLowerCase('en-US')
|
|
21
|
+
const lb = b.name.toLocaleLowerCase('en-US')
|
|
22
|
+
return la < lb ? -1 : la > lb ? 1 : 0
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 归一化目录相对路径:去空白、反斜杠→斜杠、去首尾斜杠、`.`→''。
|
|
27
|
+
* 任一路径段为 `..` 返回 null(拒绝越界)。
|
|
28
|
+
* @author ddj 2026年08月26号
|
|
29
|
+
* @param path 原始相对路径('' 表示根目录)
|
|
30
|
+
* @returns 归一化相对路径('' = 根),非法返回 null
|
|
31
|
+
*/
|
|
32
|
+
export function normalizeRel(path: string | undefined | null): string | null {
|
|
33
|
+
const rel = String(path ?? '').trim().replace(/\\/g, '/').replace(/^\/+|\/+$/g, '')
|
|
34
|
+
if (!rel) return ''
|
|
35
|
+
const segs = rel.split('/').filter((seg) => seg !== '.')
|
|
36
|
+
if (!segs.length) return ''
|
|
37
|
+
for (const seg of segs) {
|
|
38
|
+
if (seg === '..') return null
|
|
39
|
+
}
|
|
40
|
+
return segs.join('/')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* fs.listDir 结果 → TreeEntry 数组:相对路径拼接 + 目录优先排序。
|
|
45
|
+
* @author ddj 2026年08月26号
|
|
46
|
+
* @param rel 父目录相对路径('' = 根)
|
|
47
|
+
* @param children 目录子项
|
|
48
|
+
* @returns 排序后的树条目
|
|
49
|
+
*/
|
|
50
|
+
export function toTreeEntries(rel: string, children: DirChildLike[]): TreeEntry[] {
|
|
51
|
+
const out: TreeEntry[] = []
|
|
52
|
+
for (const child of children) {
|
|
53
|
+
if (!child || typeof child.name !== 'string' || !child.name) continue
|
|
54
|
+
out.push({
|
|
55
|
+
name: child.name,
|
|
56
|
+
path: rel ? rel + '/' + child.name : child.name,
|
|
57
|
+
type: child.type === 'directory' ? 'directory' : child.type === 'file' ? 'file' : 'other',
|
|
58
|
+
size: typeof child.size === 'number' ? child.size : undefined,
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
return out.sort(entryOrder)
|
|
62
|
+
}
|