dsh-vscode-mode 0.5.3 → 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 (65) hide show
  1. package/README.md +55 -2
  2. package/lib/client.js +5173 -355
  3. package/lib/client.js.map +1 -1
  4. package/lib/index.js +2356 -126
  5. package/lib/index.js.map +1 -1
  6. package/package.json +1 -1
  7. package/src/client/dap/BpWidget.ts +262 -0
  8. package/src/client/dap/bpMenu.ts +91 -0
  9. package/src/client/dap/bpRowMenu.ts +53 -0
  10. package/src/client/dap/breakpoints.ts +222 -0
  11. package/src/client/dap/decorate.ts +128 -0
  12. package/src/client/dap/hintLine.ts +29 -0
  13. package/src/client/dap/hover.ts +166 -0
  14. package/src/client/dap/hoverMode.ts +159 -0
  15. package/src/client/dap/hoverTree.ts +722 -0
  16. package/src/client/dap/launchInsert.ts +229 -0
  17. package/src/client/dap/launchSnippetProvider.ts +206 -0
  18. package/src/client/dap/modelPath.ts +23 -0
  19. package/src/client/dap/panelSplit.ts +105 -0
  20. package/src/client/dap/pidPick.ts +24 -0
  21. package/src/client/dap/store.ts +571 -0
  22. package/src/client/dap/toolbarDrag.ts +51 -0
  23. package/src/client/dap/trace.ts +22 -0
  24. package/src/client/dap/variableTree.ts +62 -0
  25. package/src/client/index.ts +9 -0
  26. package/src/client/monaco/lsp/index.ts +15 -0
  27. package/src/client/monaco/lsp/providers.ts +2 -0
  28. package/src/client/monaco/theme.ts +15 -0
  29. package/src/client/sidebar/panels/DebugPanel.ts +613 -0
  30. package/src/client/sidebar/panels/SvnPanel.ts +167 -23
  31. package/src/client/sidebar/panels/index.ts +19 -0
  32. package/src/client/sidebar/types.ts +2 -0
  33. package/src/client/styles/editor.css +176 -0
  34. package/src/client/svnActions.ts +18 -1
  35. package/src/client/svnLog.ts +3 -3
  36. package/src/client/svnStatus.ts +140 -2
  37. package/src/client/ui/EditorView.ts +544 -16
  38. package/src/client/ui/SideBySideDiff.tsx +214 -29
  39. package/src/client/ui/SvnDiffPanel.ts +21 -8
  40. package/src/client/ui/SvnLogDialog.ts +44 -2
  41. package/src/client/ui/SvnPatchDialog.ts +63 -0
  42. package/src/client/ui/SvnSumDialog.ts +77 -0
  43. package/src/client/ui/commandCatalog.ts +49 -0
  44. package/src/client/ui/svnExport.ts +71 -0
  45. package/src/dap/configSnippets.ts +269 -0
  46. package/src/dap/discovery.ts +198 -0
  47. package/src/dap/launchConfig.ts +127 -0
  48. package/src/dap/manager.ts +588 -0
  49. package/src/dap/processList.ts +76 -0
  50. package/src/dap/protocol.ts +80 -0
  51. package/src/dap/provider.ts +49 -0
  52. package/src/dap/rpc.ts +192 -0
  53. package/src/dap/sourcePath.ts +82 -0
  54. package/src/index.ts +7 -3
  55. package/src/lsp/providers.ts +3 -2
  56. package/src/reveal.ts +31 -20
  57. package/src/rpc.ts +11 -3
  58. package/src/search/ripgrep.ts +127 -11
  59. package/src/shared/dap.ts +262 -0
  60. package/src/shared/keybindings.ts +7 -0
  61. package/src/shared/rpc.ts +40 -2
  62. package/src/shared/svn.ts +147 -0
  63. package/src/shared/svnActions.ts +20 -0
  64. package/src/svn.ts +320 -11
  65. package/src/workspace.ts +0 -75
