dsh-vscode-mode 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +78 -5
  2. package/lib/client.js +5579 -236
  3. package/lib/client.js.map +1 -1
  4. package/lib/index.js +2358 -126
  5. package/lib/index.js.map +1 -1
  6. package/package.json +1 -1
  7. package/src/client/compat.ts +1 -1
  8. package/src/client/dap/BpWidget.ts +262 -0
  9. package/src/client/dap/bpMenu.ts +91 -0
  10. package/src/client/dap/bpRowMenu.ts +53 -0
  11. package/src/client/dap/breakpoints.ts +222 -0
  12. package/src/client/dap/decorate.ts +128 -0
  13. package/src/client/dap/hintLine.ts +29 -0
  14. package/src/client/dap/hover.ts +166 -0
  15. package/src/client/dap/hoverMode.ts +159 -0
  16. package/src/client/dap/hoverTree.ts +722 -0
  17. package/src/client/dap/launchInsert.ts +229 -0
  18. package/src/client/dap/launchSnippetProvider.ts +206 -0
  19. package/src/client/dap/modelPath.ts +23 -0
  20. package/src/client/dap/panelSplit.ts +105 -0
  21. package/src/client/dap/pidPick.ts +24 -0
  22. package/src/client/dap/store.ts +571 -0
  23. package/src/client/dap/toolbarDrag.ts +51 -0
  24. package/src/client/dap/trace.ts +22 -0
  25. package/src/client/dap/variableTree.ts +62 -0
  26. package/src/client/editorLimit.ts +31 -0
  27. package/src/client/index.ts +13 -0
  28. package/src/client/markdownPreview.ts +24 -0
  29. package/src/client/md/componentType.ts +28 -0
  30. package/src/client/md/mdPanel.ts +98 -0
  31. package/src/client/monaco/lsp/index.ts +15 -0
  32. package/src/client/monaco/lsp/providers.ts +2 -0
  33. package/src/client/monaco/theme.ts +15 -0
  34. package/src/client/searchSeed.ts +64 -0
  35. package/src/client/sidebar/panels/DebugPanel.ts +613 -0
  36. package/src/client/sidebar/panels/SearchPanel.ts +70 -15
  37. package/src/client/sidebar/panels/SvnPanel.ts +167 -23
  38. package/src/client/sidebar/panels/index.ts +19 -0
  39. package/src/client/sidebar/types.ts +2 -0
  40. package/src/client/styles/editor.css +186 -1
  41. package/src/client/svnActions.ts +18 -1
  42. package/src/client/svnLog.ts +3 -3
  43. package/src/client/svnStatus.ts +140 -2
  44. package/src/client/tabActions.ts +54 -0
  45. package/src/client/ui/EditorView.ts +711 -29
  46. package/src/client/ui/McpSettings.ts +29 -0
  47. package/src/client/ui/SideBySideDiff.tsx +214 -29
  48. package/src/client/ui/SvnDiffPanel.ts +21 -8
  49. package/src/client/ui/SvnLogDialog.ts +44 -2
  50. package/src/client/ui/SvnPatchDialog.ts +63 -0
  51. package/src/client/ui/SvnSumDialog.ts +77 -0
  52. package/src/client/ui/commandCatalog.ts +54 -0
  53. package/src/client/ui/horizontalWheel.ts +85 -0
  54. package/src/client/ui/svnExport.ts +71 -0
  55. package/src/client-primitives.d.ts +34 -0
  56. package/src/dap/configSnippets.ts +269 -0
  57. package/src/dap/discovery.ts +198 -0
  58. package/src/dap/launchConfig.ts +127 -0
  59. package/src/dap/manager.ts +588 -0
  60. package/src/dap/processList.ts +76 -0
  61. package/src/dap/protocol.ts +80 -0
  62. package/src/dap/provider.ts +49 -0
  63. package/src/dap/rpc.ts +192 -0
  64. package/src/dap/sourcePath.ts +82 -0
  65. package/src/fileOpenSettings.ts +3 -0
  66. package/src/index.ts +7 -3
  67. package/src/lsp/providers.ts +3 -2
  68. package/src/reveal.ts +31 -20
  69. package/src/rpc.ts +11 -3
  70. package/src/search/ripgrep.ts +127 -11
  71. package/src/shared/dap.ts +262 -0
  72. package/src/shared/editorLimit.ts +46 -0
  73. package/src/shared/keybindings.ts +10 -0
  74. package/src/shared/rpc.ts +40 -2
  75. package/src/shared/svn.ts +147 -0
  76. package/src/shared/svnActions.ts +20 -0
  77. package/src/svn.ts +320 -11
  78. package/src/workspace.ts +0 -75
