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.
- package/README.md +55 -2
- package/lib/client.js +5173 -355
- package/lib/client.js.map +1 -1
- package/lib/index.js +2356 -126
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/dap/BpWidget.ts +262 -0
- package/src/client/dap/bpMenu.ts +91 -0
- package/src/client/dap/bpRowMenu.ts +53 -0
- package/src/client/dap/breakpoints.ts +222 -0
- package/src/client/dap/decorate.ts +128 -0
- package/src/client/dap/hintLine.ts +29 -0
- package/src/client/dap/hover.ts +166 -0
- package/src/client/dap/hoverMode.ts +159 -0
- package/src/client/dap/hoverTree.ts +722 -0
- package/src/client/dap/launchInsert.ts +229 -0
- package/src/client/dap/launchSnippetProvider.ts +206 -0
- package/src/client/dap/modelPath.ts +23 -0
- package/src/client/dap/panelSplit.ts +105 -0
- package/src/client/dap/pidPick.ts +24 -0
- package/src/client/dap/store.ts +571 -0
- package/src/client/dap/toolbarDrag.ts +51 -0
- package/src/client/dap/trace.ts +22 -0
- package/src/client/dap/variableTree.ts +62 -0
- package/src/client/index.ts +9 -0
- package/src/client/monaco/lsp/index.ts +15 -0
- package/src/client/monaco/lsp/providers.ts +2 -0
- package/src/client/monaco/theme.ts +15 -0
- package/src/client/sidebar/panels/DebugPanel.ts +613 -0
- package/src/client/sidebar/panels/SvnPanel.ts +167 -23
- package/src/client/sidebar/panels/index.ts +19 -0
- package/src/client/sidebar/types.ts +2 -0
- package/src/client/styles/editor.css +176 -0
- package/src/client/svnActions.ts +18 -1
- package/src/client/svnLog.ts +3 -3
- package/src/client/svnStatus.ts +140 -2
- package/src/client/ui/EditorView.ts +544 -16
- package/src/client/ui/SideBySideDiff.tsx +214 -29
- package/src/client/ui/SvnDiffPanel.ts +21 -8
- package/src/client/ui/SvnLogDialog.ts +44 -2
- package/src/client/ui/SvnPatchDialog.ts +63 -0
- package/src/client/ui/SvnSumDialog.ts +77 -0
- package/src/client/ui/commandCatalog.ts +49 -0
- package/src/client/ui/svnExport.ts +71 -0
- package/src/dap/configSnippets.ts +269 -0
- package/src/dap/discovery.ts +198 -0
- package/src/dap/launchConfig.ts +127 -0
- package/src/dap/manager.ts +588 -0
- package/src/dap/processList.ts +76 -0
- package/src/dap/protocol.ts +80 -0
- package/src/dap/provider.ts +49 -0
- package/src/dap/rpc.ts +192 -0
- package/src/dap/sourcePath.ts +82 -0
- package/src/index.ts +7 -3
- package/src/lsp/providers.ts +3 -2
- package/src/reveal.ts +31 -20
- package/src/rpc.ts +11 -3
- package/src/search/ripgrep.ts +127 -11
- package/src/shared/dap.ts +262 -0
- package/src/shared/keybindings.ts +7 -0
- package/src/shared/rpc.ts +40 -2
- package/src/shared/svn.ts +147 -0
- package/src/shared/svnActions.ts +20 -0
- package/src/svn.ts +320 -11
- 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, '&')
|
|
107
|
+
.replace(/</g, '<')
|
|
108
|
+
.replace(/>/g, '>')
|
|
109
|
+
.replace(/"/g, '"')
|
|
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
|