dsh-vscode-mode 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +78 -5
  2. package/lib/client.js +5579 -236
  3. package/lib/client.js.map +1 -1
  4. package/lib/index.js +2358 -126
  5. package/lib/index.js.map +1 -1
  6. package/package.json +1 -1
  7. package/src/client/compat.ts +1 -1
  8. package/src/client/dap/BpWidget.ts +262 -0
  9. package/src/client/dap/bpMenu.ts +91 -0
  10. package/src/client/dap/bpRowMenu.ts +53 -0
  11. package/src/client/dap/breakpoints.ts +222 -0
  12. package/src/client/dap/decorate.ts +128 -0
  13. package/src/client/dap/hintLine.ts +29 -0
  14. package/src/client/dap/hover.ts +166 -0
  15. package/src/client/dap/hoverMode.ts +159 -0
  16. package/src/client/dap/hoverTree.ts +722 -0
  17. package/src/client/dap/launchInsert.ts +229 -0
  18. package/src/client/dap/launchSnippetProvider.ts +206 -0
  19. package/src/client/dap/modelPath.ts +23 -0
  20. package/src/client/dap/panelSplit.ts +105 -0
  21. package/src/client/dap/pidPick.ts +24 -0
  22. package/src/client/dap/store.ts +571 -0
  23. package/src/client/dap/toolbarDrag.ts +51 -0
  24. package/src/client/dap/trace.ts +22 -0
  25. package/src/client/dap/variableTree.ts +62 -0
  26. package/src/client/editorLimit.ts +31 -0
  27. package/src/client/index.ts +13 -0
  28. package/src/client/markdownPreview.ts +24 -0
  29. package/src/client/md/componentType.ts +28 -0
  30. package/src/client/md/mdPanel.ts +98 -0
  31. package/src/client/monaco/lsp/index.ts +15 -0
  32. package/src/client/monaco/lsp/providers.ts +2 -0
  33. package/src/client/monaco/theme.ts +15 -0
  34. package/src/client/searchSeed.ts +64 -0
  35. package/src/client/sidebar/panels/DebugPanel.ts +613 -0
  36. package/src/client/sidebar/panels/SearchPanel.ts +70 -15
  37. package/src/client/sidebar/panels/SvnPanel.ts +167 -23
  38. package/src/client/sidebar/panels/index.ts +19 -0
  39. package/src/client/sidebar/types.ts +2 -0
  40. package/src/client/styles/editor.css +186 -1
  41. package/src/client/svnActions.ts +18 -1
  42. package/src/client/svnLog.ts +3 -3
  43. package/src/client/svnStatus.ts +140 -2
  44. package/src/client/tabActions.ts +54 -0
  45. package/src/client/ui/EditorView.ts +711 -29
  46. package/src/client/ui/McpSettings.ts +29 -0
  47. package/src/client/ui/SideBySideDiff.tsx +214 -29
  48. package/src/client/ui/SvnDiffPanel.ts +21 -8
  49. package/src/client/ui/SvnLogDialog.ts +44 -2
  50. package/src/client/ui/SvnPatchDialog.ts +63 -0
  51. package/src/client/ui/SvnSumDialog.ts +77 -0
  52. package/src/client/ui/commandCatalog.ts +54 -0
  53. package/src/client/ui/horizontalWheel.ts +85 -0
  54. package/src/client/ui/svnExport.ts +71 -0
  55. package/src/client-primitives.d.ts +34 -0
  56. package/src/dap/configSnippets.ts +269 -0
  57. package/src/dap/discovery.ts +198 -0
  58. package/src/dap/launchConfig.ts +127 -0
  59. package/src/dap/manager.ts +588 -0
  60. package/src/dap/processList.ts +76 -0
  61. package/src/dap/protocol.ts +80 -0
  62. package/src/dap/provider.ts +49 -0
  63. package/src/dap/rpc.ts +192 -0
  64. package/src/dap/sourcePath.ts +82 -0
  65. package/src/fileOpenSettings.ts +3 -0
  66. package/src/index.ts +7 -3
  67. package/src/lsp/providers.ts +3 -2
  68. package/src/reveal.ts +31 -20
  69. package/src/rpc.ts +11 -3
  70. package/src/search/ripgrep.ts +127 -11
  71. package/src/shared/dap.ts +262 -0
  72. package/src/shared/editorLimit.ts +46 -0
  73. package/src/shared/keybindings.ts +10 -0
  74. package/src/shared/rpc.ts +40 -2
  75. package/src/shared/svn.ts +147 -0
  76. package/src/shared/svnActions.ts +20 -0
  77. package/src/svn.ts +320 -11
  78. package/src/workspace.ts +0 -75
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vscode-mode",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
4
4
  "description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 DSH 0.1.5+ 官方右侧 Sidebar 与对话同屏)+ 命令栏(Ctrl+Shift+P)与可扩展指令系统 + VS Code 兼容代码片段(.code-snippets 全局/项目,IntelliSense 展开)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚)+ SVN 集成(侧栏变更面板/日志弹窗/并排差异)+ 语言服务器(LSP)智能(跳转定义/引用/hover/大纲 + VSIX 扩展市场安装),状态持久化到工作区旁车",