@@ -0,0 +1,613 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * dsh-vscode-mode client — 侧边栏「调试」面板(对齐 CodeBuddy「运行和调试」视图)。
4
+ * 顶部工具条:配置下拉 + 启动/继续 + 暂停 + 单步组 + 停止(禁用逻辑镜像编辑器浮动条)。
5
+ * 四段折叠区(变量 / 监视 / 调用堆栈 / 断点,图2 顺序)+ 底部调试控制台(REPL);
6
+ * 每段头部可折叠(状态按 scope 持久化)且带悬浮动作按钮。
7
+ * 断点多方式管理(对齐 CodeBuddy 断点区):段头「新建断点」菜单(条件断点/记录点,
8
+ * 落在活动编辑器光标行)、禁用/启用所有、删除所有;断点行右键菜单(编辑条件/命中次数/
9
+ * 记录点/启停/删除)→ 行内展开编辑器(Enter 提交 / Escape 取消,字段语义同 BpWidget);
10
+ * 行内点展示适配器回执(未验证 = 空心点 + tooltip)。
11
+ * 数据自取:paused 时经 dapStore 拉栈/作用域/变量/监视求值;断点读写 dapStore 断点表;
12
+ * 输出流 = host poll 事件缓存(outputs)。host 不可达或未暂停时各段空态降级。
13
+ * 断点行整行可点:除启停 checkbox 与删除 × 外,点击任一处即打开并定位到该断点行。
14
+ * 作者 ddj 2026年09月29号 / 2026年09月21号
15
+ */
16
+ import React from 'react'
17
+ import { DAP_LAUNCH_REL } from '../../../shared/dap.js'
18
+ import { dapStore } from '../../dap/store.js'
19
+ import { bpJumpTargetOf } from '../../dap/breakpoints.js'
20
+ import { flattenVariableRows } from '../../dap/variableTree.js'
21
+ import { buildBpRowMenu } from '../../dap/bpRowMenu.js'
22
+ import { editorModelPathOf } from '../../dap/modelPath.js'
23
+ import { redistribute, loadSectionHeights, saveSectionHeights, type SplitKey } from '../../dap/panelSplit.js'
24
+ import { BP_MODE, MODES as BP_WIDGET_MODES, fieldOfMode } from '../../dap/BpWidget.js'
25
+ import { ContextMenu } from '../../ui/ContextMenu.js'
26
+ import { rpc } from '../../rpc.js'
27
+ import type { SidebarCtx } from '../types.js'
28
+
29
+ /** 变量展开缓存上限(ref → 子级;防长会话累积)。 */
30
+ const VAR_CACHE_CAP = 200
31
+ /** REPL 历史行上限。 */
32
+ const REPL_CAP = 200
33
+ /** 监视持久化 key 前缀(CACHE_KEY 无调试段,自管命名空间)。 */
34
+ const WATCH_KEY = 'edrv.dap.watch.'
35
+ /** 段折叠持久化 key 前缀(存折叠段名数组;损坏安全,默认全展开)。 */
36
+ const FOLD_KEY = 'edrv.dap.fold.'
37
+
38
+ /** 调试配置文件(工作区相对路径;⚙ 按钮跳转/新建目标;与 VS Code 的 .vscode/launch.json 互不干扰)。 */
39
+ const LAUNCH_REL = DAP_LAUNCH_REL
40
+ /** 新建调试配置模板(JSONC;示例按本机已装的 EmmyLua 适配器写)。 */
41
+ const LAUNCH_TEMPLATE = `{
42
+ // DSH 调试配置(插件专属 .dsh/launch.json,支持注释;改完整页刷新生效)
43
+ // type 来自本机已装扩展的 contributes.debuggers(EmmyLua:emmylua_attach / emmylua_launch / emmylua_new)
44
+ "version": "0.2.0",
45
+ "configurations": [
46
+ {
47
+ "name": "附加 Unity (xLua)",
48
+ "type": "emmylua_attach",
49
+ "request": "attach",
50
+ "processName": "Unity",
51
+ "captureLog": true
52
+ }
53
+ ]
54
+ }`
55
+
56
+ /** 相位徽标文案。 */
57
+ const PHASE_LABEL = { idle: '未启动', starting: '启动中…', waiting: '等待目标…', running: '运行中', paused: '已暂停', terminated: '已结束' }
58
+
59
+ /** 读取监视表达式列表(损坏安全)。 */
60
+ function loadWatches(scope) {
61
+ if (!scope) return []
62
+ try {
63
+ const raw = window.localStorage.getItem(WATCH_KEY + scope)
64
+ const list = raw ? JSON.parse(raw) : []
65
+ return Array.isArray(list) ? list.filter((item) => typeof item === 'string' && item.trim()) : []
66
+ } catch {
67
+ return []
68
+ }
69
+ }
70
+
71
+ /** 写监视表达式列表。 */
72
+ function saveWatches(scope, list) {
73
+ try { window.localStorage.setItem(WATCH_KEY + scope, JSON.stringify(list)) } catch { /* 忽略 */ }
74
+ }
75
+
76
+ /** 读折叠段名列表(损坏安全;非字符串项丢弃)。 */
77
+ function loadFolds(scope) {
78
+ if (!scope) return []
79
+ try {
80
+ const raw = window.localStorage.getItem(FOLD_KEY + scope)
81
+ const list = raw ? JSON.parse(raw) : []
82
+ return Array.isArray(list) ? list.filter((item) => typeof item === 'string' && item) : []
83
+ } catch {
84
+ return []
85
+ }
86
+ }
87
+
88
+ /** 写折叠段名列表(写失败静默)。 */
89
+ function saveFolds(scope, list) {
90
+ try { window.localStorage.setItem(FOLD_KEY + scope, JSON.stringify(list)) } catch { /* 忽略 */ }
91
+ }
92
+
93
+ /**
94
+ * 「调试」面板组件。
95
+ * @param ctx 侧栏上下文(openFileAt 跳转 / scope 持久化 / cwd / editor 光标)
96
+ */
97
+ export function DebugPanel({ ctx }) {
98
+ const scope = ctx?.scope || ''
99
+ const [, setTick] = React.useState(0)
100
+ React.useEffect(() => dapStore.subscribe(() => setTick((t) => t + 1)), [])
101
+ const snap = dapStore.getSnapshot()
102
+ const paused = snap.phase === 'paused'
103
+ const dapActive = snap.phase !== 'idle' && snap.phase !== 'terminated'
104
+
105
+ // 数据状态(paused 变化/停帧变化时重拉)
106
+ const [stacks, setStacks] = React.useState([])
107
+ const [scopes, setScopes] = React.useState([])
108
+ const [varCache, setVarCache] = React.useState(() => new Map())
109
+ const [expanded, setExpanded] = React.useState(() => new Set())
110
+ const [watches, setWatches] = React.useState(() => loadWatches(scope))
111
+ const [watchHits, setWatchHits] = React.useState({})
112
+ const [watchDraft, setWatchDraft] = React.useState('')
113
+ const [replLog, setReplLog] = React.useState([])
114
+ const [replDraft, setReplDraft] = React.useState('')
115
+ // 段折叠(folded 段名集合;localStorage 按 scope 持久)
116
+ const [folds, setFolds] = React.useState(() => new Set(loadFolds(scope)))
117
+ // 分栏高度(四段 splitview;拖动 sash 相邻重分配,按 scope 持久)
118
+ const [heights, setHeights] = React.useState(() => loadSectionHeights(scope))
119
+ const [splitDragging, setSplitDragging] = React.useState(false)
120
+ // 菜单态:断点行右键 / 段头「新建断点」下拉
121
+ const [bpMenu, setBpMenu] = React.useState(null)
122
+ const [newBpMenu, setNewBpMenu] = React.useState(null)
123
+ // 断点行内编辑态:{ path, line, mode, drafts }(drafts 三字段同 BpWidget 记忆口径)
124
+ const [bpEdit, setBpEdit] = React.useState(null)
125
+ // 进程候选选择(candidates 非空时镜像浮动条的选择模式)
126
+ const [pickPid, setPickPid] = React.useState(0)
127
+ const watchInputRef = React.useRef(null)
128
+ const stackTopKey = paused && snap.topFrame ? snap.topFrame.file + ':' + snap.topFrame.line : ''
129
+
130
+ // 工作区同步 + 配置预拉(幂等;与 EditorView 的 cwd 副作用互不冲突)
131
+ React.useEffect(() => {
132
+ if (!ctx?.cwd) return
133
+ dapStore.setWorkspace(ctx.cwd)
134
+ void dapStore.ensureConfigs()
135
+ }, [ctx?.cwd])
136
+
137
+ // paused:拉调用栈 + 作用域 + 首帧变量 + 监视求值;非 paused 清空(reloadTick 手动刷新)
138
+ const [reloadTick, setReloadTick] = React.useState(0)
139
+ React.useEffect(() => { if (!paused) {
140
+ setStacks([]); setScopes([]); setVarCache(new Map()); setExpanded(new Set()); setWatchHits({})
141
+ return
142
+ }
143
+ let alive = true
144
+ const load = async () => {
145
+ const frames = await dapStore.stackTrace()
146
+ if (!alive) return
147
+ setStacks(frames)
148
+ const list = await dapStore.scopes(snap.selectedFrameId)
149
+ if (!alive) return
150
+ setScopes(list)
151
+ const first = list[0]
152
+ if (first?.ref) {
153
+ const vars = await dapStore.variables(first.ref)
154
+ if (!alive) return
155
+ setVarCache((prev) => new Map(prev).set(first.ref, vars))
156
+ setExpanded((prev) => new Set(prev).add(first.ref))
157
+ }
158
+ const hits = {}
159
+ for (const expr of watches) {
160
+ const r = await dapStore.evaluate(expr, snap.selectedFrameId)
161
+ hits[expr] = r
162
+ }
163
+ if (alive) setWatchHits(hits)
164
+ }
165
+ void load()
166
+ return () => { alive = false }
167
+ }, [paused, stackTopKey, snap.selectedFrameId, watches, reloadTick])
168
+
169
+ // 展开变量节点(懒拉子级)
170
+ const expandVar = async (ref) => {
171
+ setExpanded((prev) => {
172
+ const next = new Set(prev)
173
+ if (next.has(ref)) next.delete(ref)
174
+ else next.add(ref)
175
+ return next
176
+ })
177
+ if (!varCache.has(ref)) {
178
+ const vars = await dapStore.variables(ref)
179
+ setVarCache((prev) => {
180
+ const next = new Map(prev)
181
+ next.set(ref, vars)
182
+ if (next.size > VAR_CACHE_CAP) next.delete(next.keys().next().value)
183
+ return next
184
+ })
185
+ }
186
+ }
187
+
188
+ const addWatch = () => {
189
+ const expr = watchDraft.trim()
190
+ if (!expr || watches.includes(expr)) { setWatchDraft(''); return }
191
+ const next = [...watches, expr]
192
+ setWatches(next)
193
+ saveWatches(scope, next)
194
+ setWatchDraft('')
195
+ if (paused) void dapStore.evaluate(expr, snap.selectedFrameId).then((r) => setWatchHits((prev) => ({ ...prev, [expr]: r })))
196
+ }
197
+
198
+ const removeWatch = (expr) => {
199
+ const next = watches.filter((item) => item !== expr)
200
+ setWatches(next)
201
+ saveWatches(scope, next)
202
+ }
203
+
204
+ /** 移除全部监视(段头动作;持久化与命中缓存同步清)。 */
205
+ const clearWatches = () => {
206
+ setWatches([])
207
+ saveWatches(scope, [])
208
+ setWatchHits({})
209
+ }
210
+
211
+ /** 聚焦监视新增输入(段折叠时先展开,二次点击聚焦)。 */
212
+ const focusWatchInput = () => {
213
+ if (folds.has('watch')) { toggleFold('watch'); return }
214
+ watchInputRef.current?.focus?.()
215
+ }
216
+
217
+ const runRepl = async () => {
218
+ const expr = replDraft.trim()
219
+ if (!expr) return
220
+ setReplDraft('')
221
+ const r = await dapStore.evaluate(expr, snap.selectedFrameId)
222
+ setReplLog((prev) => {
223
+ const next = [...prev, { expr, result: r.result, type: r.type }]
224
+ return next.length > REPL_CAP ? next.slice(next.length - REPL_CAP) : next
225
+ })
226
+ }
227
+
228
+ /** 切换段折叠(持久化到 localStorage)。 */
229
+ const toggleFold = (key) => {
230
+ setFolds((prev) => {
231
+ const next = new Set(prev)
232
+ if (next.has(key)) next.delete(key)
233
+ else next.add(key)
234
+ saveFolds(scope, [...next])
235
+ return next
236
+ })
237
+ }
238
+
239
+ // 分栏拖动:进行中的清理句柄(pointerup 与组件卸载双路径移除 window 监听)
240
+ const splitCleanupRef = React.useRef(null)
241
+ React.useEffect(() => () => {
242
+ const cleanup = splitCleanupRef.current
243
+ if (cleanup) cleanup()
244
+ }, [])
245
+
246
+ /** 段间 sash 拖动:以起点高度为基准按 dy 相邻重分配;pointerup 持久化(镜像 dapbar 拖动模式)。 */
247
+ const startSplitDrag = (above: SplitKey, below: SplitKey) => (e) => {
248
+ e.preventDefault()
249
+ const startY = e.clientY
250
+ const startHeights = { ...heights }
251
+ const onMove = (ev) => {
252
+ setHeights(redistribute(startHeights, above, below, ev.clientY - startY))
253
+ }
254
+ const detach = () => {
255
+ window.removeEventListener('pointermove', onMove)
256
+ window.removeEventListener('pointerup', onUp)
257
+ splitCleanupRef.current = null
258
+ setSplitDragging(false)
259
+ }
260
+ const onUp = () => {
261
+ detach()
262
+ setHeights((prev) => { saveSectionHeights(scope, prev); return prev })
263
+ }
264
+ splitCleanupRef.current = detach
265
+ window.addEventListener('pointermove', onMove)
266
+ window.addEventListener('pointerup', onUp)
267
+ setSplitDragging(true)
268
+ }
269
+
270
+ // 断点启停/删除/跳转(checkbox + 红点 + 文件名 + 目录 + 行号徽标;整行可点跳转)
271
+ const cwd = ctx?.cwd || dapStore.workspace()
272
+ const bpMap = dapStore.bpMapOf(scope)
273
+ const bpRows = Object.entries(bpMap).flatMap(([path, list]) => list.map((entry) => ({ path, entry })))
274
+ .sort((a, b) => a.path.localeCompare(b.path) || a.entry.line - b.entry.line)
275
+ const allBpEnabled = bpRows.length > 0 && bpRows.every((row) => row.entry.enabled)
276
+
277
+ const toggleBpEnabled = (row) => dapStore.setBpEnabled(scope, cwd, row.path, row.entry.line, !row.entry.enabled)
278
+ const removeBp = (row) => dapStore.toggleAt(scope, cwd, row.path, row.entry.line)
279
+ /** 删除所有断点(confirm 守卫;逐文件同步空载荷清空 Monaco 装饰与 host 断点)。 */
280
+ const removeAllBps = () => {
281
+ if (ctx?.confirm && !ctx.confirm('删除所有断点?')) return
282
+ dapStore.removeAllBps(scope, cwd)
283
+ setBpEdit(null)
284
+ }
285
+ // 跳转目标经纯函数守卫(路径缺失/行号非法的行不发起导航);openFileAt 内含 recordNav,
286
+ // 故整行只挂这一个 handler,文件名按钮不再自带 onClick(防双触发双记导航历史)。
287
+ const jumpBp = (row) => {
288
+ const target = bpJumpTargetOf(row)
289
+ if (target) ctx?.openFileAt?.(target.path, target.line, target.column)
290
+ }
291
+
292
+ // 断点行内编辑器(字段语义同 BpWidget:三字段独立草稿,提交全量落 store,空串清字段)
293
+ const autoBpModeOf = (entry) => (
294
+ entry?.logMessage ? BP_MODE.LOG_MESSAGE : entry?.hitCondition ? BP_MODE.HIT_COUNT : BP_MODE.EXPRESSION
295
+ )
296
+ const openBpRowEditor = (path, line, entry, mode) => {
297
+ setBpEdit({
298
+ path, line,
299
+ mode: typeof mode === 'number' ? mode : autoBpModeOf(entry),
300
+ drafts: {
301
+ condition: entry?.condition || '',
302
+ hitCondition: entry?.hitCondition || '',
303
+ logMessage: entry?.logMessage || '',
304
+ },
305
+ })
306
+ }
307
+ const commitBpEdit = () => {
308
+ if (!bpEdit) return
309
+ dapStore.applyBpFields(scope, cwd, bpEdit.path, bpEdit.line, { ...bpEdit.drafts })
310
+ setBpEdit(null)
311
+ ctx?.notify?.('断点已更新')
312
+ }
313
+
314
+ /** 断点行右键菜单动作分发(菜单数据由 buildBpRowMenu 产出)。 */
315
+ const runBpRowAction = (action, row) => {
316
+ if (action === 'remove') { removeBp(row); return }
317
+ if (action === 'enable' || action === 'disable') {
318
+ dapStore.setBpEnabled(scope, cwd, row.path, row.entry.line, action === 'enable')
319
+ return
320
+ }
321
+ const mode = action === 'edit-condition' ? BP_MODE.EXPRESSION
322
+ : action === 'edit-hit-count' ? BP_MODE.HIT_COUNT
323
+ : action === 'edit-logpoint' ? BP_MODE.LOG_MESSAGE
324
+ : undefined
325
+ openBpRowEditor(row.path, row.entry.line, row.entry, mode)
326
+ }
327
+
328
+ /** 段头「新建断点」:条件断点/记录点落在活动编辑器光标行(已有断点则直接编辑不翻转)。 */
329
+ const addBpAtCursor = (mode) => {
330
+ const ed = ctx?.editor?.()
331
+ const path = editorModelPathOf(ed)
332
+ const line = ed?.getPosition?.()?.lineNumber
333
+ if (!path || !line || !Number.isFinite(line)) { ctx?.notify?.('无活动文件,无法添加断点'); return }
334
+ const existing = (dapStore.bpMapOf(scope)[path] ?? []).find((it) => it.line === line)
335
+ if (!existing) dapStore.toggleAt(scope, cwd, path, line)
336
+ const entry = (dapStore.bpMapOf(scope)[path] ?? []).find((it) => it.line === line)
337
+ if (entry) openBpRowEditor(path, line, entry, mode)
338
+ }
339
+
340
+ /** ⚙ 打开/新建调试配置:存在即跳转;缺失经确认后写模板(edrv.save 无 rev 仅用于新建,绝不覆盖已有文件)。 */
341
+ const openLaunchConfig = async () => {
342
+ if (!ctx?.cwd) { ctx?.notify?.('无工作区,无法定位 ' + LAUNCH_REL); return }
343
+ try {
344
+ const probe = await rpc('edrv.read', { sessionId: ctx?.sessionId, path: LAUNCH_REL }).catch(() => null)
345
+ if (probe?.ok) { ctx?.openFile?.(LAUNCH_REL); return }
346
+ } catch { /* 探活失败按缺失处理 */ }
347
+ if (ctx?.confirm && !ctx.confirm('未找到 ' + LAUNCH_REL + ',是否创建调试配置模板?')) return
348
+ try {
349
+ const saved = await rpc('edrv.save', { sessionId: ctx?.sessionId, path: LAUNCH_REL, content: LAUNCH_TEMPLATE })
350
+ if (!saved?.ok) { ctx?.notify?.(saved?.error || '创建 ' + LAUNCH_REL + ' 失败'); return }
351
+ void dapStore.ensureConfigs(true)
352
+ ctx?.openFile?.(LAUNCH_REL)
353
+ ctx?.notify?.('已创建 ' + LAUNCH_REL)
354
+ } catch (error) {
355
+ ctx?.notify?.('创建 ' + LAUNCH_REL + ' 失败:' + String(error))
356
+ }
357
+ }
358
+
359
+ /** 段头动作按钮(hover 显隐,样式见 .edrv-dapseg-act)。 */
360
+ const segAct = (key, label, title, onClick) => React.createElement('button', {
361
+ key, className: 'edrv-dapseg-act', title,
362
+ onClick: (e) => { e.stopPropagation(); onClick(e) },
363
+ }, label)
364
+
365
+ /** 折叠段壳:chevron + 标题 + 悬浮动作区 + 可折叠主体(高度由分栏 state 内联接管)。 */
366
+ const section = (title, key, variant, actions, children) => {
367
+ const folded = folds.has(key)
368
+ return React.createElement('div', { className: 'edrv-dapseg edrv-dapseg-' + variant, key },
369
+ React.createElement('div', { className: 'edrv-dapseg-head', onClick: () => toggleFold(key) },
370
+ React.createElement('span', { className: 'edrv-dapseg-chevron', title: folded ? '展开' : '折叠' }, folded ? '▸' : '▾'),
371
+ React.createElement('span', { className: 'edrv-dapseg-title' }, title),
372
+ React.createElement('span', { style: { flex: 1 } }),
373
+ ...(folded ? [] : (actions || []))),
374
+ ...(folded ? [] : [React.createElement('div', {
375
+ className: 'edrv-dapseg-body', style: { height: (heights[key] ?? 200) + 'px' },
376
+ }, ...children)]))
377
+ }
378
+
379
+ /** 段间分隔条(两侧段都展开时才渲染;拖动调整相邻两段高度占比)。 */
380
+ const sash = (above: SplitKey, below: SplitKey) => (folds.has(above) || folds.has(below) ? null : React.createElement('div', {
381
+ key: 'sash-' + above + '-' + below,
382
+ className: 'edrv-dapseg-sash' + (splitDragging ? ' dragging' : ''),
383
+ title: '拖动调整上下两段高度占比',
384
+ onPointerDown: startSplitDrag(above, below),
385
+ }))
386
+
387
+ const renderVarRow = (row) => {
388
+ const v = row.variable
389
+ const depth = row.depth + 1
390
+ return React.createElement('div', { key: row.key, className: 'edrv-dapvar', style: { paddingLeft: 8 + depth * 12 } },
391
+ v.ref > 0
392
+ ? React.createElement('button', {
393
+ className: 'edrv-dapvar-tw', title: expanded.has(v.ref) ? '折叠' : '展开',
394
+ onClick: () => { void expandVar(v.ref) },
395
+ }, expanded.has(v.ref) ? '▾' : '▸')
396
+ : React.createElement('span', { className: 'edrv-dapvar-tw' }, ''),
397
+ React.createElement('span', { className: 'edrv-dapvar-name', title: v.name }, v.name),
398
+ React.createElement('span', { className: 'edrv-dapvar-val', title: v.value }, v.value),
399
+ (v.type ? React.createElement('span', { className: 'edrv-dapvar-type' }, v.type) : null))
400
+ }
401
+
402
+ const stackRows = stacks.length
403
+ ? stacks.map((f) => React.createElement('div', {
404
+ key: f.id,
405
+ className: 'edrv-dapframe' + (f.id === snap.selectedFrameId ? ' selected' : ''),
406
+ title: f.rawFile,
407
+ onClick: () => {
408
+ dapStore.selectFrame(f.id)
409
+ const target = f.file || f.rawFile
410
+ if (target) ctx?.openFileAt?.(target, f.line, 1)
411
+ },
412
+ },
413
+ React.createElement('span', { className: 'edrv-dapframe-name' }, f.name || '(anonymous)'),
414
+ React.createElement('span', { className: 'edrv-dapframe-loc' }, (f.file || f.rawFile) + ':' + f.line)))
415
+ : [React.createElement('div', { key: 'empty', className: 'edrv-dapempty' }, paused ? '(无调用栈)' : '暂停时显示调用堆栈')]
416
+
417
+ const scopeRows = scopes.flatMap((s) => [
418
+ React.createElement('div', { key: 'scope' + s.name, className: 'edrv-dapscope' }, s.name),
419
+ ...flattenVariableRows(varCache.get(s.ref) ?? [], expanded, varCache, { maxRows: 800 }).map(renderVarRow),
420
+ ])
421
+ const varRows = scopes.length ? scopeRows : [React.createElement('div', { key: 'e', className: 'edrv-dapempty' }, '暂停时显示变量')]
422
+
423
+ const watchRows = watches.flatMap((expr) => {
424
+ const hit = watchHits[expr]
425
+ const rows = [React.createElement('div', { key: expr, className: 'edrv-dapwatch' },
426
+ React.createElement('button', { className: 'edrv-dapwatch-x', title: '移除监视', onClick: () => removeWatch(expr) }, '×'),
427
+ (hit?.ref > 0
428
+ ? React.createElement('button', {
429
+ className: 'edrv-dapvar-tw', title: expanded.has(hit.ref) ? '折叠 table' : '展开 table',
430
+ onClick: () => { void expandVar(hit.ref) },
431
+ }, expanded.has(hit.ref) ? '▾' : '▸')
432
+ : React.createElement('span', { className: 'edrv-dapvar-tw' }, '')),
433
+ React.createElement('span', { className: 'edrv-dapvar-name', title: expr }, expr),
434
+ React.createElement('span', { className: 'edrv-dapvar-val', title: hit?.result || '' }, hit ? '= ' + hit.result : '= 不可用'),
435
+ (hit?.type ? React.createElement('span', { className: 'edrv-dapvar-type' }, hit.type) : null))]
436
+ if (hit?.ref > 0 && expanded.has(hit.ref)) {
437
+ rows.push(...flattenVariableRows(varCache.get(hit.ref) ?? [], expanded, varCache, { maxRows: 500 }).map(renderVarRow))
438
+ }
439
+ return rows
440
+ })
441
+ const watchSection = section('监视', 'watch', 'watch', [
442
+ segAct('watch-add', '+', '新增监视表达式', focusWatchInput),
443
+ segAct('watch-clear', '🗑', '移除全部监视', clearWatches),
444
+ ], [
445
+ ...watchRows,
446
+ React.createElement('div', { key: 'add', className: 'edrv-dapwatch-add' },
447
+ React.createElement('input', {
448
+ ref: watchInputRef,
449
+ className: 'edrv-daprepl-in', placeholder: '新增监视表达式,回车确认',
450
+ value: watchDraft, onChange: (e) => setWatchDraft(e.target.value),
451
+ onKeyDown: (e) => { if (e.key === 'Enter') addWatch() },
452
+ })),
453
+ ])
454
+
455
+ // 断点行 + 行内编辑行(编辑中的行紧随其后展开,对齐 VS Code 断点视图的行内编辑形态)
456
+ const bpSectionChildren = bpRows.length
457
+ ? bpRows.flatMap((row) => {
458
+ const base = row.path.split('/').pop() || row.path
459
+ const dir = row.path.includes('/') ? row.path.slice(0, row.path.lastIndexOf('/')) : ''
460
+ const ack = dapStore.ackOf(row.path, row.entry.line)
461
+ const unverified = Boolean(ack && !ack.verified)
462
+ const editing = bpEdit && bpEdit.path === row.path && bpEdit.line === row.entry.line
463
+ const rows = [React.createElement('div', {
464
+ key: row.path + ':' + row.entry.line, className: 'edrv-dapbp',
465
+ title: '跳转到 ' + row.path + ':' + row.entry.line
466
+ + (unverified && ack.message ? '(' + ack.message + ')' : ''),
467
+ onClick: () => jumpBp(row),
468
+ onContextMenu: (e) => { e.preventDefault(); setBpMenu({ x: e.clientX, y: e.clientY, row }) },
469
+ },
470
+ React.createElement('input', {
471
+ type: 'checkbox', checked: row.entry.enabled, title: row.entry.enabled ? '禁用断点' : '启用断点',
472
+ onChange: () => toggleBpEnabled(row),
473
+ // 启停不连带跳转(整行可点后必须阻断冒泡)
474
+ onClick: (e) => e.stopPropagation(),
475
+ }),
476
+ React.createElement('span', {
477
+ className: 'edrv-dapbp-dot'
478
+ + (row.entry.enabled ? '' : ' off')
479
+ + (unverified ? ' unverified' : ''),
480
+ title: unverified && ack.message ? ack.message : undefined,
481
+ }),
482
+ React.createElement('button', { className: 'edrv-dapbp-file', title: row.path }, base),
483
+ React.createElement('span', { className: 'edrv-dapbp-dir', title: dir }, dir),
484
+ React.createElement('span', { className: 'edrv-dapbp-line' }, row.entry.line),
485
+ React.createElement('button', {
486
+ className: 'edrv-dapbp-x', title: '删除断点',
487
+ // 删除不连带跳转
488
+ onClick: (e) => { e.stopPropagation(); removeBp(row) },
489
+ }, '×'))]
490
+ if (editing) {
491
+ const fieldKey = fieldOfMode(bpEdit.mode)
492
+ const modeItem = BP_WIDGET_MODES.find((m) => m.mode === bpEdit.mode) ?? BP_WIDGET_MODES[0]
493
+ rows.push(React.createElement('div', { key: row.path + ':' + row.entry.line + ':edit', className: 'edrv-dapbp-edit' },
494
+ React.createElement('select', {
495
+ className: 'edrv-bpwidget-mode', title: '断点类型', value: String(bpEdit.mode),
496
+ onChange: (e) => setBpEdit({ ...bpEdit, mode: Number(e.target.value) }),
497
+ },
498
+ BP_WIDGET_MODES.filter((m) => m.mode !== BP_MODE.WAIT).map((m) => React.createElement('option', { key: m.mode, value: String(m.mode) }, m.label))),
499
+ React.createElement('input', {
500
+ className: 'edrv-bpwidget-input', autoFocus: true, spellCheck: false,
501
+ placeholder: modeItem?.placeholder ?? '',
502
+ hidden: fieldKey === null,
503
+ value: fieldKey ? (bpEdit.drafts[fieldKey] || '') : '',
504
+ onChange: (e) => setBpEdit({ ...bpEdit, drafts: { ...bpEdit.drafts, [fieldKey]: e.target.value } }),
505
+ onKeyDown: (e) => {
506
+ if (e.key === 'Enter') { e.preventDefault(); commitBpEdit(); return }
507
+ if (e.key === 'Escape') { e.preventDefault(); setBpEdit(null) }
508
+ },
509
+ })))
510
+ }
511
+ return rows
512
+ })
513
+ : [React.createElement('div', { key: 'e', className: 'edrv-dapempty' }, '在编辑器行首点击或 F9 添加断点')]
514
+
515
+ const bpSection = section('断点', 'breakpoints', 'breakpoints', [
516
+ segAct('bp-new', '新建断点 ▾', '添加条件断点 / 记录点(落在当前编辑器光标行)', (e) => setNewBpMenu({ x: e.clientX, y: e.clientY })),
517
+ (bpRows.length ? segAct('bp-toggle', allBpEnabled ? '⊘' : '◉', allBpEnabled ? '禁用所有断点' : '启用所有断点', () => dapStore.setAllBpEnabled(scope, cwd, !allBpEnabled)) : null),
518
+ (bpRows.length ? segAct('bp-clear', '🗑', '删除所有断点', removeAllBps) : null),
519
+ ], bpSectionChildren)
520
+
521
+ const outputRows = dapStore.outputs().slice(-60).map((ev, idx) => React.createElement('div', { key: ev.seq + ':' + idx, className: 'edrv-daprepl-line out' }, ev.text?.trimEnd() || ''))
522
+
523
+ // 顶部工具条:配置下拉(option 可用性置灰逻辑镜像编辑器浮动条)+ 控制按钮组
524
+ const configReady = snap.configs.some((c) => snap.adapters.length === 0 || snap.adapters.find((a) => a.type === c.type)?.available === true)
525
+ const toolbarBtn = (key, label, title, disabled, onClick) => React.createElement('button', {
526
+ key, className: 'edrv-dappanel-run', title, disabled: disabled === true, onClick,
527
+ }, label)
528
+
529
+ // 断点行右键菜单(面板内行级操作:编辑字段 / 启停 / 删除;同 EditorView dapBpMenuEl 装配模式)
530
+ const bpMenuEl = bpMenu
531
+ ? React.createElement(ContextMenu, {
532
+ x: bpMenu.x, y: bpMenu.y, onClose: () => setBpMenu(null),
533
+ entries: buildBpRowMenu({ enabled: bpMenu.row.entry.enabled }).map((item) => ({
534
+ id: item.id, label: item.label, separator: item.separator,
535
+ onClick: () => runBpRowAction(item.id, bpMenu.row),
536
+ })),
537
+ })
538
+ : null
539
+ // 段头「新建断点」下拉(条件断点 / 记录点)
540
+ const newBpMenuEl = newBpMenu
541
+ ? React.createElement(ContextMenu, {
542
+ x: newBpMenu.x, y: newBpMenu.y, onClose: () => setNewBpMenu(null),
543
+ entries: [
544
+ { id: 'add-conditional', label: '添加条件断点...', onClick: () => addBpAtCursor(BP_MODE.EXPRESSION) },
545
+ { id: 'add-logpoint', label: '添加记录点...', onClick: () => addBpAtCursor(BP_MODE.LOG_MESSAGE) },
546
+ ],
547
+ })
548
+ : null
549
+
550
+ return React.createElement('div', { className: 'edrv-dappanel' },
551
+ React.createElement('div', { className: 'edrv-dappanel-head' },
552
+ ...(snap.candidates
553
+ ? [
554
+ React.createElement('select', {
555
+ key: 'pick', className: 'edrv-dappanel-cfg-select', title: '选择附加目标进程(processName 命中多个候选)',
556
+ value: String(pickPid || snap.candidates[0]?.pid || ''),
557
+ onChange: (e) => setPickPid(Number(e.target.value)),
558
+ },
559
+ snap.candidates.map((it) => React.createElement('option', { key: it.pid, value: String(it.pid) },
560
+ it.pid + ' : ' + it.name + (it.title ? ' — ' + it.title.slice(0, 44) : '')))),
561
+ toolbarBtn('pick-ok', '✓', '附加到选中进程', false, () => dapStore.confirmPick(pickPid || snap.candidates![0]?.pid || 0)),
562
+ toolbarBtn('pick-cancel', '✕', '取消', false, () => dapStore.cancelPick()),
563
+ ]
564
+ : [React.createElement('select', {
565
+ key: 'cfg', className: 'edrv-dappanel-cfg-select',
566
+ title: '调试配置(读取工作区 .dsh/launch.json;适配器由扩展清单 contributes.debuggers 提供)',
567
+ value: snap.selectedConfig ?? '',
568
+ onChange: (e) => dapStore.selectConfig(e.target.value),
569
+ },
570
+ snap.configs.length
571
+ ? snap.configs.map((c) => {
572
+ const adapter = snap.adapters.find((a) => a.type === c.type)
573
+ const ready = snap.adapters.length === 0 || adapter?.available === true
574
+ const suffix = ready ? '' : '(' + (adapter?.reason || '未发现该类型适配器') + ')'
575
+ return React.createElement('option', { key: c.name, value: c.name, disabled: !ready }, c.name + suffix)
576
+ })
577
+ : React.createElement('option', { value: '' }, '无可用调试配置'))]),
578
+ toolbarBtn('run', '▶', paused ? '继续(F5)' : '启动调试(F5)', !paused && (!configReady || dapActive), () => dapStore.startOrContinue()),
579
+ toolbarBtn('cfg-gear', '⚙', '打开/新建调试配置(' + LAUNCH_REL + ')', !ctx?.cwd, () => { void openLaunchConfig() }),
580
+ toolbarBtn('pause', '⏸', '暂停', !dapActive, () => { void dapStore.action('pause') }),
581
+ toolbarBtn('next', '⤵', '单步跳过(F10)', !paused, () => { void dapStore.action('next') }),
582
+ toolbarBtn('stepin', '↓', '单步步入(F11)', !paused, () => { void dapStore.action('stepIn') }),
583
+ toolbarBtn('stepout', '↑', '单步步出(Shift+F11)', !paused, () => { void dapStore.action('stepOut') }),
584
+ toolbarBtn('stop', '⏹', '停止(Shift+F5)', !dapActive, () => { void dapStore.stop() }),
585
+ React.createElement('span', { className: 'edrv-dappanel-phase' + (paused ? ' paused' : '') }, PHASE_LABEL[snap.phase] || snap.phase),
586
+ (snap.pid ? React.createElement('span', { className: 'edrv-dappanel-pid' }, 'pid ' + snap.pid) : null),
587
+ React.createElement('span', { style: { flex: 1 } }),
588
+ (snap.error ? React.createElement('span', { className: 'edrv-dappanel-err', title: snap.error }, '⚠') : null)),
589
+ section('变量', 'variables', 'variables', [
590
+ segAct('var-reload', '⟳', '刷新变量', () => setReloadTick((t) => t + 1)),
591
+ ], varRows),
592
+ sash('variables', 'watch'),
593
+ watchSection,
594
+ sash('watch', 'stack'),
595
+ section('调用堆栈', 'stack', 'stack', [], stackRows),
596
+ sash('stack', 'breakpoints'),
597
+ bpSection,
598
+ React.createElement('div', { className: 'edrv-daprepl' },
599
+ React.createElement('div', { className: 'edrv-dapseg-head' }, '调试控制台'),
600
+ React.createElement('div', { className: 'edrv-daprepl-log' },
601
+ ...replLog.map((row, idx) => React.createElement('div', { key: idx, className: 'edrv-daprepl-line' },
602
+ React.createElement('span', { className: 'edrv-dapvar-name' }, row.expr),
603
+ React.createElement('span', { className: 'edrv-dapvar-val' }, '= ' + row.result))),
604
+ ...outputRows),
605
+ React.createElement('input', {
606
+ className: 'edrv-daprepl-in', placeholder: paused ? '输入表达式求值,回车执行(如 self.itemId)' : '暂停时才可求值',
607
+ disabled: !paused, value: replDraft,
608
+ onChange: (e) => setReplDraft(e.target.value),
609
+ onKeyDown: (e) => { if (e.key === 'Enter') void runRepl() },
610
+ })),
611
+ bpMenuEl,
612
+ newBpMenuEl)
613
+ }