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.
- package/README.md +78 -5
- package/lib/client.js +5579 -236
- package/lib/client.js.map +1 -1
- package/lib/index.js +2358 -126
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/compat.ts +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/editorLimit.ts +31 -0
- package/src/client/index.ts +13 -0
- package/src/client/markdownPreview.ts +24 -0
- package/src/client/md/componentType.ts +28 -0
- package/src/client/md/mdPanel.ts +98 -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/searchSeed.ts +64 -0
- package/src/client/sidebar/panels/DebugPanel.ts +613 -0
- package/src/client/sidebar/panels/SearchPanel.ts +70 -15
- 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 +186 -1
- package/src/client/svnActions.ts +18 -1
- package/src/client/svnLog.ts +3 -3
- package/src/client/svnStatus.ts +140 -2
- package/src/client/tabActions.ts +54 -0
- package/src/client/ui/EditorView.ts +711 -29
- package/src/client/ui/McpSettings.ts +29 -0
- 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 +54 -0
- package/src/client/ui/horizontalWheel.ts +85 -0
- package/src/client/ui/svnExport.ts +71 -0
- package/src/client-primitives.d.ts +34 -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/fileOpenSettings.ts +3 -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/editorLimit.ts +46 -0
- package/src/shared/keybindings.ts +10 -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,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
|
+
}
|