dsh-vscode-mode 0.5.3 → 0.7.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 (77) hide show
  1. package/README.md +72 -2
  2. package/lib/client.js +12082 -6420
  3. package/lib/client.js.map +1 -1
  4. package/lib/index.js +2750 -147
  5. package/lib/index.js.map +1 -1
  6. package/package.json +1 -1
  7. package/src/client/copyText.ts +28 -0
  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/fileClipboard.ts +46 -0
  27. package/src/client/index.ts +12 -3
  28. package/src/client/monaco/lsp/index.ts +15 -0
  29. package/src/client/monaco/lsp/providers.ts +2 -0
  30. package/src/client/monaco/theme.ts +15 -0
  31. package/src/client/openWithDialog.ts +66 -0
  32. package/src/client/searchSeed.ts +36 -0
  33. package/src/client/sidebar/contextMenu.ts +51 -10
  34. package/src/client/sidebar/menuItems.ts +473 -50
  35. package/src/client/sidebar/panels/DebugPanel.ts +613 -0
  36. package/src/client/sidebar/panels/SearchPanel.ts +24 -3
  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 +8 -0
  40. package/src/client/styles/editor.css +176 -0
  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 +22 -10
  45. package/src/client/ui/EditorView.ts +625 -23
  46. package/src/client/ui/OfficialSideTab.ts +1 -0
  47. package/src/client/ui/PromptDialog.ts +63 -0
  48. package/src/client/ui/SideBySideDiff.tsx +214 -29
  49. package/src/client/ui/SideEditorTab.ts +1 -0
  50. package/src/client/ui/SvnDiffPanel.ts +21 -8
  51. package/src/client/ui/SvnLogDialog.ts +44 -2
  52. package/src/client/ui/SvnPatchDialog.ts +63 -0
  53. package/src/client/ui/SvnSumDialog.ts +77 -0
  54. package/src/client/ui/commandCatalog.ts +63 -0
  55. package/src/client/ui/svnExport.ts +71 -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/index.ts +7 -3
  66. package/src/lsp/providers.ts +3 -2
  67. package/src/reveal.ts +31 -20
  68. package/src/rpc.ts +209 -5
  69. package/src/search/ripgrep.ts +127 -11
  70. package/src/shared/dap.ts +262 -0
  71. package/src/shared/fsNames.ts +115 -0
  72. package/src/shared/keybindings.ts +9 -0
  73. package/src/shared/rpc.ts +52 -2
  74. package/src/shared/svn.ts +147 -0
  75. package/src/shared/svnActions.ts +20 -0
  76. package/src/svn.ts +320 -11
  77. package/src/workspace.ts +0 -75