5
5
  "keywords": [
6
6
  "dsh",
@@ -12,7 +12,7 @@ export const SIDEBAR_PLUGIN = 'dsh-better-sidebar'
12
12
 
13
13
  /** 设置 scope 的跨组件上下文形状(与官方 settingsScope 绑定的最小面)。 */
14
14
  export interface SettingsScopeLike {
15
- getSnapshot: () => { status?: string; value?: { fileOpenTool?: unknown; keybindings?: Record<string, string>; sidebarMinWidth?: unknown }; writable?: boolean }
15
+ getSnapshot: () => { status?: string; value?: { fileOpenTool?: unknown; keybindings?: Record<string, string>; sidebarMinWidth?: unknown; maxOpenEditors?: unknown }; writable?: boolean }
16
16
  subscribe: (listener: () => void) => () => void
17
17
  set: (field: string, value: unknown) => Promise<void>
18
18
  }
@@ -0,0 +1,262 @@
1
+ /**
2
+ * dsh-vscode-mode client — 断点行内编辑浮层(逐项对齐 VS Code/CodeBuddy breakpointWidget)。
3
+ *
4
+ * 权威结构(CodeBuddy workbench.desktop.main.js):
5
+ * - **View Zone**(`changeViewZones(z => z.addZone({ afterLineNumber, heightInLines, domNode, afterColumnAffinity:1 }))`)
6
+ * → 占据**整个编辑区宽度**并把下方内容推开(不是 content widget:后者宽度受限且不推挤):
7
+ * `_getWidth(i) = i.width - i.minimap.minimapWidth - i.verticalScrollbarWidth`。
8
+ * - 左侧「模式下拉」四项(表达式 / 命中次数 / 日志消息 / 等待断点)+ 右侧**单行输入框**;
9
+ * 切换模式时各模式内容独立记忆(rememberInput/getInputValue),「等待断点」模式隐藏输入框。
10
+ * - 无「确定」按钮(仅「等待断点」模式有):`Enter` 提交(primary:3)、`Escape` 取消(primary:9),
11
+ * 提示写进输入框占位。
12
+ * - 打开期间给**目标行**挂高亮装饰(`edrv-dap-bp-editing`),关闭时移除。
13
+ * 文案逐字取自 CodeBuddy 官方中文语言包(索引 7259/7262~7265)。
14
+ * 作者 ddj 2026年09月29号
15
+ */
16
+ import { setEditingLine } from './decorate.js'
17
+
18
+ /** 模式索引(与权威实现 context 一致:0 表达式 / 1 命中次数 / 2 日志消息 / 3 等待断点)。 */
19
+ export const BP_MODE = { EXPRESSION: 0, HIT_COUNT: 1, LOG_MESSAGE: 2, WAIT: 3 } as const
20
+
21
+ /** 模式 → 字段键(等待断点无输入字段)。 */
22
+ export type BpFieldKey = 'condition' | 'hitCondition' | 'logMessage'
23
+
24
+ /** 浮层输入。 */
25
+ export interface BpWidgetInput {
26
+ line: number
27
+ entry: { condition?: string; hitCondition?: string; logMessage?: string }
28
+ /** 打开时选中的模式(「添加条件断点…」→ 表达式、「添加记录点…」→ 日志消息)。 */
29
+ mode?: number
30
+ onSave: (fields: { condition: string; hitCondition: string; logMessage: string }) => void
31
+ onClose: () => void
32
+ }
33
+
34
+ /** 浮层宿主(编辑器 + zone 句柄),由调用方持有用于关闭。 */
35
+ export interface BpWidgetHost {
36
+ close: () => void
37
+ }
38
+
39
+ /** 模式项(顺序与权威实现一致)。 */
40
+ const MODES: readonly { mode: number; label: string; placeholder: string }[] = [
41
+ { mode: BP_MODE.EXPRESSION, label: '表达式', placeholder: '在表达式结果为真时中断。按 "Enter" 键确认,"Escape" 键取消。' },
42
+ { mode: BP_MODE.HIT_COUNT, label: '命中次数', placeholder: '在满足命中次数条件时中断。按 "Enter" 键确认,"Escape" 键取消。' },
43
+ { mode: BP_MODE.LOG_MESSAGE, label: '日志消息', placeholder: '断点命中时记录的消息。{} 内的表达式将被替换。按 "Enter" 键确认,"Escape" 键取消。' },
44
+ { mode: BP_MODE.WAIT, label: '等待断点', placeholder: '选择此断点命中前必须先命中的断点。' },
45
+ ]
46
+ /** 模式清单(调试面板行内编辑器同源复用:标签与占位文案一致)。 */
47
+ export { MODES }
48
+
49
+ /** 模式 → 字段键映射(等待断点返回 null);调试面板行内编辑器同源复用。 */
50
+ export function fieldOfMode(mode: number): BpFieldKey | null {
51
+ if (mode === BP_MODE.EXPRESSION) return 'condition'
52
+ if (mode === BP_MODE.HIT_COUNT) return 'hitCondition'
53
+ if (mode === BP_MODE.LOG_MESSAGE) return 'logMessage'
54
+ return null
55
+ }
56
+
57
+ /**
58
+ * 打开断点行内编辑浮层(View Zone:整编辑区宽 + 推开内容 + 目标行高亮)。
59
+ * @author ddj 2026年09月29号
60
+ * @param editor Monaco 编辑器
61
+ * @param input 目标行、初始值、初始模式、保存/取消回调
62
+ * @returns 宿主句柄(close 关闭浮层)
63
+ */
64
+ export function createBpWidget(editor: unknown, input: BpWidgetInput): BpWidgetHost {
65
+ const ed = editor as {
66
+ changeViewZones?: (cb: (accessor: { addZone: (zone: unknown) => string; removeZone: (id: string) => void }) => void) => void
67
+ } | null
68
+ let zoneId: string | null = null
69
+ let closed = false
70
+
71
+ // 各模式独立记忆(对齐 rememberInput / getInputValue)
72
+ const remembered: Record<BpFieldKey, string> = {
73
+ condition: input.entry?.condition ?? '',
74
+ hitCondition: input.entry?.hitCondition ?? '',
75
+ logMessage: input.entry?.logMessage ?? '',
76
+ }
77
+ let mode = typeof input.mode === 'number' ? input.mode : BP_MODE.EXPRESSION
78
+
79
+ const root = document.createElement('div')
80
+ root.className = 'edrv-bpwidget'
81
+ root.setAttribute('data-edrv-view', '1')
82
+
83
+ // 左侧模式下拉
84
+ const select = document.createElement('select')
85
+ select.className = 'edrv-bpwidget-mode'
86
+ select.title = '断点类型'
87
+ for (const item of MODES) {
88
+ const opt = document.createElement('option')
89
+ opt.value = String(item.mode)
90
+ opt.textContent = item.label
91
+ select.appendChild(opt)
92
+ }
93
+ select.value = String(mode)
94
+
95
+ // 右侧单行输入
96
+ const field = document.createElement('input')
97
+ field.className = 'edrv-bpwidget-input'
98
+ field.spellcheck = false
99
+
100
+ root.appendChild(select)
101
+ root.appendChild(field)
102
+
103
+ /** 当前模式正文(等待断点模式无正文)。 */
104
+ const currentText = (): string => {
105
+ const key = fieldOfMode(mode)
106
+ return key ? remembered[key] : ''
107
+ }
108
+
109
+ /** 模式切换:先记忆当前,再套用新模式的正文与占位。 */
110
+ const applyMode = (): void => {
111
+ const key = fieldOfMode(mode)
112
+ const item = MODES.find((m) => m.mode === mode)
113
+ field.value = currentText()
114
+ field.placeholder = item?.placeholder ?? ''
115
+ field.hidden = key === null
116
+ select.value = String(mode)
117
+ if (key !== null) { try { field.focus(); field.select() } catch { /* 忽略 */ } }
118
+ }
119
+
120
+ /** 记忆当前正文。 */
121
+ const remember = (): void => {
122
+ const key = fieldOfMode(mode)
123
+ if (!key) return
124
+ remembered[key] = field.value
125
+ }
126
+
127
+ /** 提交:记忆全部字段并回调。 */
128
+ const save = (): void => {
129
+ remember()
130
+ input.onSave({ ...remembered })
131
+ }
132
+
133
+ select.addEventListener('change', () => {
134
+ remember()
135
+ mode = Number(select.value)
136
+ applyMode()
137
+ })
138
+ // 输入框/下拉内点击:preventDefault 阻止 Monaco 抢焦(浏览器默认聚焦被取消后由
139
+ // 显式 focus 补上),保证"点进去一定能输入"。
140
+ const keepFocus = (ev: MouseEvent): void => {
141
+ ev.preventDefault()
142
+ ev.stopPropagation()
143
+ const el = ev.currentTarget === select ? select : field
144
+ try { el.focus() } catch { /* 忽略 */ }
145
+ }
146
+ field.addEventListener('mousedown', keepFocus)
147
+ select.addEventListener('mousedown', keepFocus)
148
+
149
+ const onKeyDown = (ev: KeyboardEvent): void => {
150
+ ev.stopPropagation()
151
+ if (ev.key === 'Enter') { ev.preventDefault(); save(); return }
152
+ if (ev.key === 'Escape') { ev.preventDefault(); input.onClose() }
153
+ }
154
+ field.addEventListener('keydown', onKeyDown)
155
+ select.addEventListener('keydown', onKeyDown)
156
+
157
+ // ---- 全局接管(浮层存活期间)----
158
+ // ① window 级 Enter/Esc:焦点被编辑器抢走后仍可确认/取消(对齐 VS Code 的
159
+ // acceptInput primary:Enter 与 closeBreakpointWidget primary:Escape 全局命令)。
160
+ // ② 编辑器内点击浮层之外 → 关闭浮层(对齐 VS Code:点击别处即收起,想改重新右键)。
161
+ // ③ document 级 mousedown 捕获:浮层内点击阻断 Monaco 抢焦;输入框/下拉点进去可控。
162
+ const onWindowKey = (ev: KeyboardEvent): void => {
163
+ if (ev.key === 'Enter') { ev.preventDefault(); ev.stopPropagation(); save(); return }
164
+ if (ev.key === 'Escape') { ev.preventDefault(); ev.stopPropagation(); input.onClose() }
165
+ }
166
+ const onEditorMouseDown = (ev: MouseEvent): void => {
167
+ const target = ev.target as Node | null
168
+ if (target && root.contains(target)) return
169
+ input.onClose()
170
+ }
171
+ const onDocMouseDownCapture = (ev: MouseEvent): void => {
172
+ const target = ev.target as HTMLElement | null
173
+ if (!target) return
174
+ if (root.contains(target)) {
175
+ if (target !== field && target !== select) {
176
+ ev.preventDefault()
177
+ ev.stopPropagation()
178
+ try { field.focus() } catch { /* 忽略 */ }
179
+ }
180
+ return
181
+ }
182
+ // 浮层之外的点击(含编辑器正文/行号/glyph)→ 关闭(对齐 VS Code)
183
+ if ((editor as { getDomNode?: () => HTMLElement | null })?.getDomNode?.()?.contains(target)) input.onClose()
184
+ }
185
+ window.addEventListener('keydown', onWindowKey, true)
186
+ document.addEventListener('mousedown', onDocMouseDownCapture, true)
187
+ const editorDom = (editor as { getDomNode?: () => HTMLElement | null })?.getDomNode?.()
188
+ editorDom?.addEventListener('mousedown', onEditorMouseDown)
189
+ applyMode()
190
+
191
+ /** 关闭时解除全局接管(close 内调用)。 */
192
+ const detachGlobal = (): void => {
193
+ window.removeEventListener('keydown', onWindowKey, true)
194
+ document.removeEventListener('mousedown', onDocMouseDownCapture, true)
195
+ editorDom?.removeEventListener('mousedown', onEditorMouseDown)
196
+ }
197
+
198
+ // 挂 View Zone(推开内容)并把浮层铺满整个编辑区宽度。
199
+ // ⚠️ Monaco 的 .view-zones 容器宽度只有内容区(contentWidth,从 contentLeft 起),
200
+ // 而 VS Code 的 zone-widget 整宽 = width - minimapWidth - verticalScrollbarWidth
201
+ // (见其 `_getWidth`)。这里用负 left + 显式宽度补偿 contentLeft 与右侧滚动条。
202
+ const syncWidth = (): void => {
203
+ const layout = (editor as { getLayoutInfo?: () => Record<string, unknown> } | null)?.getLayoutInfo?.()
204
+ if (!layout) return
205
+ const contentLeft = Number(layout.contentLeft) || 0
206
+ const scrollbarWidth = Number((layout as { verticalScrollbarWidth?: number }).verticalScrollbarWidth) || 0
207
+ const width = contentLeft + (Number(layout.contentWidth) || 0) + scrollbarWidth
208
+ // 用 important 覆盖 Monaco addZone 写入的内联 `width:100%` / `left:0` / `display:block`。
209
+ // display 必须是 flex,否则子项不参与 flex 布局、输入框只剩默认宽度(用户反馈"输入框太短")。
210
+ // 注意:Monaco 在 addZone 之后仍可能重写内联样式,故此处由 onComputedHeight 反复调用兜住。
211
+ root.style.setProperty('left', -contentLeft + 'px', 'important')
212
+ root.style.setProperty('width', width + 'px', 'important')
213
+ root.style.setProperty('display', 'flex', 'important')
214
+ root.style.setProperty('align-items', 'center', 'important')
215
+ // 行内元素从编辑器最左(行号列)开始,右侧留出滚动条宽度(对齐 VS Code zone-widget 起始位)
216
+ root.style.setProperty('padding-left', '4px', 'important')
217
+ root.style.setProperty('padding-right', (scrollbarWidth + 6) + 'px', 'important')
218
+ }
219
+ // 挂 View Zone(推开内容)并把浮层铺满整个编辑区宽度。
220
+ // ⚠️ Monaco 的 addZone 会用内联样式强制 `width: 100%`(= .view-zones 内容区宽,不含
221
+ // glyph/行号/装饰列与右侧滚动条),直接设宽度会被覆盖。对齐 VS Code:在其
222
+ // `onComputedHeight`(每次布局回调)里重新套用整宽 = width - minimapWidth - scrollbarWidth。
223
+ ed?.changeViewZones?.((accessor) => {
224
+ zoneId = accessor.addZone({
225
+ afterLineNumber: input.line,
226
+ heightInLines: 1.6,
227
+ domNode: root,
228
+ afterColumnAffinity: 1,
229
+ onComputedHeight: () => syncWidth(),
230
+ })
231
+ })
232
+ setEditingLine(ed, input.line)
233
+ // addZone 首次布局晚于本行;且 Monaco 之后仍可能重写内联样式(display/width),
234
+ // 故布局回调 + 首帧 + 短周期守卫(浮层存活期间,关闭即停)三处兜住。
235
+ setTimeout(() => { if (!closed) syncWidth() }, 0)
236
+ const guard = setInterval(() => {
237
+ if (closed) { clearInterval(guard); return }
238
+ syncWidth()
239
+ }, 250)
240
+ setTimeout(() => clearInterval(guard), 3000).unref?.()
241
+
242
+ const close = (): void => {
243
+ if (closed) return
244
+ closed = true
245
+ detachGlobal()
246
+ setEditingLine(ed, null)
247
+ if (zoneId) {
248
+ const id = zoneId
249
+ zoneId = null
250
+ try { ed?.changeViewZones?.((accessor) => accessor.removeZone(id)) } catch { /* 编辑器可能已销毁 */ }
251
+ }
252
+ try { root.remove() } catch { /* 忽略 */ }
253
+ }
254
+
255
+ // 首帧后聚焦(有输入框时聚焦输入框,否则聚焦下拉)
256
+ setTimeout(() => {
257
+ if (closed) return
258
+ try { (fieldOfMode(mode) ? field : select).focus?.() } catch { /* 忽略 */ }
259
+ }, 0)
260
+
261
+ return { close }
262
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * dsh-vscode-mode client — 断点 gutter 右键菜单条目(纯函数,可单测)。
3
+ * 逐条对齐 CodeBuddy(VS Code 内核)breakpointEditorContribution.getContextMenuActions
4
+ * 与其官方中文语言包译文:
5
+ * - 无断点:添加断点 / 添加条件断点... / 添加记录点... / 添加触发的断点...
6
+ * - 有断点:删除 {noun}(右侧 Delete)/ 编辑 {noun}… / 禁用{noun} 或 启用 {noun}
7
+ * noun 随类型切换:普通断点 =「断点」,记录点(带日志消息)=「记录点」
8
+ * - 暂停且支持时:前置分隔线 + 运行到行
9
+ *
10
+ * 文案逐字取自语言包(**空格与省略号形态不可改**):
11
+ * removeBreakpoint「删除 {0}」/ editBreakpoint「编辑 {0}…」/
12
+ * disableBreakpoint「禁用{0}」/ enableBreakpoint「启用 {0}」/ runToLine「运行到行」
13
+ * 本模块只产出条目数据,onClick 由 EditorView 装配(无副作用)。
14
+ * 作者 ddj 2026年09月29号
15
+ */
16
+
17
+ /** 菜单动作 id。 */
18
+ export type BpMenuAction =
19
+ | 'add'
20
+ | 'add-conditional'
21
+ | 'add-logpoint'
22
+ | 'add-triggered'
23
+ | 'delete'
24
+ | 'edit'
25
+ | 'disable'
26
+ | 'enable'
27
+ | 'run-to-line'
28
+
29
+ /** 一条菜单条目。 */
30
+ export interface BpMenuEntry {
31
+ id: BpMenuAction
32
+ label: string
33
+ /** 前置分隔线(运行到行与断点条目之间,对齐 CodeBuddy)。 */
34
+ separator?: boolean
35
+ /** 右侧键位提示(CodeBuddy 仅删除项带 Delete)。 */
36
+ hint?: string
37
+ /** 显示但当前调试器不支持:点击给出明确提示而非静默失败。 */
38
+ unsupported?: boolean
39
+ }
40
+
41
+ /** 菜单构造输入。 */
42
+ export interface BpMenuState {
43
+ /** 命中行是否已有断点。 */
44
+ hasBreakpoint: boolean
45
+ /** 已有断点是否启用(无断点时忽略)。 */
46
+ enabled?: boolean
47
+ /** 已有断点是否为记录点(带日志消息)→ 名词用「记录点」。 */
48
+ isLogpoint?: boolean
49
+ /** 调试器是否暂停中(运行到行仅在暂停时出现)。 */
50
+ paused?: boolean
51
+ /** 调试器是否支持运行到行。 */
52
+ canRunTo?: boolean
53
+ /** 调试器是否支持触发的断点。 */
54
+ canTriggered?: boolean
55
+ }
56
+
57
+ /**
58
+ * 构造断点右键菜单条目。
59
+ * @author ddj 2026年09月29号
60
+ * @param state 命中行与调试器状态
61
+ * @returns 菜单条目列表(顺序即展示顺序)
62
+ */
63
+ export function buildBpMenu(state: BpMenuState): BpMenuEntry[] {
64
+ const entries = state.hasBreakpoint ? ownedEntries(state) : addEntries(state.canTriggered === true)
65
+ // CodeBuddy 在 if/else 链之后无条件追加:空行同样支持运行到行(仅在暂停时)
66
+ if (state.paused === true && state.canRunTo === true) {
67
+ entries.push({ id: 'run-to-line', label: '运行到行', separator: true })
68
+ }
69
+ return entries
70
+ }
71
+
72
+ /** 无断点:四个添加类条目(对齐 CodeBuddy 无条件四项)。 */
73
+ function addEntries(canTriggered: boolean): BpMenuEntry[] {
74
+ return [
75
+ { id: 'add', label: '添加断点' },
76
+ { id: 'add-conditional', label: '添加条件断点...' },
77
+ { id: 'add-logpoint', label: '添加记录点...' },
78
+ { id: 'add-triggered', label: '添加触发的断点...', unsupported: !canTriggered },
79
+ ]
80
+ }
81
+
82
+ /** 已有断点:删除 / 编辑 / 启停(名词随记录点切换)。 */
83
+ function ownedEntries(state: BpMenuState): BpMenuEntry[] {
84
+ const noun = state.isLogpoint === true ? '记录点' : '断点'
85
+ const enabled = state.enabled !== false
86
+ return [
87
+ { id: 'delete', label: '删除 ' + noun, hint: 'Delete' },
88
+ { id: 'edit', label: '编辑 ' + noun + '…' },
89
+ enabled ? { id: 'disable', label: '禁用' + noun } : { id: 'enable', label: '启用 ' + noun },
90
+ ]
91
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * dsh-vscode-mode client — 调试面板断点行右键菜单条目(纯函数,可单测)。
3
+ * 对齐 CodeBuddy(VS Code 内核)断点视图行上下文菜单:
4
+ * 编辑断点… / 编辑条件… / 编辑命中次数… / 编辑记录点… / 启用|禁用断点 / 删除断点。
5
+ * 四个编辑条目都打开面板行内编辑器,仅初始模式不同
6
+ * (0 表达式 / 1 命中次数 / 2 日志消息;"编辑断点…"按现有字段自动判定)。
7
+ * 本模块只产出条目数据,onClick 由 DebugPanel 装配(无副作用)。
8
+ * 作者 ddj 2026年09月21号
9
+ */
10
+
11
+ /** 菜单动作 id。 */
12
+ export type BpRowMenuAction =
13
+ | 'edit'
14
+ | 'edit-condition'
15
+ | 'edit-hit-count'
16
+ | 'edit-logpoint'
17
+ | 'enable'
18
+ | 'disable'
19
+ | 'remove'
20
+
21
+ /** 一条菜单条目。 */
22
+ export interface BpRowMenuEntry {
23
+ id: BpRowMenuAction
24
+ label: string
25
+ /** 前置分隔线(编辑组与启停/删除组之间,对齐 CodeBuddy)。 */
26
+ separator?: boolean
27
+ }
28
+
29
+ /** 菜单构造输入。 */
30
+ export interface BpRowMenuState {
31
+ /** 当前断点是否启用(禁用态显示「启用断点」)。 */
32
+ enabled?: boolean
33
+ }
34
+
35
+ /**
36
+ * 构造调试面板断点行右键菜单条目。
37
+ * @author ddj 2026年09月21号
38
+ * @param state 行内断点状态
39
+ * @returns 菜单条目列表(顺序即展示顺序)
40
+ */
41
+ export function buildBpRowMenu(state: BpRowMenuState): BpRowMenuEntry[] {
42
+ const enabled = state.enabled !== false
43
+ return [
44
+ { id: 'edit', label: '编辑断点…' },
45
+ { id: 'edit-condition', label: '编辑条件…' },
46
+ { id: 'edit-hit-count', label: '编辑命中次数…' },
47
+ { id: 'edit-logpoint', label: '编辑记录点…' },
48
+ enabled
49
+ ? { id: 'disable', label: '禁用断点', separator: true }
50
+ : { id: 'enable', label: '启用断点', separator: true },
51
+ { id: 'remove', label: '删除断点' },
52
+ ]
53
+ }
@@ -0,0 +1,222 @@
1
+ /**
2
+ * dsh-vscode-mode client — 调试断点集合(纯函数 + localStorage 持久)。
3
+ * 存储:`edrv.dap.bp.<scope>` → Record<relPath, BpEntry[]>(行升序)。
4
+ * scope 与编辑器状态作用域一致(workspaceScopeOf 产物),跨刷新保留。
5
+ * 作者 ddj 2026年09月29号
6
+ */
7
+
8
+ /** 单条断点。 */
9
+ export interface BpEntry {
10
+ line: number
11
+ enabled: boolean
12
+ /** 命中条件表达式(Lua,如 count % 10 == 0;空=无条件)。 */
13
+ condition?: string
14
+ /** 命中次数条件(适配器 hitCondition,如 >=5)。 */
15
+ hitCondition?: string
16
+ /** 日志断点消息(适配器 logMessage;存在时命中不打断仅打日志)。 */
17
+ logMessage?: string
18
+ }
19
+
20
+ /** 断点表:relPath → 条目列表。 */
21
+ export type BpMap = Record<string, BpEntry[]>
22
+
23
+ /** localStorage key 前缀。 */
24
+ const KEY_PREFIX = 'edrv.dap.bp.'
25
+
26
+ /**
27
+ * 读断点表(缺失/损坏返回空表)。
28
+ * @author ddj 2026年09月29号
29
+ * @param scope 状态作用域
30
+ */
31
+ export function loadBpMap(scope: string): BpMap {
32
+ try {
33
+ const raw = localStorage.getItem(KEY_PREFIX + scope)
34
+ if (!raw) return {}
35
+ const data = JSON.parse(raw)
36
+ if (!data || typeof data !== 'object' || Array.isArray(data)) return {}
37
+ const out: BpMap = {}
38
+ for (const [path, list] of Object.entries(data as Record<string, unknown>)) {
39
+ if (!Array.isArray(list)) continue
40
+ const entries: BpEntry[] = []
41
+ for (const item of list) {
42
+ if (!item || typeof item !== 'object') continue
43
+ const line = (item as BpEntry).line
44
+ if (typeof line !== 'number' || line < 1) continue
45
+ const entry: BpEntry = { line, enabled: (item as BpEntry).enabled !== false }
46
+ if (typeof (item as BpEntry).condition === 'string') entry.condition = (item as BpEntry).condition
47
+ if (typeof (item as BpEntry).hitCondition === 'string') entry.hitCondition = (item as BpEntry).hitCondition
48
+ if (typeof (item as BpEntry).logMessage === 'string') entry.logMessage = (item as BpEntry).logMessage
49
+ entries.push(entry)
50
+ }
51
+ if (entries.length) out[path] = entries.sort((a, b) => a.line - b.line)
52
+ }
53
+ return out
54
+ } catch {
55
+ return {}
56
+ }
57
+ }
58
+
59
+ /**
60
+ * 写断点表(空表时清 key;写失败静默)。
61
+ * @author ddj 2026年09月29号
62
+ * @param scope 状态作用域
63
+ * @param map 断点表
64
+ */
65
+ export function saveBpMap(scope: string, map: BpMap): void {
66
+ try {
67
+ const keys = Object.keys(map)
68
+ if (!keys.length) localStorage.removeItem(KEY_PREFIX + scope)
69
+ else localStorage.setItem(KEY_PREFIX + scope, JSON.stringify(map))
70
+ } catch { /* 无 localStorage 环境:仅内存生效 */ }
71
+ }
72
+
73
+ /**
74
+ * 切换某文件某行断点(纯函数,返回新表与是否新增)。
75
+ * @author ddj 2026年09月29号
76
+ * @param map 原表
77
+ * @param path 文件相对路径
78
+ * @param line 行号
79
+ */
80
+ export function toggleBp(map: BpMap, path: string, line: number): { map: BpMap; added: boolean } {
81
+ const list = map[path] ?? []
82
+ const hit = list.find((item) => item.line === line)
83
+ let nextList: BpEntry[]
84
+ let added: boolean
85
+ if (hit) {
86
+ nextList = list.filter((item) => item.line !== line)
87
+ added = false
88
+ } else {
89
+ nextList = [...list, { line, enabled: true }].sort((a, b) => a.line - b.line)
90
+ added = true
91
+ }
92
+ const next: BpMap = { ...map }
93
+ if (nextList.length) next[path] = nextList
94
+ else delete next[path]
95
+ return { map: next, added }
96
+ }
97
+
98
+ /**
99
+ * 取某文件启用的断点行(升序)。
100
+ * @author ddj 2026年09月29号
101
+ * @param map 断点表
102
+ * @param path 文件相对路径
103
+ */
104
+ export function bpLinesOf(map: BpMap, path: string): number[] {
105
+ return (map[path] ?? []).filter((item) => item.enabled).map((item) => item.line)
106
+ }
107
+
108
+ /**
109
+ * 取某文件禁用的断点行(升序;灰点展示用)。
110
+ * @author ddj 2026年09月29号
111
+ * @param map 断点表
112
+ * @param path 文件相对路径
113
+ */
114
+ export function bpDisabledLinesOf(map: BpMap, path: string): number[] {
115
+ return (map[path] ?? []).filter((item) => !item.enabled).map((item) => item.line)
116
+ }
117
+
118
+ /**
119
+ * 断点表规模(跨文件总数;版本指纹用)。
120
+ * @author ddj 2026年09月29号
121
+ * @param map 断点表
122
+ */
123
+ export function bpTotalOf(map: BpMap): number {
124
+ let total = 0
125
+ for (const list of Object.values(map)) total += list.length
126
+ return total
127
+ }
128
+
129
+ /**
130
+ * 全量启停所有断点(不删条目,只改 enabled;纯函数)。
131
+ * @author ddj 2026年09月21号
132
+ * @param map 原表
133
+ * @param enabled 目标启用态
134
+ */
135
+ export function setAllEnabled(map: BpMap, enabled: boolean): BpMap {
136
+ const next: BpMap = {}
137
+ for (const [path, list] of Object.entries(map)) {
138
+ if (!list.length) continue
139
+ next[path] = list.map((item) => (item.enabled === enabled ? item : { ...item, enabled }))
140
+ }
141
+ return next
142
+ }
143
+
144
+ /**
145
+ * 清空所有断点(纯函数;不修改原表)。
146
+ * 同时返回曾有条目的文件清单(store 逐文件 syncPoints 空载荷用)。
147
+ * @author ddj 2026年09月21号
148
+ * @param map 原表
149
+ */
150
+ export function removeAll(map: BpMap): { map: BpMap; files: string[] } {
151
+ return { map: {}, files: Object.keys(map).filter((path) => (map[path] ?? []).length > 0) }
152
+ }
153
+
154
+ /**
155
+ * 更新某行断点的编辑字段(条件/命中次数/日志;纯函数)。
156
+ * 空串视为清除该字段。
157
+ * @author ddj 2026年09月29号
158
+ * @param map 原表
159
+ * @param path 文件相对路径
160
+ * @param line 行号
161
+ * @param fields 要写入的字段(undefined = 不变)
162
+ */
163
+ export function updateBpFields(
164
+ map: BpMap,
165
+ path: string,
166
+ line: number,
167
+ fields: { condition?: string; hitCondition?: string; logMessage?: string },
168
+ ): BpMap {
169
+ const clean = (value?: string): string | undefined => {
170
+ const trimmed = typeof value === 'string' ? value.trim() : undefined
171
+ return trimmed ? trimmed : undefined
172
+ }
173
+ const list = (map[path] ?? []).map((item) => {
174
+ if (item.line !== line) return item
175
+ const next: BpEntry = { ...item }
176
+ if (fields.condition !== undefined) next.condition = clean(fields.condition)
177
+ if (fields.hitCondition !== undefined) next.hitCondition = clean(fields.hitCondition)
178
+ if (fields.logMessage !== undefined) next.logMessage = clean(fields.logMessage)
179
+ return next
180
+ })
181
+ return { ...map, [path]: list }
182
+ }
183
+
184
+ /** 断点行跳转入参(调试面板行视图:文件路径 + 条目)。 */
185
+ export interface BpJumpRowLike {
186
+ path?: string
187
+ entry?: { line?: number }
188
+ }
189
+
190
+ /** 断点行跳转目标(1-based;列固定 1,与搜索面板命中跳转同口径)。 */
191
+ export interface BpJumpTarget {
192
+ path: string
193
+ line: number
194
+ column: number
195
+ }
196
+
197
+ /**
198
+ * 推导断点行的跳转目标(调试面板点击断点项 → 打开文件并定位到断点行)。
199
+ * 路径缺失或行号非 ≥1 的有限数时返回 null(提前返回,不发起无效导航)。
200
+ * @author ddj 2026年09月21号
201
+ * @param row 面板行视图(文件路径 + 断点条目)
202
+ * @returns 跳转目标;入参不合法时返回 null
203
+ */
204
+ export function bpJumpTargetOf(row: BpJumpRowLike | null | undefined): BpJumpTarget | null {
205
+ const path = typeof row?.path === 'string' ? row.path : ''
206
+ const line = row?.entry?.line
207
+ if (!path || typeof line !== 'number' || !Number.isFinite(line) || line < 1) return null
208
+ return { path, line: Math.floor(line), column: 1 }
209
+ }
210
+
211
+ /**
212
+ * 路径是否为可下断点的调试目标(当前调试器为 Lua;按扩展名后缀匹配,
213
+ * 支持 '.lua' 与 '.lua.bytes' 这类带尾缀的 Unity 资产形态)。
214
+ * @author ddj 2026年09月29号
215
+ * @param path 文件路径(相对/绝对均可)
216
+ * @param exts 允许的扩展名列表(默认 ['.lua'];大小写不敏感)
217
+ */
218
+ export function isDebuggablePath(path: string, exts: readonly string[] = ['.lua']): boolean {
219
+ const lower = String(path || '').toLowerCase()
220
+ if (!lower) return false
221
+ return exts.some((ext) => lower.endsWith(String(ext).toLowerCase()))
222
+ }