@@ -0,0 +1,722 @@
1
+ /**
2
+ * dsh-vscode-mode client — 暂停态 hover 变量树(成员全量直显 + 折叠展开 + 子级懒加载)。
3
+ *
4
+ * 形态:复用 Monaco 内置 hover 面板。调试内容由 hover.ts 渲染成**单个 HTML 块**
5
+ * (`[data-edrv-dap]`),本模块负责:
6
+ * - 树容器的 click/mousedown 委托(展开/折叠,子级懒加载)+ 重新渲染后的兜底绑定;
7
+ * - hover 层:给行打 `debug`/`lsp` 归属标记、注入贴底常驻的 Alt 提示层、按 Alt 状态切换显隐
8
+ * (纯显示层切换:不动 provider、不触发重算,因此不依赖 Monaco 的重算路径)。
9
+ * 约定(与 src/client/dap/variableTree.ts 一致):循环 ref 不再展开、限制深度与总行数。
10
+ *
11
+ * ⚠️ 依赖内置 hover 容器 `.monaco-hover` 与 markdown 的 supportHtml(已在本仓 vendored
12
+ * Monaco 构建中确认存在);找不到容器时退化为静态行,不抛错、不影响其它功能。
13
+ * 作者 ddj 2026年09月21号
14
+ */
15
+ import { dapStore } from './store.js'
16
+ import { dapTrace } from './trace.js'
17
+ import type { DapVariableView } from '../../shared/dap.js'
18
+
19
+ // --region 常量
20
+ /** 最大展开深度(到达后不再给展开箭头)。 */
21
+ export const MAX_DEPTH = 8
22
+ /** 单棵树的行数上限(DOM 保护;超出给上限提示行)。 */
23
+ export const MAX_ROWS = 1000
24
+ /** 值截断长度(完整值写入 title)。 */
25
+ export const VALUE_CLIP = 120
26
+ /** 树容器标记(DOM 查找与重新绑定用)。 */
27
+ export const TREE_ATTR = 'data-edrv-tree'
28
+ /** 调试内容块标记(hover.ts 渲染的单块 HTML 根节点)。 */
29
+ export const BLOCK_ATTR = 'data-edrv-dap'
30
+ /** 贴底提示层标记。 */
31
+ export const HINT_ATTR = 'data-edrv-hint'
32
+ /** 承载提示层的 hover 容器标记(用于预留同高内边距,避免提示层遮住末行)。 */
33
+ export const HINT_HOST_ATTR = 'data-edrv-hint-host'
34
+ /** LSP 占位行标记(Alt 按住但真实 LSP 行尚未返回时顶位显示)。 */
35
+ export const PLACEHOLDER_ATTR = 'data-edrv-placeholder'
36
+ /** LSP 占位行文案。 */
37
+ export const LSP_PLACEHOLDER_TEXT = 'LSP 信息加载中…'
38
+ /** 行归属标记(`debug` = 调试块所在行,其余为 `lsp`)。 */
39
+ const ROW_ATTR = 'data-edrv-row'
40
+ /**
41
+ * 属性钩子(折叠按钮/名称/等号/值/提示行/行本身)。
42
+ * ⚠️ 必须用 data-*:实测本仓 vendored Monaco 的 markdown→DOM 管线会把我们 HTML 里的 `class`
43
+ * 全部剥掉(`supportHtml` / `isTrusted` 都不豁免),只有 `data-*` 存活;因此 CSS 也一律用
44
+ * 属性选择器,否则样式与交互钩子都会静默失效。
45
+ */
46
+ export const LINE_ATTR = 'data-edrv-line'
47
+ export const TWIST_ATTR = 'data-edrv-twist'
48
+ export const NAME_ATTR = 'data-edrv-name'
49
+ export const EQ_ATTR = 'data-edrv-eq'
50
+ export const VAL_ATTR = 'data-edrv-val'
51
+ export const NOTE_ATTR = 'data-edrv-note'
52
+ export const HEAD_ATTR = 'data-edrv-head'
53
+ export const EXPR_ATTR = 'data-edrv-expr'
54
+ export const TYPE_ATTR = 'data-edrv-type'
55
+ /** 已绑定标记(同一 DOM 不重复挂监听)。 */
56
+ const WIRED_ATTR = 'data-edrv-wired'
57
+ /** 已展开标记(重新渲染后据此回放)。 */
58
+ const OPEN_ATTR = 'data-edrv-open'
59
+ /** 状态表上限(防止跨多次 hover 无限增长)。 */
60
+ const STATE_CAP = 50
61
+ /** mousedown 与 click 双通道去重窗口(ms)。 */
62
+ const CLICK_DEDUPE_MS = 600
63
+ // --endregion
64
+
65
+ /** 变量节点视图(与 DAP variables 返回结构一致)。 */
66
+ export type TreeNode = DapVariableView
67
+
68
+ /** 展平后的树行。 */
69
+ export interface TreeRow {
70
+ /** 稳定路径(根 `r0`,子级 `r0.1`),也是缓存与展开状态的键。 */
71
+ path: string
72
+ /** 所属层级路径(根为 '')。 */
73
+ parentPath: string
74
+ depth: number
75
+ node: TreeNode
76
+ /** 祖先 ref 链(不含自身):循环 ref 防护。 */
77
+ anc: number[]
78
+ /** 可展开(ref>0、未超深度、非循环)。 */
79
+ expandable: boolean
80
+ }
81
+
82
+ /** 一次 hover 的树状态(DOM 无关,可单测)。 */
83
+ export interface TreeState {
84
+ key: string
85
+ roots: readonly TreeNode[]
86
+ /** path → 已取回的子项(懒加载缓存)。 */
87
+ cache: Map<string, TreeNode[]>
88
+ /** 已展开的 path。 */
89
+ expanded: Set<string>
90
+ }
91
+
92
+ /** 状态注册表:hover key → 树状态。 */
93
+ const states = new Map<string, TreeState>()
94
+ /** DOM 观察器(hover 面板插入/重渲染时重新绑定)。 */
95
+ let observer: MutationObserver | null = null
96
+
97
+ // --region 纯逻辑(可单测)
98
+ /**
99
+ * HTML 转义(调试值来自被调试进程,必须全量转义后再拼进 supportHtml 片段)。
100
+ * @author ddj 2026年09月21号
101
+ * @param text 原始文本
102
+ * @returns 转义后的文本
103
+ */
104
+ export function escapeHtml(text: string): string {
105
+ return String(text ?? '')
106
+ .replace(/&/g, '&amp;')
107
+ .replace(/</g, '&lt;')
108
+ .replace(/>/g, '&gt;')
109
+ .replace(/"/g, '&quot;')
110
+ }
111
+
112
+ /**
113
+ * 值截断(完整值另存 title,避免长字符串撑爆浮窗)。
114
+ * @author ddj 2026年09月21号
115
+ * @param text 原始值
116
+ * @returns 截断后的值
117
+ */
118
+ export function clipValue(text: string): string {
119
+ const one = String(text ?? '')
120
+ return one.length > VALUE_CLIP ? one.slice(0, VALUE_CLIP) + '…' : one
121
+ }
122
+
123
+ /**
124
+ * 根级变量序列是否等价(名 + ref 相同):等价即视为「同一 hover 的重渲染」。
125
+ * @author ddj 2026年09月21号
126
+ * @param a 上一次根级变量
127
+ * @param b 本次根级变量
128
+ * @returns 是否等价
129
+ */
130
+ export function sameRoots(a: readonly TreeNode[], b: readonly TreeNode[]): boolean {
131
+ if (a === b) return true
132
+ if (a.length !== b.length) return false
133
+ for (let index = 0; index < a.length; index += 1) {
134
+ if (a[index].name !== b[index].name || a[index].ref !== b[index].ref) return false
135
+ }
136
+ return true
137
+ }
138
+
139
+ /**
140
+ * 建立并登记一次 hover 的树状态。
141
+ * 同一 key 且根级等价时**复用**旧状态:Monaco 会在鼠标微动时重跑 provider,若每次新建,
142
+ * 展开状态与子项缓存会被清空 → 表现为「点开又自动收起」(折叠看似失效)。
143
+ * @author ddj 2026年09月21号
144
+ * @param key hover 身份键(表达式 + 模型路径 + 范围)
145
+ * @param roots 根级变量
146
+ * @returns 树状态
147
+ */
148
+ export function createTreeState(key: string, roots: readonly TreeNode[]): TreeState {
149
+ const prev = states.get(key)
150
+ if (prev && sameRoots(prev.roots, roots)) return prev
151
+ const state: TreeState = { key, roots, cache: new Map(), expanded: new Set() }
152
+ states.delete(key)
153
+ states.set(key, state)
154
+ while (states.size > STATE_CAP) {
155
+ const oldest = states.keys().next().value as string | undefined
156
+ if (oldest === undefined) break
157
+ states.delete(oldest)
158
+ }
159
+ return state
160
+ }
161
+
162
+ /**
163
+ * 行可见性计划:未按 Alt → 只看调试块;按住 Alt → 只看 LSP 行。
164
+ * @author ddj 2026年09月21号
165
+ * @param held Alt 是否按住
166
+ * @returns 两类行的可见性
167
+ */
168
+ export function rowPlan(held: boolean): { debug: boolean; lsp: boolean } {
169
+ return held ? { debug: false, lsp: true } : { debug: true, lsp: false }
170
+ }
171
+
172
+ /**
173
+ * 贴底提示文案(随 Alt 状态提示当前/下一步动作)。
174
+ * @author ddj 2026年09月21号
175
+ * @param held Alt 是否按住
176
+ * @returns 单行文案
177
+ */
178
+ export function hintText(held: boolean): string {
179
+ return held ? '松开 Alt 返回调试值' : '按住 Alt 查看 LSP 信息'
180
+ }
181
+
182
+ /**
183
+ * 是否需要 LSP 占位行:Alt 按住、且还没有真实 LSP 行。
184
+ * LuaLS 首次 hover 可能数百毫秒,没有占位行时面板会空白,看起来像「没切过去」。
185
+ * @author ddj 2026年09月21号
186
+ * @param held Alt 是否按住
187
+ * @param hasRealLsp 是否已有真实 LSP 行
188
+ * @returns 是否需要占位行
189
+ */
190
+ export function needsLspPlaceholder(held: boolean, hasRealLsp: boolean): boolean {
191
+ return held && !hasRealLsp
192
+ }
193
+
194
+ /**
195
+ * 查询已登记的树状态。
196
+ * @author ddj 2026年09月21号
197
+ * @param key hover 身份键
198
+ * @returns 树状态(未登记为 null)
199
+ */
200
+ export function stateOf(key: string): TreeState | null {
201
+ return states.get(key) ?? null
202
+ }
203
+
204
+ /**
205
+ * 子路径生成(父 + 序号;根级为 `r<n>`)。
206
+ * @author ddj 2026年09月21号
207
+ * @param parent 父路径(根为 '')
208
+ * @param index 序号
209
+ * @returns 稳定路径
210
+ */
211
+ function pathOf(parent: string, index: number): string {
212
+ return parent ? parent + '.' + index : 'r' + index
213
+ }
214
+
215
+ /**
216
+ * 取某父节点的子项列表(根级取 roots,其余取缓存)。
217
+ * @author ddj 2026年09月21号
218
+ * @param state 树状态
219
+ * @param parent 父路径(根为 '')
220
+ * @returns 子项列表
221
+ */
222
+ function itemsOf(state: TreeState, parent: string): readonly TreeNode[] {
223
+ if (!parent) return state.roots
224
+ return state.cache.get(parent) ?? []
225
+ }
226
+
227
+ /**
228
+ * 构造单行(含循环 ref 判定)。
229
+ * @author ddj 2026年09月21号
230
+ * @param state 树状态
231
+ * @param parent 父路径(根为 '')
232
+ * @param depth 行深度
233
+ * @param anc 祖先 ref 链(不含本层节点)
234
+ * @param index 子项下标
235
+ * @returns 树行
236
+ */
237
+ export function rowAt(state: TreeState, parent: string, depth: number, anc: readonly number[], index: number): TreeRow {
238
+ const node = itemsOf(state, parent)[index]
239
+ const expandable = node.ref > 0 && depth < MAX_DEPTH && !anc.includes(node.ref)
240
+ return { path: pathOf(parent, index), parentPath: parent, depth, node, anc: [...anc], expandable }
241
+ }
242
+
243
+ /**
244
+ * 展平某父节点下的一层行(成员全量直显,仅受 budget 上限约束)。
245
+ * @author ddj 2026年09月21号
246
+ * @param state 树状态
247
+ * @param parent 父路径(根为 '')
248
+ * @param depth 行深度
249
+ * @param anc 祖先 ref 链(不含本层节点)
250
+ * @param budget 本层最多渲染的行数(缺省 MAX_ROWS)
251
+ * @returns 行列表
252
+ */
253
+ export function rowsOf(state: TreeState, parent: string, depth: number, anc: readonly number[], budget: number = MAX_ROWS): TreeRow[] {
254
+ const items = itemsOf(state, parent)
255
+ const size = Math.min(items.length, Math.max(0, budget))
256
+ const rows: TreeRow[] = []
257
+ for (let index = 0; index < size; index += 1) rows.push(rowAt(state, parent, depth, anc, index))
258
+ return rows
259
+ }
260
+
261
+ /**
262
+ * 行数上限提示行(仅在极端大表触顶时出现)。
263
+ * @author ddj 2026年09月21号
264
+ * @param depth 所属深度(与子项同层,便于随父级折叠一并移除)
265
+ * @returns 提示行 HTML
266
+ */
267
+ function limitNoteHtml(depth: number): string {
268
+ return '<li ' + LINE_ATTR + '="1" data-edrv-depth="' + String(depth) + '" ' + NOTE_ATTR + '="limit">已达行数上限(' + String(MAX_ROWS) + '),请在监视面板查看</li>'
269
+ }
270
+
271
+ /**
272
+ * 「无子项」提示行(点了展开却没有任何子项时给出明确反馈)。
273
+ * @author ddj 2026年09月21号
274
+ * @param depth 所属深度(与子项同层,便于随父级折叠一并移除)
275
+ * @returns 提示行 HTML
276
+ */
277
+ function emptyNoteHtml(depth: number): string {
278
+ return '<li ' + LINE_ATTR + '="1" data-edrv-depth="' + String(depth) + '" ' + NOTE_ATTR + '="empty">(无子项)</li>'
279
+ }
280
+
281
+ /**
282
+ * 展开箭头文案(可展开行按开合给 ▸/▾,不可展开留空)。
283
+ * @author ddj 2026年09月21号
284
+ * @param row 树行
285
+ * @param open 是否已展开
286
+ * @returns 箭头文案
287
+ */
288
+ function twistOf(row: TreeRow, open: boolean): string {
289
+ if (!row.expandable) return ''
290
+ return open ? '▾' : '▸'
291
+ }
292
+
293
+ /**
294
+ * 单行 → HTML(名称/值全部转义;箭头仅可展开行给出)。
295
+ * @author ddj 2026年09月21号
296
+ * @param row 树行
297
+ * @param open 该行当前是否已展开
298
+ * @returns 行 HTML
299
+ */
300
+ export function rowHtml(row: TreeRow, open: boolean): string {
301
+ const attrs = [
302
+ LINE_ATTR + '="1"',
303
+ 'data-edrv-path="' + escapeHtml(row.path) + '"',
304
+ 'data-edrv-parent="' + escapeHtml(row.parentPath) + '"',
305
+ 'data-edrv-depth="' + String(row.depth) + '"',
306
+ 'data-edrv-anc="' + escapeHtml(row.anc.join(',')) + '"',
307
+ 'data-edrv-ref="' + String(row.node.ref) + '"',
308
+ 'style="padding-left:' + String(row.depth * 12) + 'px"',
309
+ ]
310
+ const full = String(row.node.value ?? '')
311
+ return '<li ' + attrs.join(' ') + '>'
312
+ + '<span ' + TWIST_ATTR + '="1">' + twistOf(row, open) + '</span>'
313
+ + '<span ' + NAME_ATTR + '="1">' + escapeHtml(row.node.name) + '</span>'
314
+ + '<span ' + EQ_ATTR + '="1">=</span>'
315
+ + '<span ' + VAL_ATTR + '="1" title="' + escapeHtml(full) + '">' + escapeHtml(clipValue(full)) + '</span>'
316
+ + '</li>'
317
+ }
318
+
319
+ /**
320
+ * 行列表 → HTML。
321
+ * @author ddj 2026年09月21号
322
+ * @param rows 行列表
323
+ * @param open 已展开路径集合
324
+ * @returns 拼接后的 HTML
325
+ */
326
+ export function rowsHtml(rows: readonly TreeRow[], open: ReadonlySet<string>): string {
327
+ return rows.map((row) => rowHtml(row, open.has(row.path))).join('')
328
+ }
329
+
330
+ /**
331
+ * 生成树初始 HTML(根级成员全量直显;无可渲染行时返回空串)。
332
+ * @author ddj 2026年09月21号
333
+ * @param state 树状态
334
+ * @returns 树容器 HTML
335
+ */
336
+ export function initialHtml(state: TreeState): string {
337
+ const rows = rowsOf(state, '', 0, [], MAX_ROWS)
338
+ if (!rows.length) return ''
339
+ // 不加自定义 class:markdown 渲染会剥掉 class(见常量区注释),钩子/样式一律用 data-*
340
+ const note = state.roots.length > rows.length ? limitNoteHtml(0) : ''
341
+ return '<ul ' + TREE_ATTR + '="1" data-edrv-key="' + escapeHtml(state.key) + '">'
342
+ + rowsHtml(rows, state.expanded)
343
+ + note
344
+ + '</ul>'
345
+ }
346
+ // --endregion
347
+
348
+ // --region DOM 绑定与懒加载
349
+ /**
350
+ * 设置展开箭头方向。
351
+ * @author ddj 2026年09月21号
352
+ * @param row 行元素
353
+ * @param open 是否展开
354
+ */
355
+ function setTwist(row: HTMLElement, open: boolean): void {
356
+ const twist = row.querySelector('[' + TWIST_ATTR + ']')
357
+ if (!twist) return
358
+ twist.textContent = open ? '▾' : '▸'
359
+ }
360
+
361
+ /**
362
+ * 行内祖先 ref 链(DOM 属性 → 数组)。
363
+ * @author ddj 2026年09月21号
364
+ * @param row 行元素
365
+ * @returns ref 数组
366
+ */
367
+ function ancOf(row: HTMLElement): number[] {
368
+ const raw = row.getAttribute('data-edrv-anc') ?? ''
369
+ return raw ? raw.split(',').map((item) => Number(item)).filter((item) => Number.isFinite(item)) : []
370
+ }
371
+
372
+ /**
373
+ * 渲染某行已缓存的子项(成员全量直显;不发起请求,命中与否由调用方判断)。
374
+ * @author ddj 2026年09月21号
375
+ * @param state 树状态
376
+ * @param row 行元素
377
+ * @param host 树容器
378
+ * @returns 实际插入的行数
379
+ */
380
+ function renderChildren(state: TreeState, row: HTMLElement, host: HTMLElement): number {
381
+ const path = row.getAttribute('data-edrv-path') ?? ''
382
+ const depth = Number(row.getAttribute('data-edrv-depth') ?? '0') + 1
383
+ const anc = [...ancOf(row), Number(row.getAttribute('data-edrv-ref') ?? '0')]
384
+ const budget = Math.max(0, MAX_ROWS - host.querySelectorAll('[' + LINE_ATTR + ']').length)
385
+ if (!budget) return 0
386
+ const rows = rowsOf(state, path, depth, anc, budget)
387
+ const note = itemsOf(state, path).length > rows.length ? limitNoteHtml(depth) : ''
388
+ row.insertAdjacentHTML('afterend', rowsHtml(rows, state.expanded) + note)
389
+ return rows.length
390
+ }
391
+
392
+ /**
393
+ * 折叠某行(移除其后深度更大的行)。
394
+ * @author ddj 2026年09月21号
395
+ * @param state 树状态
396
+ * @param row 行元素
397
+ */
398
+ function collapseRow(state: TreeState, row: HTMLElement): void {
399
+ const depth = Number(row.getAttribute('data-edrv-depth') ?? '0')
400
+ let node = row.nextElementSibling
401
+ while (node && Number(node.getAttribute('data-edrv-depth') ?? '-1') > depth) {
402
+ const next = node.nextElementSibling
403
+ node.remove()
404
+ node = next
405
+ }
406
+ state.expanded.delete(row.getAttribute('data-edrv-path') ?? '')
407
+ row.removeAttribute(OPEN_ATTR)
408
+ setTwist(row, false)
409
+ }
410
+
411
+ /**
412
+ * 展开某行(子项懒加载:未缓存才请求 variables;空子项给出明确反馈)。
413
+ * @author ddj 2026年09月21号
414
+ * @param state 树状态
415
+ * @param row 行元素
416
+ * @param host 树容器
417
+ */
418
+ async function expandRow(state: TreeState, row: HTMLElement, host: HTMLElement): Promise<void> {
419
+ const path = row.getAttribute('data-edrv-path') ?? ''
420
+ if (state.expanded.has(path)) {
421
+ collapseRow(state, row)
422
+ return
423
+ }
424
+ if (!state.cache.has(path)) {
425
+ const ref = Number(row.getAttribute('data-edrv-ref') ?? '0')
426
+ const children = await dapStore.variables(ref)
427
+ state.cache.set(path, children)
428
+ if (!children.length) dapTrace('hover-expand-empty', { path, ref })
429
+ }
430
+ state.expanded.add(path)
431
+ row.setAttribute(OPEN_ATTR, '1')
432
+ renderChildren(state, row, host)
433
+ // 子项为空:给出明确反馈,避免看起来「点了没反应」
434
+ if (!itemsOf(state, path).length) row.insertAdjacentHTML('afterend', emptyNoteHtml(Number(row.getAttribute('data-edrv-depth') ?? '0') + 1))
435
+ setTwist(row, true)
436
+ }
437
+
438
+ /** 双通道去重时间戳(mousedown 已切换时,紧随的 click 不再重复切换)。 */
439
+ let lastToggleAt = 0
440
+
441
+ /**
442
+ * 命中折叠按钮的行(未命中返回 null)。
443
+ * @author ddj 2026年09月21号
444
+ * @param target 事件目标
445
+ * @returns 行元素(非折叠区/不可展开为 null)
446
+ */
447
+ function twistRowOf(target: EventTarget | null): HTMLElement | null {
448
+ const el = target as HTMLElement | null
449
+ const row = el?.closest?.('[' + LINE_ATTR + ']') as HTMLElement | null
450
+ if (!row) return null
451
+ if (!row.querySelector('[' + TWIST_ATTR + ']')?.textContent) return null
452
+ return row
453
+ }
454
+
455
+ /**
456
+ * 切换某行展开/折叠(含容器与状态查找)。
457
+ * @author ddj 2026年09月21号
458
+ * @param row 行元素
459
+ * @returns 是否已派发
460
+ */
461
+ function toggleRow(row: HTMLElement): boolean {
462
+ const host = row.closest('[' + TREE_ATTR + ']') as HTMLElement | null
463
+ if (!host) return false
464
+ const state = stateOf(host.getAttribute('data-edrv-key') ?? '')
465
+ if (!state) return false
466
+ lastToggleAt = Date.now()
467
+ void expandRow(state, row, host)
468
+ return true
469
+ }
470
+
471
+ /**
472
+ * 折叠按钮 mousedown(capture 主通道):早于文本选择/焦点变化响应,并阻止默认行为。
473
+ * @author ddj 2026年09月21号
474
+ * @param ev 指针事件
475
+ */
476
+ function onTreeDown(ev: Event): void {
477
+ const row = twistRowOf(ev.target)
478
+ if (!row) return
479
+ ev.preventDefault()
480
+ ev.stopPropagation()
481
+ toggleRow(row)
482
+ }
483
+
484
+ /**
485
+ * 折叠按钮 click(capture 兜底通道):mousedown 被宿主吞掉时仍可切换;与主通道去重。
486
+ * @author ddj 2026年09月21号
487
+ * @param ev 点击事件
488
+ */
489
+ function onTreeClick(ev: Event): void {
490
+ if (Date.now() - lastToggleAt < CLICK_DEDUPE_MS) return
491
+ const row = twistRowOf(ev.target)
492
+ if (!row) return
493
+ ev.preventDefault()
494
+ ev.stopPropagation()
495
+ toggleRow(row)
496
+ }
497
+
498
+ /**
499
+ * 绑定一个树容器(幂等):双通道委托 + 回放已展开层级。
500
+ * @author ddj 2026年09月21号
501
+ * @param tree 树容器元素
502
+ */
503
+ function wireTree(tree: HTMLElement): void {
504
+ if (tree.getAttribute(WIRED_ATTR) === '1') return
505
+ tree.setAttribute(WIRED_ATTR, '1')
506
+ tree.addEventListener('mousedown', onTreeDown, true)
507
+ tree.addEventListener('click', onTreeClick, true)
508
+ const state = stateOf(tree.getAttribute('data-edrv-key') ?? '')
509
+ if (state) replayExpansions(state, tree)
510
+ }
511
+
512
+ /**
513
+ * 重新渲染后回放已展开层级(只用缓存,不再请求)。
514
+ * @author ddj 2026年09月21号
515
+ * @param state 树状态
516
+ * @param tree 树容器元素
517
+ */
518
+ function replayExpansions(state: TreeState, tree: HTMLElement): void {
519
+ for (let round = 0; round < MAX_DEPTH; round += 1) {
520
+ const opened = (row: HTMLElement): boolean => !row.hasAttribute(OPEN_ATTR) && state.cache.has(row.getAttribute('data-edrv-path') ?? '')
521
+ const pending = Array.from(tree.querySelectorAll<HTMLElement>('[' + LINE_ATTR + ']'))
522
+ .filter((row) => state.expanded.has(row.getAttribute('data-edrv-path') ?? '') && opened(row))
523
+ if (!pending.length) return
524
+ for (const row of pending) {
525
+ row.setAttribute(OPEN_ATTR, '1')
526
+ renderChildren(state, row, tree)
527
+ setTwist(row, true)
528
+ }
529
+ }
530
+ }
531
+
532
+ // --region hover 层(提示贴底常驻 + Alt 行级显隐)
533
+ /** 当前 Alt 状态镜像(由 hoverMode 经 applyAltToHovers 注入,避免模块循环依赖)。 */
534
+ let altHeld = false
535
+
536
+ /**
537
+ * 同步 LSP 占位行(幂等):Alt 按住且还没有真实 LSP 行时补一行「LSP 信息加载中…」,
538
+ * 真实 LSP 行到达(或松开 Alt)即移除;避免按下 Alt 后面板空白被当成「没切换」。
539
+ * @author ddj 2026年09月21号
540
+ * @param hover hover 容器
541
+ * @param held Alt 是否按住
542
+ */
543
+ function syncPlaceholder(hover: HTMLElement, held: boolean): void {
544
+ const existing = hover.querySelector('[' + PLACEHOLDER_ATTR + ']')
545
+ const rows = Array.from(hover.querySelectorAll('.hover-row[' + ROW_ATTR + ']'))
546
+ const hasRealLsp = rows.some((row) => row.getAttribute(ROW_ATTR) === 'lsp' && !row.hasAttribute(PLACEHOLDER_ATTR))
547
+ if (!needsLspPlaceholder(held, hasRealLsp)) {
548
+ if (existing) existing.remove()
549
+ return
550
+ }
551
+ if (existing) return
552
+ const content = hover.querySelector('.monaco-hover-content')
553
+ if (!content) return
554
+ const node = document.createElement('div')
555
+ node.className = 'hover-row'
556
+ node.setAttribute(PLACEHOLDER_ATTR, '1')
557
+ node.setAttribute(ROW_ATTR, 'lsp')
558
+ // 内联样式:占位行不在本文件既有 CSS 钩子清单里,避免为此扩散样式改动
559
+ node.style.cssText = 'padding:2px 8px;font-style:italic;color:var(--dsw-alias-label-tertiary,#8a949b)'
560
+ node.textContent = LSP_PLACEHOLDER_TEXT
561
+ content.appendChild(node)
562
+ }
563
+
564
+ /**
565
+ * 按 Alt 状态切换单次 hover 的行可见性、占位行与提示文案(纯显示层,不动 provider)。
566
+ * @author ddj 2026年09月21号
567
+ * @param hover hover 容器
568
+ * @param held Alt 是否按住
569
+ */
570
+ function applyRows(hover: HTMLElement, held: boolean): void {
571
+ syncPlaceholder(hover, held)
572
+ const plan = rowPlan(held)
573
+ for (const row of Array.from(hover.querySelectorAll<HTMLElement>('.hover-row[' + ROW_ATTR + ']'))) {
574
+ const visible = row.getAttribute(ROW_ATTR) === 'debug' ? plan.debug : plan.lsp
575
+ row.style.display = visible ? '' : 'none'
576
+ }
577
+ const hint = hover.querySelector<HTMLElement>('[' + HINT_ATTR + ']')
578
+ if (hint) hint.textContent = hintText(held)
579
+ }
580
+
581
+ /**
582
+ * 同步 hover 层(幂等):打行归属标记、注入贴底提示层、应用当前 Alt 状态。
583
+ * 没有调试块时清除标记与提示层,避免污染普通(LSP)hover。
584
+ * @author ddj 2026年09月21号
585
+ * @param hover hover 容器
586
+ */
587
+ function syncHoverLayer(hover: HTMLElement): void {
588
+ const block = hover.querySelector('[' + BLOCK_ATTR + ']')
589
+ const hint = hover.querySelector<HTMLElement>('[' + HINT_ATTR + ']')
590
+ if (!block) {
591
+ if (hint) hint.remove()
592
+ hover.removeAttribute(HINT_HOST_ATTR)
593
+ for (const row of Array.from(hover.querySelectorAll('[' + ROW_ATTR + ']'))) row.removeAttribute(ROW_ATTR)
594
+ return
595
+ }
596
+ for (const row of Array.from(hover.querySelectorAll('.hover-row'))) {
597
+ row.setAttribute(ROW_ATTR, row.querySelector('[' + BLOCK_ATTR + ']') ? 'debug' : 'lsp')
598
+ }
599
+ if (!hint) {
600
+ const node = document.createElement('div')
601
+ node.setAttribute(HINT_ATTR, '1')
602
+ node.className = 'edrv-dap-hint'
603
+ hover.appendChild(node)
604
+ }
605
+ // 容器标记:CSS 依此预留 22px 底部内边距,避免贴底提示层遮住最后一行
606
+ hover.setAttribute(HINT_HOST_ATTR, '1')
607
+ applyRows(hover, altHeld)
608
+ }
609
+
610
+ /**
611
+ * 当前是否有可见的调试 hover(供 hoverMode 判定「是否消费裸 Alt」)。
612
+ * @author ddj 2026年09月21号
613
+ * @returns 是否有可见的调试浮层
614
+ */
615
+ export function hasDebugHover(): boolean {
616
+ if (typeof document === 'undefined') return false
617
+ for (const hover of Array.from(document.querySelectorAll<HTMLElement>('.monaco-hover'))) {
618
+ if (!hover.classList.contains('hidden') && hover.querySelector('[' + BLOCK_ATTR + ']')) return true
619
+ }
620
+ return false
621
+ }
622
+
623
+ /**
624
+ * 应用 Alt 状态到所有 hover(幂等;供 hoverMode 在 Alt 变化时调用)。
625
+ * @author ddj 2026年09月21号
626
+ * @param held Alt 是否按住
627
+ */
628
+ export function applyAltToHovers(held: boolean): void {
629
+ altHeld = held
630
+ if (typeof document === 'undefined') return
631
+ for (const hover of Array.from(document.querySelectorAll<HTMLElement>('.monaco-hover'))) applyRows(hover, held)
632
+ }
633
+
634
+ /**
635
+ * 批次末兜底扫描:绑定所有未绑定的树容器 + 同步所有 hover 层。
636
+ * 覆盖 markdown 异步渲染的各种插入形态(先插容器后填子节点等),也保证 hover
637
+ * 重渲染后重新打标记并回放展开层级。
638
+ * @author ddj 2026年09月21号
639
+ */
640
+ function sweepWires(): void {
641
+ for (const tree of Array.from(document.querySelectorAll<HTMLElement>('[' + TREE_ATTR + ']:not([' + WIRED_ATTR + '])'))) wireTree(tree)
642
+ for (const hover of Array.from(document.querySelectorAll<HTMLElement>('.monaco-hover'))) syncHoverLayer(hover)
643
+ }
644
+
645
+ /** 兜底扫描合并计时器(一帧内多次 DOM 变更只扫一次)。 */
646
+ let sweepTimer: number | null = null
647
+
648
+ /**
649
+ * 合并调度一次兜底扫描(避免与宿主高频渲染互相放大)。
650
+ * @author ddj 2026年09月21号
651
+ */
652
+ function scheduleSweep(): void {
653
+ if (typeof window === 'undefined' || sweepTimer !== null) return
654
+ sweepTimer = window.setTimeout(() => {
655
+ sweepTimer = null
656
+ sweepWires()
657
+ }, 0)
658
+ }
659
+
660
+ /**
661
+ * 收集本次变更影响的 hover 容器(新增节点自身 / 祖先 / 后代)。
662
+ * @author ddj 2026年09月21号
663
+ * @param records 变更记录
664
+ * @returns hover 容器集合
665
+ */
666
+ function hoversOf(records: MutationRecord[]): Set<HTMLElement> {
667
+ const found = new Set<HTMLElement>()
668
+ for (const record of records) {
669
+ for (const node of Array.from(record.addedNodes)) {
670
+ if (node.nodeType !== 1) continue
671
+ const el = node as Element
672
+ const owner = (el.matches?.('.monaco-hover') ? el : el.closest?.('.monaco-hover')) as HTMLElement | null
673
+ if (owner) {
674
+ found.add(owner)
675
+ continue
676
+ }
677
+ for (const hover of Array.from(el.querySelectorAll?.('.monaco-hover') ?? [])) found.add(hover as HTMLElement)
678
+ }
679
+ }
680
+ return found
681
+ }
682
+
683
+ /**
684
+ * 清理已注入的 hover 层(卸载时避免残留提示层与隐藏态)。
685
+ * @author ddj 2026年09月21号
686
+ */
687
+ function clearHoverLayer(): void {
688
+ if (typeof document === 'undefined') return
689
+ for (const hint of Array.from(document.querySelectorAll<HTMLElement>('[' + HINT_ATTR + ']'))) hint.remove()
690
+ for (const ph of Array.from(document.querySelectorAll<HTMLElement>('[' + PLACEHOLDER_ATTR + ']'))) ph.remove()
691
+ for (const host of Array.from(document.querySelectorAll<HTMLElement>('[' + HINT_HOST_ATTR + ']'))) host.removeAttribute(HINT_HOST_ATTR)
692
+ for (const row of Array.from(document.querySelectorAll<HTMLElement>('.hover-row[' + ROW_ATTR + ']'))) {
693
+ row.style.display = ''
694
+ row.removeAttribute(ROW_ATTR)
695
+ }
696
+ }
697
+
698
+ /**
699
+ * 安装 hover 树绑定(幂等):观察 hover 面板插入与内容重渲染,批次末兜底扫描。
700
+ * @author ddj 2026年09月21号
701
+ */
702
+ export function installHoverTree(): void {
703
+ if (typeof window === 'undefined' || observer) return
704
+ observer = new MutationObserver((records) => {
705
+ const hovers = hoversOf(records)
706
+ // 同步应用:新行一落地就处于正确的 Alt 可见性,不会在按住 Alt 时闪回调试块
707
+ for (const hover of hovers) syncHoverLayer(hover)
708
+ if (hovers.size) scheduleSweep()
709
+ })
710
+ observer.observe(document.body, { childList: true, subtree: true })
711
+ }
712
+
713
+ /** 卸载 hover 树绑定并清空状态(插件重载/卸载)。 */
714
+ export function disposeHoverTree(): void {
715
+ observer?.disconnect()
716
+ observer = null
717
+ if (typeof window !== 'undefined' && sweepTimer !== null) window.clearTimeout(sweepTimer)
718
+ sweepTimer = null
719
+ states.clear()
720
+ clearHoverLayer()
721
+ }
722
+ // --endregion