@@ -0,0 +1,571 @@
1
+ /**
2
+ * dsh-vscode-mode client — 调试会话 store(单例):host 状态轮询镜像 + 动作封装。
3
+ * - 轮询:phase 非 idle/terminated 时 400ms 拉 `edrv.dap.poll`(增量事件游标);
4
+ * 空闲时停表(零开销)。host 不可达/降级(旧 host 无方法)静默空转。
5
+ * - paused → state.topFrame 驱动 `edrv:debug-stopped` 事件(EditorView 跳转高亮)。
6
+ * - 断点:本地表(localStorage per scope)+ `edrv.dap.setBreakpoints` 同步 host;
7
+ * host 不可达时仅本地,下次 start 由 manager.flushBreakpoints 全量下发。
8
+ * 作者 ddj 2026年09月29号
9
+ */
10
+ import { rpc } from '../rpc.js'
11
+ import type { DapAction, DapAdapterInfo, DapBreakpointInput, DapDebugConfig, DapEventView, DapFrameView, DapPhase, DapPollResult, DapProcessInfo, DapScopeView, DapStateView, DapVariableView } from '../../shared/dap.js'
12
+ import { bpLinesOf, loadBpMap, removeAll, saveBpMap, setAllEnabled, toggleBp, updateBpFields, type BpEntry, type BpMap } from './breakpoints.js'
13
+ import { resolvePidPlan } from './pidPick.js'
14
+ import { dapTrace } from './trace.js'
15
+
16
+ /** 客户端轮询间隔(调试活跃期)。 */
17
+ const POLL_MS = 400
18
+ /** 事件缓存上限(调试控制台渲染源,批次 3 消费)。 */
19
+ const EVENT_CAP = 400
20
+ /** 记忆配置名的 localStorage key。 */
21
+ const CFG_KEY = 'edrv.dap.selectedConfig'
22
+
23
+ /** 对外快照(useSyncExternalStore 口径:version 驱动重渲)。 */
24
+ export interface DapSnapshot {
25
+ version: number
26
+ phase: DapPhase
27
+ configName?: string
28
+ configType?: string
29
+ pid?: number
30
+ error?: string
31
+ topFrame: DapStateView['topFrame']
32
+ configs: DapDebugConfig[]
33
+ /** 已发现适配器可用性(下拉置灰与原因提示;旧 host 无此数据时为空表)。 */
34
+ adapters: DapAdapterInfo[]
35
+ selectedConfig: string | null
36
+ /** 当前选中的调用栈帧 id。 */
37
+ selectedFrameId: number
38
+ /** 进程选择器候选(非 null = 工具条切选择模式,多候选附加时出现)。 */
39
+ candidates: DapProcessInfo[] | null
40
+ /** 断点回执版本(适配器真实回执到达时推进,驱动空心断点重渲)。 */
41
+ bpAckVersion: number
42
+ }
43
+
44
+ /** store 单例类型。 */
45
+ export interface DapStore {
46
+ subscribe(fn: () => void): () => void
47
+ getSnapshot(): DapSnapshot
48
+ outputs(): DapEventView[]
49
+ setWorkspace(path: string): void
50
+ workspace(): string
51
+ ensureConfigs(force?: boolean): Promise<DapDebugConfig[]>
52
+ selectConfig(name: string): void
53
+ currentConfig(): DapDebugConfig | null
54
+ processes(processName?: string): Promise<DapProcessInfo[]>
55
+ start(pid?: number): Promise<void>
56
+ /** 确认进程选择器(按选定 pid 重新启动)。 */
57
+ confirmPick(pid: number): void
58
+ /** 取消进程选择器。 */
59
+ cancelPick(): void
60
+ stop(): Promise<void>
61
+ startOrContinue(): void
62
+ /** 运行到行(临时断点 + 继续;命中后自动清理,不影响用户断点)。 */
63
+ runTo(cwd: string, path: string, line: number): Promise<void>
64
+ action(action: DapAction): Promise<void>
65
+ toggleAt(scope: string, cwd: string, path: string, line: number): BpMap
66
+ setBpEnabled(scope: string, cwd: string, path: string, line: number, enabled: boolean): void
67
+ /** 全量启停所有断点(跨文件;逐含断点文件全量同步 host)。 */
68
+ setAllBpEnabled(scope: string, cwd: string, enabled: boolean): void
69
+ /** 清空所有断点(逐文件同步空载荷;Monaco 装饰与 host 断点同步清空)。 */
70
+ removeAllBps(scope: string, cwd: string): void
71
+ applyBpFields(scope: string, cwd: string, path: string, line: number, fields: { condition?: string; hitCondition?: string; logMessage?: string }): void
72
+ bpMapOf(scope: string): BpMap
73
+ pointsOf(path: string, list: BpEntry[]): DapBreakpointInput[]
74
+ syncPoints(cwd: string, path: string, list: BpEntry[]): void
75
+ /** 断点回执查询(适配器在线时的真实 verified/挪行;无回执返回 null)。 */
76
+ ackOf(path: string, line: number): { verified: boolean; line: number; message?: string } | null
77
+ /** 客户端(重)装配后与 host 对账:采纳在跑的会话状态并按需续轮询(幂等)。 */
78
+ resync(): Promise<void>
79
+ /** 选择当前栈帧;暂停态下驱动 Variables/Watch/REPL 上下文切换。 */
80
+ selectFrame(frameId: number): void
81
+ stackTrace(): Promise<DapFrameView[]>
82
+ scopes(frameId: number): Promise<DapScopeView[]>
83
+ variables(ref: number): Promise<DapVariableView[]>
84
+ evaluate(expression: string, frameId?: number): Promise<{ result: string; type?: string; ref: number }>
85
+ }
86
+
87
+ let version = 0
88
+ let bpVersion = 0
89
+ let pollSeq = 0
90
+ let timer: ReturnType<typeof setInterval> | null = null
91
+ let workspacePath = ''
92
+ let selectedConfig: string | null = null
93
+ let configs: DapDebugConfig[] = []
94
+ let adapters: DapAdapterInfo[] = []
95
+ let configsDirty = true
96
+ let state: DapStateView = { phase: 'idle', topFrame: null }
97
+ let selectedFrameId = 0
98
+ let outputs: DapEventView[] = []
99
+ let lastTopKey = ''
100
+ let bpScope = ''
101
+ let bpMap: BpMap = {}
102
+ let candidates: DapProcessInfo[] | null = null
103
+ /** 断点回执(key = path:line 请求行;适配器在线时真实回执,start/stop 清空)。 */
104
+ const bpAcks = new Map<string, { verified: boolean; line: number; message?: string }>()
105
+ let bpAckVersion = 0
106
+ /** 运行到行的临时断点(命中/离开暂停后自动移除,不动用户断点表)。 */
107
+ let runToFile = ''
108
+ let runToLine = 0
109
+ let runToCwd = ''
110
+ let runToScope = ''
111
+ /** 本次运行到行是否真的插入了临时断点(目标行已有用户断点时不需要插,清理时也不得动它)。 */
112
+ let runToAdded = false
113
+ /** runTo 自身发起的 continue:不得立即清理刚下发的临时断点。 */
114
+ let runToContinuing = false
115
+ /** 对账进行中标记(重复 apply/挂载不并发打多轮 poll)。 */
116
+ let resyncing = false
117
+ const listeners = new Set<() => void>()
118
+
119
+ /** 通知订阅者(版本推进)。 */
120
+ function emit(): void {
121
+ version += 1
122
+ for (const fn of listeners) fn()
123
+ }
124
+
125
+ /** 首帧 key(同文件同行不重复派发跳转)。 */
126
+ function topKeyOf(top: NonNullable<DapStateView['topFrame']>): string {
127
+ return top.file + ':' + top.line
128
+ }
129
+
130
+ /** 运行到行命中后的清理:仅移除本次插入的临时断点(用户断点表始终不受影响)。 */
131
+ function clearRunTo(store: { bpMapOf: (scope: string) => BpMap; syncPoints: (cwd: string, path: string, list: BpEntry[]) => void }): void {
132
+ if (!runToFile || !runToAdded) { runToFile = ''; runToLine = 0; runToCwd = ''; runToScope = ''; runToAdded = false; return }
133
+ const file = runToFile
134
+ const cwd = runToCwd
135
+ const line = runToLine
136
+ const scope = runToScope
137
+ runToFile = ''
138
+ runToLine = 0
139
+ runToCwd = ''
140
+ runToScope = ''
141
+ runToAdded = false
142
+ const list = (store.bpMapOf(scope)[file] ?? []).filter((it) => it.line !== line)
143
+ store.syncPoints(cwd, file, list)
144
+ }
145
+
146
+ /** 应用一轮 poll 结果。 */
147
+ function applyPoll(result: DapPollResult): void {
148
+ const prevPhase = state.phase
149
+ state = result.state
150
+ pollSeq = result.nextSeq
151
+ let changed = prevPhase !== state.phase
152
+ if (result.events.length) {
153
+ outputs.push(...result.events)
154
+ if (outputs.length > EVENT_CAP) outputs.splice(0, outputs.length - EVENT_CAP)
155
+ }
156
+ // 运行到行:命中目标行(暂停且首帧即目标)→ 移除临时断点
157
+ if (state.phase === 'paused' && state.topFrame && runToFile) {
158
+ const hit = state.topFrame.file?.replace(/\\/g, '/') === runToFile
159
+ if (hit) clearRunTo(dapStore)
160
+ }
161
+ if (state.phase === 'paused' && state.topFrame) {
162
+ const key = topKeyOf(state.topFrame)
163
+ if (key !== lastTopKey) {
164
+ lastTopKey = key
165
+ selectedFrameId = 0
166
+ changed = true
167
+ dapTrace('stopped', { topFrame: state.topFrame, selectedFrameId })
168
+ if (typeof window !== 'undefined') {
169
+ window.dispatchEvent(new CustomEvent('edrv:debug-stopped', {
170
+ detail: { file: state.topFrame.file, line: state.topFrame.line },
171
+ }))
172
+ }
173
+ }
174
+ } else if (state.phase !== 'paused') {
175
+ selectedFrameId = 0
176
+ }
177
+ if (state.phase === 'idle' || state.phase === 'terminated') stopTimer()
178
+ if (changed) emit()
179
+ }
180
+
181
+ /**
182
+ * 会话是否处于活动相位(需要继续轮询跟踪)。
183
+ * 按「非 idle/terminated」判定:新增相位默认按活动处理,避免漏跟。
184
+ * @author ddj 2026年09月21号
185
+ * @param phase host 上报的会话相位
186
+ */
187
+ function activePhase(phase: DapPhase): boolean {
188
+ return phase !== 'idle' && phase !== 'terminated'
189
+ }
190
+
191
+ /** 确保轮询在跑。 */
192
+ function ensureTimer(): void {
193
+ if (timer || typeof window === 'undefined') return
194
+ timer = setInterval(() => { void pollOnce() }, POLL_MS)
195
+ }
196
+
197
+ /** 停轮询。 */
198
+ function stopTimer(): void {
199
+ if (!timer) return
200
+ clearInterval(timer)
201
+ timer = null
202
+ }
203
+
204
+ /** 单轮 poll(失败/降级静默停表,下次动作重启)。 */
205
+ async function pollOnce(): Promise<void> {
206
+ if (state.phase === 'idle' || state.phase === 'terminated') { stopTimer(); return }
207
+ try {
208
+ const res = await rpc('edrv.dap.poll', { since: pollSeq })
209
+ if (res.ok) applyPoll(res)
210
+ else stopTimer()
211
+ } catch {
212
+ stopTimer()
213
+ }
214
+ }
215
+
216
+ /** 调试 store 单例。 */
217
+ export const dapStore: DapStore = {
218
+ subscribe(fn) {
219
+ listeners.add(fn)
220
+ return () => listeners.delete(fn)
221
+ },
222
+
223
+ getSnapshot() {
224
+ return {
225
+ version,
226
+ phase: state.phase,
227
+ configName: state.configName,
228
+ configType: state.configType,
229
+ pid: state.pid,
230
+ error: state.error,
231
+ topFrame: state.phase === 'paused' ? state.topFrame : null,
232
+ configs,
233
+ adapters,
234
+ selectedConfig,
235
+ bpVersion,
236
+ selectedFrameId,
237
+ candidates,
238
+ bpAckVersion,
239
+ }
240
+ },
241
+
242
+ outputs() {
243
+ return outputs
244
+ },
245
+
246
+ setWorkspace(path) {
247
+ if (path === workspacePath) return
248
+ workspacePath = path
249
+ configsDirty = true
250
+ if (state.phase === 'idle') emit()
251
+ },
252
+
253
+ workspace() {
254
+ return workspacePath
255
+ },
256
+
257
+ async ensureConfigs(force = false) {
258
+ if (!workspacePath) return []
259
+ if (!force && !configsDirty && configs.length) return configs
260
+ try {
261
+ const res = await rpc('edrv.dap.configs', { workspacePath })
262
+ if (res.ok) {
263
+ configs = res.configs
264
+ adapters = res.adapters ?? []
265
+ configsDirty = false
266
+ if (!selectedConfig) {
267
+ try { selectedConfig = localStorage.getItem(CFG_KEY) } catch { /* 忽略 */ }
268
+ }
269
+ if (!configs.some((c) => c.name === selectedConfig)) selectedConfig = configs[0]?.name ?? null
270
+ emit()
271
+ }
272
+ } catch { /* host 降级:保持空表 */ }
273
+ return configs
274
+ },
275
+
276
+ selectConfig(name) {
277
+ selectedConfig = name
278
+ try { localStorage.setItem(CFG_KEY, name) } catch { /* 忽略 */ }
279
+ emit()
280
+ },
281
+
282
+ currentConfig() {
283
+ return configs.find((c) => c.name === selectedConfig) ?? configs[0] ?? null
284
+ },
285
+
286
+ async processes(processName) {
287
+ try {
288
+ const res = await rpc('edrv.dap.processes', { processName })
289
+ return res.ok ? res.items : []
290
+ } catch {
291
+ return []
292
+ }
293
+ },
294
+
295
+ /**
296
+ * 客户端(重)装配后与 host 对账:整页刷新 / 插件热换 bundle 会复位本模块状态,
297
+ * 而 host 的 DapSession 常驻(可能仍停在断点)—— 不对账则 phase 恒为 idle,
298
+ * 工具栏、变量面板与「暂停态 hover」全部静默失效。
299
+ * 只取当前状态、以超大游标跳过历史事件(重装不该回放调试控制台)。
300
+ * @author ddj 2026年09月21号
301
+ */
302
+ async resync() {
303
+ if (timer || resyncing) return
304
+ resyncing = true
305
+ try {
306
+ const res = await rpc('edrv.dap.poll', { since: Number.MAX_SAFE_INTEGER })
307
+ if (!res.ok) return
308
+ applyPoll({ state: res.state, events: [], nextSeq: res.nextSeq })
309
+ if (activePhase(res.state.phase)) ensureTimer()
310
+ } catch { /* host 降级:保持 idle,用户动作会再触发 */ } finally {
311
+ resyncing = false
312
+ }
313
+ },
314
+
315
+ async start(pid) {
316
+ const config = this.currentConfig()
317
+ if (!config || !workspacePath) return
318
+ // attach 型且无 pid/processId:客户端先做 0/1/多 命中决策(多候选弹进程选择器,
319
+ // 避免 host 侧唯一命中校验把多候选变成一条不可操作的错误文案)
320
+ let targetPid = pid && pid > 0 ? pid : undefined
321
+ const isAttach = config.request === 'attach' || (!config.request && config.type === 'emmylua_attach')
322
+ if (isAttach && !targetPid && !config.processId) {
323
+ const items = await this.processes(config.processName)
324
+ const plan = resolvePidPlan(items, config.processName)
325
+ if (plan.kind === 'none') {
326
+ state = { phase: 'idle', error: plan.message, topFrame: null }
327
+ candidates = null
328
+ emit()
329
+ return
330
+ }
331
+ if (plan.kind === 'picker') {
332
+ candidates = plan.items
333
+ state = { phase: 'idle', topFrame: null }
334
+ emit()
335
+ return
336
+ }
337
+ targetPid = plan.pid
338
+ }
339
+ try {
340
+ const res = await rpc('edrv.dap.start', { config, workspacePath, pid: targetPid })
341
+ if (res.ok) {
342
+ candidates = null
343
+ selectedFrameId = 0
344
+ bpAcks.clear()
345
+ bpAckVersion += 1
346
+ state = { phase: res.phase, configName: config.name, configType: config.type, pid: targetPid, topFrame: null }
347
+ lastTopKey = ''
348
+ emit()
349
+ ensureTimer()
350
+ void pollOnce()
351
+ } else {
352
+ state = { phase: 'idle', error: res.error, topFrame: null }
353
+ emit()
354
+ }
355
+ } catch (error) {
356
+ state = { phase: 'idle', error: String(error), topFrame: null }
357
+ emit()
358
+ }
359
+ },
360
+
361
+ /** 确认进程选择器:按选定 pid 重新启动。 */
362
+ confirmPick(pid) {
363
+ candidates = null
364
+ emit()
365
+ void this.start(pid)
366
+ },
367
+
368
+ /** 取消进程选择器。 */
369
+ cancelPick() {
370
+ candidates = null
371
+ emit()
372
+ },
373
+
374
+ async stop() {
375
+ try { await rpc('edrv.dap.stop', {}) } catch { /* 忽略 */ }
376
+ state = { phase: 'terminated', configName: state.configName, topFrame: null }
377
+ selectedFrameId = 0
378
+ bpAcks.clear()
379
+ bpAckVersion += 1
380
+ stopTimer()
381
+ emit()
382
+ },
383
+
384
+ startOrContinue() {
385
+ if (state.phase === 'paused') void this.action('continue')
386
+ else if (state.phase === 'idle' || state.phase === 'terminated') void this.start()
387
+ },
388
+
389
+ /** 运行到行(VS Code 同款语义:临时断点 + 继续,命中后自动移除,不动用户断点表)。 */
390
+ async runTo(cwd, path, line) {
391
+ if (!cwd || !path || !line) return
392
+ const scope = bpScope
393
+ const existing = this.bpMapOf(scope)[path] ?? []
394
+ // 目标行已有用户断点时无需插临时断点(直接继续即可命中)
395
+ const already = existing.some((it) => it.line === line)
396
+ runToFile = path.replace(/\\/g, '/')
397
+ runToLine = line
398
+ runToCwd = cwd
399
+ runToScope = scope
400
+ runToAdded = !already
401
+ if (runToAdded) this.syncPoints(cwd, path, [...existing, { line, enabled: true }])
402
+ if (state.phase === 'paused') {
403
+ runToContinuing = true
404
+ await this.action('continue')
405
+ runToContinuing = false
406
+ }
407
+ },
408
+
409
+ async action(action) {
410
+ try {
411
+ const res = await rpc('edrv.dap.command', { action })
412
+ if (res.ok && action !== 'pause' && action !== 'disconnect') {
413
+ state = { ...state, phase: 'running', topFrame: null }
414
+ // 手动继续/步进时若仍有未命中的运行到行临时断点,一并清理(runTo 自身发起的 continue 除外)
415
+ if (!runToContinuing && (action === 'continue' || action === 'next' || action === 'stepIn' || action === 'stepOut')) clearRunTo(this)
416
+ emit()
417
+ ensureTimer()
418
+ void pollOnce()
419
+ }
420
+ } catch { /* 忽略 */ }
421
+ },
422
+
423
+ toggleAt(scope, cwd, path, line) {
424
+ const next = toggleBp(this.bpMapOf(scope), path, line)
425
+ bpMap = next.map
426
+ bpScope = scope
427
+ bpVersion += 1
428
+ saveBpMap(scope, bpMap)
429
+ emit()
430
+ this.syncPoints(cwd, path, bpMap[path] ?? [])
431
+ return bpMap
432
+ },
433
+
434
+ /** 断点启停(只改 enabled 不删条目;禁用行不下发 host)。 */
435
+ setBpEnabled(scope, cwd, path, line, enabled) {
436
+ const map = this.bpMapOf(scope)
437
+ const list = (map[path] ?? []).map((it) => (it.line === line ? { ...it, enabled } : it))
438
+ const next: BpMap = { ...map, [path]: list }
439
+ bpMap = next
440
+ bpVersion += 1
441
+ saveBpMap(scope, next)
442
+ emit()
443
+ this.syncPoints(cwd, path, next[path] ?? [])
444
+ },
445
+
446
+ /** 全量启停所有断点(跨文件;已处于目标态的条目原样保留,同步幂等)。 */
447
+ setAllBpEnabled(scope, cwd, enabled) {
448
+ const next = setAllEnabled(this.bpMapOf(scope), enabled)
449
+ bpMap = next
450
+ bpScope = scope
451
+ bpVersion += 1
452
+ saveBpMap(scope, next)
453
+ emit()
454
+ if (!cwd) return
455
+ for (const path of Object.keys(next)) this.syncPoints(cwd, path, next[path] ?? [])
456
+ },
457
+
458
+ /** 清空所有断点(纯函数给出曾有条目的文件清单,逐文件下发空载荷)。 */
459
+ removeAllBps(scope, cwd) {
460
+ const cleared = removeAll(this.bpMapOf(scope))
461
+ bpMap = cleared.map
462
+ bpScope = scope
463
+ bpVersion += 1
464
+ saveBpMap(scope, bpMap)
465
+ emit()
466
+ if (!cwd) return
467
+ for (const path of cleared.files) this.syncPoints(cwd, path, [])
468
+ },
469
+
470
+ /** 编辑断点字段(条件/命中次数/日志;空串清除,随后全量同步)。 */
471
+ applyBpFields(scope, cwd, path, line, fields) {
472
+ const next = updateBpFields(this.bpMapOf(scope), path, line, fields)
473
+ bpMap = next
474
+ bpScope = scope
475
+ bpVersion += 1
476
+ saveBpMap(scope, next)
477
+ emit()
478
+ this.syncPoints(cwd, path, next[path] ?? [])
479
+ },
480
+
481
+ /** 启用条目 → DAP 断点载荷(含条件等编辑字段)。 */
482
+ pointsOf(path, list: BpEntry[]): DapBreakpointInput[] {
483
+ return list.filter((it) => it.enabled).map((it) => ({
484
+ path,
485
+ line: it.line,
486
+ condition: it.condition,
487
+ hitCondition: it.hitCondition,
488
+ logMessage: it.logMessage,
489
+ }))
490
+ },
491
+
492
+ /** 按完整条目同步某文件断点到 host,并消费适配器真实回执(驱动空心断点渲染)。 */
493
+ syncPoints(cwd: string, path: string, list: BpEntry[]): void {
494
+ if (!cwd) return
495
+ const points = this.pointsOf(path, list)
496
+ void rpc('edrv.dap.setBreakpoints', { workspacePath: cwd, file: path, points })
497
+ .then((res) => {
498
+ if (!res.ok) return
499
+ for (const key of [...bpAcks.keys()]) {
500
+ if (key.startsWith(path + ':')) bpAcks.delete(key)
501
+ }
502
+ for (let i = 0; i < res.breakpoints.length; i++) {
503
+ const point = points[i]
504
+ const ack = res.breakpoints[i]
505
+ if (!point || !ack) continue
506
+ bpAcks.set(path + ':' + point.line, { verified: ack.verified, line: ack.line, ...(ack.message ? { message: ack.message } : {}) })
507
+ }
508
+ bpAckVersion += 1
509
+ emit()
510
+ })
511
+ .catch(() => { /* host 不可达:重连后再同步 */ })
512
+ },
513
+
514
+ /** 断点回执查询(适配器在线时的真实 verified/挪行;无回执返回 null)。 */
515
+ ackOf(path: string, line: number) {
516
+ return bpAcks.get(path + ':' + line) ?? null
517
+ },
518
+
519
+ bpMapOf(scope) {
520
+ if (bpScope !== scope) {
521
+ bpScope = scope
522
+ bpMap = loadBpMap(scope)
523
+ }
524
+ return bpMap
525
+ },
526
+
527
+ /** 选择当前栈帧;非暂停态忽略无效选择。 */
528
+ selectFrame(frameId) {
529
+ if (state.phase !== 'paused' || !Number.isFinite(frameId) || frameId < 0) return
530
+ if (selectedFrameId === frameId) return
531
+ selectedFrameId = frameId
532
+ emit()
533
+ },
534
+
535
+ async stackTrace() {
536
+ try {
537
+ const res = await rpc('edrv.dap.stackTrace', {})
538
+ return res.ok ? res.frames : []
539
+ } catch {
540
+ return []
541
+ }
542
+ },
543
+
544
+ async scopes(frameId) {
545
+ try {
546
+ const res = await rpc('edrv.dap.scopes', { frameId })
547
+ return res.ok ? res.scopes : []
548
+ } catch {
549
+ return []
550
+ }
551
+ },
552
+
553
+ async variables(ref) {
554
+ try {
555
+ const res = await rpc('edrv.dap.variables', { ref })
556
+ return res.ok ? res.variables : []
557
+ } catch {
558
+ return []
559
+ }
560
+ },
561
+
562
+ async evaluate(expression, frameId) {
563
+ try {
564
+ const res = await rpc('edrv.dap.evaluate', { expression, frameId })
565
+ if (res.ok) return { result: res.result, type: res.type, ref: res.ref }
566
+ return { result: res.error, ref: 0 }
567
+ } catch (error) {
568
+ return { result: String(error), ref: 0 }
569
+ }
570
+ },
571
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * dsh-vscode-mode client — 调试工具条拖拽位置纯逻辑。
3
+ * 位置按工作区 scope 持久化;拖动结果始终 clamp 在编辑区边界内。
4
+ * 作者 ddj 2026年09月29号
5
+ */
6
+
7
+ export interface ToolbarPoint {
8
+ left: number
9
+ top: number
10
+ }
11
+
12
+ export interface ToolbarBounds {
13
+ width: number
14
+ height: number
15
+ barWidth: number
16
+ barHeight: number
17
+ margin?: number
18
+ }
19
+
20
+ const KEY_PREFIX = 'edrv.dap.toolbar.'
21
+
22
+ /** 将工具条位置限制在编辑区内。 */
23
+ export function clampToolbarPosition(point: ToolbarPoint, bounds: ToolbarBounds): ToolbarPoint {
24
+ const margin = bounds.margin ?? 6
25
+ const maxLeft = Math.max(margin, bounds.width - bounds.barWidth - margin)
26
+ const maxTop = Math.max(margin, bounds.height - bounds.barHeight - margin)
27
+ return {
28
+ left: Math.min(Math.max(margin, point.left), maxLeft),
29
+ top: Math.min(Math.max(margin, point.top), maxTop),
30
+ }
31
+ }
32
+
33
+ /** 读取当前工作区工具条位置;无记录返回 null(走 CSS 默认右上角)。 */
34
+ export function loadToolbarPosition(scope: string): ToolbarPoint | null {
35
+ if (!scope) return null
36
+ try {
37
+ const raw = localStorage.getItem(KEY_PREFIX + scope)
38
+ if (!raw) return null
39
+ const value = JSON.parse(raw)
40
+ if (!value || !Number.isFinite(value.left) || !Number.isFinite(value.top)) return null
41
+ return { left: value.left, top: value.top }
42
+ } catch {
43
+ return null
44
+ }
45
+ }
46
+
47
+ /** 保存工具条位置;存储失败仅影响下次恢复,不影响本次拖动。 */
48
+ export function saveToolbarPosition(scope: string, point: ToolbarPoint): void {
49
+ if (!scope) return
50
+ try { localStorage.setItem(KEY_PREFIX + scope, JSON.stringify(point)) } catch { /* 忽略 */ }
51
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * dsh-vscode-mode client — DAP 调试链路诊断 trace。
3
+ * 仅写入 window 环形 buffer,诊断失败绝不影响调试流程。
4
+ * 作者 ddj 2026年09月29号
5
+ */
6
+
7
+ /**
8
+ * 写入 DAP 调试诊断事件。
9
+ * @author ddj 2026年09月29号
10
+ * @param event 事件名
11
+ * @param data 诊断数据(不包含凭据)
12
+ */
13
+ export function dapTrace(event: string, data?: unknown): void {
14
+ try {
15
+ if (typeof window === 'undefined') return
16
+ const target = window as unknown as { __DSH_DEBUG_LOG__?: Array<Record<string, unknown>> }
17
+ const buffer = (target.__DSH_DEBUG_LOG__ = target.__DSH_DEBUG_LOG__ || [])
18
+ buffer.push({ t: Date.now(), plugin: 'dsh-vscode-mode', event: 'dap.' + event, data })
19
+ if (buffer.length > 500) buffer.splice(0, buffer.length - 500)
20
+ console.info('[dsh:dap-debug]', event, data ?? '')
21
+ } catch { /* 诊断失败不能干扰调试 */ }
22
+ }