dsh-vscode-mode 0.5.3 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +55 -2
  2. package/lib/client.js +5173 -355
  3. package/lib/client.js.map +1 -1
  4. package/lib/index.js +2356 -126
  5. package/lib/index.js.map +1 -1
  6. package/package.json +1 -1
  7. package/src/client/dap/BpWidget.ts +262 -0
  8. package/src/client/dap/bpMenu.ts +91 -0
  9. package/src/client/dap/bpRowMenu.ts +53 -0
  10. package/src/client/dap/breakpoints.ts +222 -0
  11. package/src/client/dap/decorate.ts +128 -0
  12. package/src/client/dap/hintLine.ts +29 -0
  13. package/src/client/dap/hover.ts +166 -0
  14. package/src/client/dap/hoverMode.ts +159 -0
  15. package/src/client/dap/hoverTree.ts +722 -0
  16. package/src/client/dap/launchInsert.ts +229 -0
  17. package/src/client/dap/launchSnippetProvider.ts +206 -0
  18. package/src/client/dap/modelPath.ts +23 -0
  19. package/src/client/dap/panelSplit.ts +105 -0
  20. package/src/client/dap/pidPick.ts +24 -0
  21. package/src/client/dap/store.ts +571 -0
  22. package/src/client/dap/toolbarDrag.ts +51 -0
  23. package/src/client/dap/trace.ts +22 -0
  24. package/src/client/dap/variableTree.ts +62 -0
  25. package/src/client/index.ts +9 -0
  26. package/src/client/monaco/lsp/index.ts +15 -0
  27. package/src/client/monaco/lsp/providers.ts +2 -0
  28. package/src/client/monaco/theme.ts +15 -0
  29. package/src/client/sidebar/panels/DebugPanel.ts +613 -0
  30. package/src/client/sidebar/panels/SvnPanel.ts +167 -23
  31. package/src/client/sidebar/panels/index.ts +19 -0
  32. package/src/client/sidebar/types.ts +2 -0
  33. package/src/client/styles/editor.css +176 -0
  34. package/src/client/svnActions.ts +18 -1
  35. package/src/client/svnLog.ts +3 -3
  36. package/src/client/svnStatus.ts +140 -2
  37. package/src/client/ui/EditorView.ts +544 -16
  38. package/src/client/ui/SideBySideDiff.tsx +214 -29
  39. package/src/client/ui/SvnDiffPanel.ts +21 -8
  40. package/src/client/ui/SvnLogDialog.ts +44 -2
  41. package/src/client/ui/SvnPatchDialog.ts +63 -0
  42. package/src/client/ui/SvnSumDialog.ts +77 -0
  43. package/src/client/ui/commandCatalog.ts +49 -0
  44. package/src/client/ui/svnExport.ts +71 -0
  45. package/src/dap/configSnippets.ts +269 -0
  46. package/src/dap/discovery.ts +198 -0
  47. package/src/dap/launchConfig.ts +127 -0
  48. package/src/dap/manager.ts +588 -0
  49. package/src/dap/processList.ts +76 -0
  50. package/src/dap/protocol.ts +80 -0
  51. package/src/dap/provider.ts +49 -0
  52. package/src/dap/rpc.ts +192 -0
  53. package/src/dap/sourcePath.ts +82 -0
  54. package/src/index.ts +7 -3
  55. package/src/lsp/providers.ts +3 -2
  56. package/src/reveal.ts +31 -20
  57. package/src/rpc.ts +11 -3
  58. package/src/search/ripgrep.ts +127 -11
  59. package/src/shared/dap.ts +262 -0
  60. package/src/shared/keybindings.ts +7 -0
  61. package/src/shared/rpc.ts +40 -2
  62. package/src/shared/svn.ts +147 -0
  63. package/src/shared/svnActions.ts +20 -0
  64. package/src/svn.ts +320 -11
  65. package/src/workspace.ts +0 -75
@@ -58,13 +58,15 @@ import { onLspProgress, refreshStatus, setSession as setLspSession } from '../mo
58
58
  import { setupAiInline, trackAiEditor, aiInlineEnabled } from '../ai/inlineProvider.js'
59
59
  import { SnippetsPicker } from './SnippetsPicker.js'
60
60
  import { invalidateSnippets, setSnippetsSession, setupSnippets } from '../snippets/provider.js'
61
+ import { setupLaunchJson, prefetchSnippets } from '../dap/launchSnippetProvider.js'
62
+ import { findArrayPos, LAUNCH_JSON_RE } from '../dap/launchInsert.js'
61
63
  import {
62
64
  absoluteOf, ancestorDirsOf, applyClose, baseNameOf, closeAll, closeOthers, closeRight, closeSaved,
63
65
  evictPlan, insertTab, isTreeRevealable, normalizeTabs, pickActive, relativeOf, tabPathOf, togglePin,
64
66
  } from '../tabActions.js'
65
67
  import { getMaxOpenEditors } from '../editorLimit.js'
66
68
  import { buildTabMenu } from '../tabMenu.js'
67
- import { ensureSvnChanges, ensureSvnStatus, getSvnChanges, getSvnStatus, refreshSvnChanges, svnAdd, svnChangeMapOf, svnDiffBase, svnEditorActions, svnRevert, svnTortoise, svnUpdate } from '../svnStatus.js'
69
+ import { ensureSvnChanges, ensureSvnStatus, getSvnChanges, getSvnStatus, refreshSvnChanges, svnAdd, svnChangeMapOf, svnDiffBase, svnDiffLocalPair, svnEditorActions, svnRevert, svnTortoise, svnUpdate } from '../svnStatus.js'
68
70
  import { ensureSvnLog, getSvnLog, refreshSvnLog, svnDiffPair, svnDiffRev, svnDiffWorking, svnLogKeyOf, svnLogLoadedLimit, svnLogTruncated, svnWcRev, SVN_LOG_SHOW_ALL_LIMIT } from '../svnLog.js'
69
71
  import { runSvnAction } from '../svnActions.js'
70
72
  import { dshTrace } from '../svnStore.js'
@@ -74,7 +76,18 @@ import type { SvnAction } from '../../shared/svn.js'
74
76
  import { createSaveTimer } from '../saveDebounce.js'
75
77
  import { ContextMenu } from './ContextMenu.js'
76
78
  import { SvnLogDialog } from './SvnLogDialog.js'
79
+ import { SvnPatchDialog } from './SvnPatchDialog.js'
80
+ import { SvnSumDialog } from './SvnSumDialog.js'
77
81
  import { LogDialog } from './LogDialog.js'
82
+ import { dapStore } from '../dap/store.js'
83
+ import { bpDisabledLinesOf, bpLinesOf } from '../dap/breakpoints.js'
84
+ import { applyBpDecorations, setEditingLine, setHintLine, setPausedLine } from '../dap/decorate.js'
85
+ import { hintLineFor } from '../dap/hintLine.js'
86
+ import { createBpWidget } from '../dap/BpWidget.js'
87
+ import { buildBpMenu } from '../dap/bpMenu.js'
88
+ import { clampToolbarPosition, loadToolbarPosition, saveToolbarPosition } from '../dap/toolbarDrag.js'
89
+ import { editorModelPathOf } from '../dap/modelPath.js'
90
+ import { dapTrace } from '../dap/trace.js'
78
91
 
79
92
  /** 日志弹窗每次加载条数(「加载更多」按此步长递增;上限由 host 的 SVN_LOG_SHOW_ALL_CAP 约束)。 */
80
93
  const SVN_LOG_PAGE = 100
@@ -90,6 +103,18 @@ function svnLogOptsOf(svnLog) {
90
103
  return { stopOnCopy: svnLog.soc === true, showMerged: svnLog.merged === true, range: svnLog.range ?? null }
91
104
  }
92
105
 
106
+ /**
107
+ * 工作区相对路径 → 所在目录相对路径(W2-2 目录对比的默认目标;无目录 = 工作副本根)。
108
+ * @author ddj 2026年09月20号
109
+ * @param relPath 工作区相对路径(可空)
110
+ * @returns 目录相对路径('' = 工作副本根)
111
+ */
112
+ function dirOfRel(relPath) {
113
+ const value = String(relPath ?? '').replace(/\\/g, '/')
114
+ const cut = value.lastIndexOf('/')
115
+ return cut > 0 ? value.slice(0, cut) : ''
116
+ }
117
+
93
118
  /** 跳转目标高亮的保留时长(LSP/搜索跳转落地后给用户的位置提示,到期自动清除)。 */
94
119
  const NAV_FLASH_MS = 1200
95
120
 
@@ -194,7 +219,9 @@ export function EditorView(props) {
194
219
  const [tabMenu, setTabMenu] = React.useState(null) // Tab 右键菜单 { x,y,path }(编辑区右键走 Monaco 原生菜单,无此浮层)
195
220
  const [svnStatus, setSvnStatus] = React.useState(null) // SVN 能力状态(SvnStatusPayload;null=未加载,SVN 入口隐藏)
196
221
  const [svnChanges, setSvnChanges] = React.useState(null) // SVN 变更清单(null=未加载;徽标/菜单判定)
197
- const [svnDiff, setSvnDiff] = React.useState(null) // 差异视图 { path, base, working, reason, message, leftLabel, rightLabel, scheme }(null=关闭)
222
+ const [svnDiff, setSvnDiff] = React.useState(null) // 差异视图 { path, base, working, reason, message, leftLabel, rightLabel, scheme, logTarget?, binary?, encodingHint? }(null=关闭;logTarget 存在 = 来自日志弹窗,差异条可返回日志)
223
+ const [patchDialog, setPatchDialog] = React.useState(null) // W2-1 补丁对话框 { path, text, truncated, binary }(null = 关闭)
224
+ const [sumDialog, setSumDialog] = React.useState(null) // W2-2 目录对比对话框 { path }(null = 关闭)
198
225
  const [svnLog, setSvnLog] = React.useState(null) // 日志弹窗 { target, limit, soc?, range? }(null=关闭;P1-4/P1-6 增选项维度)
199
226
  const [svnLogData, setSvnLogData] = React.useState(null) // 日志条目(null=未加载)
200
227
  const [svnLogBusy, setSvnLogBusy] = React.useState(false) // 日志在途
@@ -262,6 +289,82 @@ export function EditorView(props) {
262
289
  const batchBusyRef = React.useRef(false) // 批量 Keep All/Undo All 防重入
263
290
  const menuHandlersRef = React.useRef(null) // 右键菜单动作的最新闭包(Monaco addAction 空依赖回调读取)
264
291
  const svnMenuDisposersRef = React.useRef([]) // Monaco 右键 SVN 组当前 disposables(同步时先注销旧组)
292
+ // 调试(DAP):store 订阅 tick 驱动重渲 + gutter/F9 切换闭包(ensureEditor 空依赖读取)
293
+ const [dapTick, setDapTick] = React.useState(0)
294
+ React.useEffect(() => dapStore.subscribe(() => setDapTick((t) => t + 1)), [])
295
+ const dapSnap = dapStore.getSnapshot()
296
+ const [dapBarPos, setDapBarPos] = React.useState(() => loadToolbarPosition(scope))
297
+ const [dapBarDragging, setDapBarDragging] = React.useState(false)
298
+ const dapBarRef = React.useRef(null)
299
+ const dapDragRef = React.useRef(null)
300
+ React.useEffect(() => {
301
+ setDapBarPos(loadToolbarPosition(scope))
302
+ }, [scope])
303
+ React.useEffect(() => {
304
+ const onMove = (event) => {
305
+ const drag = dapDragRef.current
306
+ const bar = dapBarRef.current
307
+ const area = bar?.parentElement
308
+ if (!drag || !bar || !area) return
309
+ const areaRect = area.getBoundingClientRect()
310
+ const barRect = bar.getBoundingClientRect()
311
+ const next = clampToolbarPosition({
312
+ left: event.clientX - areaRect.left - drag.offsetX,
313
+ top: event.clientY - areaRect.top - drag.offsetY,
314
+ }, {
315
+ width: areaRect.width,
316
+ height: areaRect.height,
317
+ barWidth: barRect.width,
318
+ barHeight: barRect.height,
319
+ })
320
+ setDapBarPos(next)
321
+ }
322
+ const onUp = () => {
323
+ if (!dapDragRef.current) return
324
+ dapDragRef.current = null
325
+ setDapBarDragging(false)
326
+ const latest = dapBarPosRef.current
327
+ if (latest) saveToolbarPosition(scope, latest)
328
+ }
329
+ document.addEventListener('pointermove', onMove)
330
+ document.addEventListener('pointerup', onUp)
331
+ return () => {
332
+ document.removeEventListener('pointermove', onMove)
333
+ document.removeEventListener('pointerup', onUp)
334
+ }
335
+ }, [scope])
336
+ const dapBarPosRef = React.useRef(dapBarPos)
337
+ dapBarPosRef.current = dapBarPos
338
+ const startDapBarDrag = (event) => {
339
+ if (event.button !== 0 || !dapBarRef.current) return
340
+ const rect = dapBarRef.current.getBoundingClientRect()
341
+ event.preventDefault()
342
+ event.stopPropagation()
343
+ dapDragRef.current = { offsetX: event.clientX - rect.left, offsetY: event.clientY - rect.top }
344
+ setDapBarDragging(true)
345
+ event.currentTarget.setPointerCapture?.(event.pointerId)
346
+ }
347
+ // 进程选择器选中项(多候选附加时;候选集变化回落首个)
348
+ const [dapPickPid, setDapPickPid] = React.useState(0)
349
+ React.useEffect(() => { setDapPickPid(dapSnap.candidates?.[0]?.pid ?? 0) }, [dapSnap.candidates])
350
+ // 断点右键菜单(glyph 区右键)与行内断点编辑浮层(Monaco content widget)
351
+ const [dapBpMenu, setDapBpMenu] = React.useState(null)
352
+ const bpWidgetRef = React.useRef(null) // { close, path, line } | null
353
+ const dapScopeRef = React.useRef(scope)
354
+ const dapCwdRef = React.useRef(cwd)
355
+ dapScopeRef.current = scope
356
+ dapCwdRef.current = cwd
357
+ const dapToggleRef = React.useRef(null)
358
+ dapToggleRef.current = (path, line) => {
359
+ if (!cwd || !path || !line) return
360
+ // 严格对齐 CodeBuddy:任意文件都可切换断点(不再限定 .lua 调试目标)
361
+ dapStore.toggleAt(scope, cwd, path, line)
362
+ }
363
+ // 工作区变化同步调试 store(配置清单/启动基准);有 cwd 时预拉 launch.json 配置
364
+ React.useEffect(() => {
365
+ dapStore.setWorkspace(cwd || '')
366
+ if (cwd) void dapStore.ensureConfigs()
367
+ }, [cwd])
265
368
  const svnStatusRef = React.useRef(null) // svnStatus 渲染值镜像(ensureEditor 空依赖闭包读取最新状态)
266
369
  const [snippetPicker, setSnippetPicker] = React.useState(null) // 'configure' | 'insert' | null(代码片段浮层)
267
370
  const doSaveRef = React.useRef(null) // 保存动作的最新闭包(窗口级保存监听读取)
@@ -640,8 +743,9 @@ export function EditorView(props) {
640
743
  setStatus('已加载')
641
744
  } else {
642
745
  const base = res?.error ? String(res.error) : '读取失败'
746
+ dapTrace('read-failed', { path, error: base, resolvedPath: res?.resolvedPath, cwd })
643
747
  // 带出 host 解析后的真实路径:跳转失败时一眼看出是路径解析错还是目标不存在
644
- const message = res?.resolvedPath ? base + ':' + String(res.resolvedPath) : base
748
+ const message = res?.resolvedPath ? base + ':' + relativeOf(String(res.resolvedPath), cwd) : base
645
749
  setLoadError(message)
646
750
  setError(message)
647
751
  setStatus('读取失败')
@@ -1324,6 +1428,20 @@ export function EditorView(props) {
1324
1428
  ['edrv.command.svnAdd', () => runSvnAdd(activeRef.current)],
1325
1429
  ['edrv.command.svnRevertCli', () => runSvnRevertCli(activeRef.current)],
1326
1430
  ['edrv.command.svnLog', () => openSvnLog(activeRef.current || undefined)],
1431
+ ['edrv.command.svnCreatePatch', () => runSvnAction(SVN_ACTION_BY_ID['create-patch'], {
1432
+ sessionId,
1433
+ scope,
1434
+ path: activeRef.current ?? '',
1435
+ notify: (message) => setStatus(message),
1436
+ openPatchDialog: (payload) => setPatchDialog(payload),
1437
+ })],
1438
+ ['edrv.command.svnDiffSum', () => runSvnAction(SVN_ACTION_BY_ID['diff-summarize'], {
1439
+ sessionId,
1440
+ scope,
1441
+ path: dirOfRel(activeRef.current ?? ''),
1442
+ notify: (message) => setStatus(message),
1443
+ openSumDialog: (target) => setSumDialog({ path: target }),
1444
+ })],
1327
1445
  ['edrv.command.svnCleanup', () => runSvnAction(SVN_ACTION_BY_ID.cleanup, {
1328
1446
  sessionId,
1329
1447
  scope,
@@ -1428,6 +1546,8 @@ export function EditorView(props) {
1428
1546
  setupAiInline(m)
1429
1547
  // 代码片段补全 provider 注册(幂等;条目按会话工作区懒加载)
1430
1548
  setupSnippets(m)
1549
+ // launch.json「添加配置」补全 provider 注册(幂等;仅 launch.json configurations 数组内出条目)
1550
+ setupLaunchJson(m)
1431
1551
  }).catch((e) => {
1432
1552
  if (alive) {
1433
1553
  setMonacoErr(String(e?.message ?? e))
@@ -1461,10 +1581,7 @@ export function EditorView(props) {
1461
1581
  monacoRef.current = monaco
1462
1582
 
1463
1583
  /** 从编辑器实例 model URI 解析活动文件路径(工作区相对;无模型返回 null)。 */
1464
- const modelPathOf = (edx) => {
1465
- const uriPath = edx?.getModel?.()?.uri?.path
1466
- return uriPath ? decodeURIComponent(String(uriPath).replace(/^\//, '')) : null
1467
- }
1584
+ const modelPathOf = (edx) => editorModelPathOf(edx)
1468
1585
 
1469
1586
  /**
1470
1587
  * 同步 Monaco 右键菜单 SVN 组:先注销旧组,再按最新 svn 状态注册(空状态 = 全部移除)。
@@ -1503,6 +1620,7 @@ export function EditorView(props) {
1503
1620
  path: def.id === 'update' ? (relPath ?? '') : relPath,
1504
1621
  notify: (message) => setStatus(message),
1505
1622
  openSvnDiff: (p) => runSvnDiffBase(p),
1623
+ openSvnLocalPair: runSvnDiffLocalPair,
1506
1624
  openSvnLog: (p) => openSvnLog(p ?? relPath),
1507
1625
  refreshChanges: () => refreshSvnChanges(sessionId, scope),
1508
1626
  })
@@ -1887,9 +2005,105 @@ export function EditorView(props) {
1887
2005
  if (hideTimerRef.current) { clearTimeout(hideTimerRef.current); hideTimerRef.current = null }
1888
2006
  setHoverAct({ region: hit, top, right })
1889
2007
  })
2008
+ // 调试断点:行首 glyph 圆点区**左键**点击切换(右键走下方 contextmenu 菜单,
2009
+ // 必须区分按键——否则右键会既 toggle 断点又弹菜单)。
2010
+ // ⚠️ 枚举在 monaco.editor 子命名空间(monaco.editor.MouseTargetType,值为 2);
2011
+ // 取顶层 m.MouseTargetType 是 undefined,会让点击永远静默返回(实测踩坑)。
2012
+ ed.onMouseDown((e) => {
2013
+ const target = e?.target
2014
+ if (!e?.event?.leftButton) return
2015
+ const GUTTER = m.editor?.MouseTargetType?.GUTTER_GLYPH_MARGIN ?? 2
2016
+ if (!target || target.type !== GUTTER) return
2017
+ const line = target.position?.lineNumber
2018
+ const path = pathOf(ed)
2019
+ if (line && path) dapToggleRef.current?.(path, line)
2020
+ })
2021
+ // 断点 hover 预览(对齐 VS Code `ensureBreakpointHintDecoration`):
2022
+ // 鼠标移到 glyph 区(type=2)或行号区(type=3)的可下断点行 → 半透明红点 + pointer 光标;
2023
+ // 移开/离开编辑器 → 清除。已有断点的行不显示预览(避免与实心红点重叠)。
2024
+ ed.onMouseMove((e) => {
2025
+ const target = e?.target
2026
+ const path = pathOf(ed)
2027
+ const list = path ? (dapStore.bpMapOf(dapScopeRef.current)[path] ?? []) : []
2028
+ const line = hintLineFor(target?.type, target?.position?.lineNumber, list.map((it) => it.line))
2029
+ setHintLine(ed, line || null)
2030
+ })
2031
+ ed.onMouseLeave(() => setHintLine(ed, null))
2032
+ // 断点右键菜单:glyph 区右键**始终**由本插件接管(严格对齐 CodeBuddy:任意文件都出菜单,
2033
+ // 不再限定 .lua)。命中行改用 Monaco 官方 getTargetAtClientPoint——
2034
+ // 手算 floor((relY+scrollTop)/lineHeight)+1 **未扣编辑器 padding-top**(实测 6px),
2035
+ // 点在某行框下半部会被算成下一行 → 对空行显示"添加"菜单(实测踩坑)。
2036
+ const glyphHitLine = (ev) => {
2037
+ const edi = editorRef.current
2038
+ if (!edi?.getTargetAtClientPoint) return null
2039
+ const rect = edi.getDomNode?.()?.getBoundingClientRect?.()
2040
+ if (!rect) return null
2041
+ const layout = edi.getLayoutInfo?.()
2042
+ if (!layout) return null
2043
+ const relX = ev.clientX - rect.left
2044
+ if (relX < layout.glyphMarginLeft || relX > layout.glyphMarginLeft + layout.glyphMarginWidth) return null
2045
+ const GUTTER = m.editor?.MouseTargetType?.GUTTER_GLYPH_MARGIN ?? 2
2046
+ const target = edi.getTargetAtClientPoint(ev.clientX, ev.clientY)
2047
+ if (!target || target.type !== GUTTER) return null
2048
+ return target.position?.lineNumber ?? null
2049
+ }
2050
+ const onBpCtxCapture = (ev) => {
2051
+ const line = glyphHitLine(ev)
2052
+ if (!line) return
2053
+ const path = pathOf(editorRef.current)
2054
+ if (!path) return
2055
+ const entry = (dapStore.bpMapOf(dapScopeRef.current)[path] ?? []).find((it) => it.line === line)
2056
+ ev.preventDefault()
2057
+ ev.stopPropagation()
2058
+ setDapBpMenu({
2059
+ x: ev.clientX,
2060
+ y: ev.clientY,
2061
+ path,
2062
+ line,
2063
+ hasBp: !!entry,
2064
+ enabled: entry ? entry.enabled !== false : true,
2065
+ isLogpoint: !!(entry && entry.logMessage),
2066
+ })
2067
+ }
2068
+ node.addEventListener('contextmenu', onBpCtxCapture, true)
1890
2069
  editorRef.current = ed
1891
2070
  }, [])
1892
2071
 
2072
+ // 调试断点装饰:断点表/活动文件变化时全量替换行首圆点(独立 collection,与差异/下划线互不干扰);
2073
+ // 适配器真实回执 verified:false 的行渲染空心红圈(对齐 VS Code 未验证语义)
2074
+ React.useEffect(() => {
2075
+ const ed = editorRef.current
2076
+ if (!ed || !active) return
2077
+ const map = dapStore.bpMapOf(scope)
2078
+ const unverified = bpLinesOf(map, active).filter((line) => dapStore.ackOf(active, line)?.verified === false)
2079
+ applyBpDecorations(ed, bpLinesOf(map, active), bpDisabledLinesOf(map, active), unverified)
2080
+ }, [scope, active, contentPath, dapSnap.bpVersion, dapSnap.bpAckVersion, monaco])
2081
+
2082
+ // 行内断点编辑浮层:切换文件 / 编辑器重建时关闭(浮层锚定旧 model 的行号,留着会错位)
2083
+ React.useEffect(() => {
2084
+ closeBpWidget()
2085
+ }, [active, contentPath])
2086
+
2087
+ // 编辑浮层的目标断点被删除(如 agent 清理埋点/用户在面板删除)→ 浮层自动关闭
2088
+ React.useEffect(() => {
2089
+ const held = bpWidgetRef.current
2090
+ if (!held) return
2091
+ const entry = (dapStore.bpMapOf(scope)[held.path] ?? []).some((it) => it.line === held.line)
2092
+ if (!entry) closeBpWidget()
2093
+ }, [scope, dapSnap.bpVersion])
2094
+
2095
+ // 编辑器卸载时移除浮层(content widget 不随 React 树回收)
2096
+ React.useEffect(() => () => closeBpWidget(), [])
2097
+
2098
+ // 调试停帧行:paused 且首帧属于当前文件时高亮该行;继续/停止/切走自动清除
2099
+ React.useEffect(() => {
2100
+ const ed = editorRef.current
2101
+ if (!ed) return
2102
+ const top = dapSnap.topFrame
2103
+ const hit = top && top.file && sameFile(top.file, active)
2104
+ setPausedLine(ed, hit ? top.line : null)
2105
+ }, [dapSnap.phase, dapSnap.topFrame, active, contentPath, sameFile])
2106
+
1893
2107
  /**
1894
2108
  * 跳转落地后给目标区域挂临时高亮(默认 1.2s 后自动清除)。
1895
2109
  *
@@ -1921,6 +2135,14 @@ export function EditorView(props) {
1921
2135
 
1922
2136
  // 打开文件后的跳转(导航历史恢复/差异聚焦/搜索与 LSP 行列跳转,目标内容就绪后执行一次)
1923
2137
  //
2138
+ // ⚠️ 依赖必须含 monaco:contentReady 在**文本到达**时即为真,可能早于 Monaco 就绪;
2139
+ // 那一帧 editorRef.current 仍是 null → 本 effect 提前返回并保留 pendingFocus,随后
2140
+ // Monaco 载入、ensureEditor 建好编辑器,若 effect 不因 monaco 变化重跑,pendingFocus
2141
+ // 就永久滞留 —— 观感是「首次跳转只打开文件不定位,手动再触发一次才落到目标行」
2142
+ // (实测复现:Monaco 未就绪时派发 line=500,首次停在 1:1,二次触发才到 500)。
2143
+ // ⚠️ 依赖也必须含 mdPreviewing:预览态切换会替换 Monaco host 并**重建**编辑器实例,
2144
+ // 而 active/content 都没变;缺此依赖同样不重跑(同下方 model 同步 effect)。
2145
+ //
1924
2146
  // ⚠️ 门控必须用 contentReady(contentPath === active),不能用 `content === null`:
1925
2147
  // 跨文件跳转时切页签的那一帧 content 仍是**上一个文件**的内容(非 null),据此判定
1926
2148
  // 「已就绪」会提前定位;随后真实内容到达触发 model 重设/内容写入,Monaco 会重置光标,
@@ -1962,7 +2184,7 @@ export function EditorView(props) {
1962
2184
  ed.revealLineInCenter(Math.max(1, target.start ?? 1))
1963
2185
  ed.setPosition({ lineNumber: Math.max(1, target.start ?? 1), column: 1 })
1964
2186
  ed.focus()
1965
- }, [active, content, contentReady, pendingRegions, focusRequest])
2187
+ }, [active, content, contentReady, pendingRegions, focusRequest, monaco, mdPreviewing])
1966
2188
 
1967
2189
  const jumpTo = (region) => {
1968
2190
  if (editorRef.current) {
@@ -2379,6 +2601,8 @@ export function EditorView(props) {
2379
2601
  leftLabel: 'BASE',
2380
2602
  rightLabel: '工作区',
2381
2603
  scheme: 'edrv-svn-base',
2604
+ binary: outcome.binary === true,
2605
+ encodingHint: outcome.encodingHint === true,
2382
2606
  })
2383
2607
  setStatus(outcome.message)
2384
2608
  })
@@ -2390,8 +2614,9 @@ export function EditorView(props) {
2390
2614
  * @author ddj 2026年09月16号
2391
2615
  * @param relPath 目标工作区相对路径
2392
2616
  * @param revision 目标版本
2617
+ * @param logTarget 来自日志弹窗时的日志目标(不传 = 非日志入口)
2393
2618
  */
2394
- const runSvnDiffRev = (relPath, revision) => {
2619
+ const runSvnDiffRev = (relPath, revision, logTarget) => {
2395
2620
  if (!relPath) { setStatus('无目标文件'); return }
2396
2621
  setStatus('读取 r' + revision + ' 差异…')
2397
2622
  void svnDiffRev(sessionId, relPath, revision).then((outcome) => {
@@ -2406,6 +2631,9 @@ export function EditorView(props) {
2406
2631
  leftLabel: 'r' + Math.max(1, revision - 1),
2407
2632
  rightLabel: 'r' + revision,
2408
2633
  scheme: 'edrv-svn-rev' + revision,
2634
+ binary: outcome.binary === true,
2635
+ encodingHint: outcome.encodingHint === true,
2636
+ logTarget,
2409
2637
  })
2410
2638
  setStatus('已打开 r' + revision + ' 差异')
2411
2639
  })
@@ -2418,8 +2646,9 @@ export function EditorView(props) {
2418
2646
  * @param relPath 目标工作区相对路径
2419
2647
  * @param revA 左侧版本(选中顺序在前)
2420
2648
  * @param revB 右侧版本(选中顺序在后)
2649
+ * @param logTarget 来自日志弹窗时的日志目标(不传 = 非日志入口)
2421
2650
  */
2422
- const runSvnDiffPair = (relPath, revA, revB) => {
2651
+ const runSvnDiffPair = (relPath, revA, revB, logTarget) => {
2423
2652
  if (!relPath) { setStatus('仅文件目标支持比较两个修订'); return }
2424
2653
  setStatus('读取 r' + revA + ' ↔ r' + revB + ' 差异…')
2425
2654
  void svnDiffPair(sessionId, relPath, revA, revB).then((outcome) => {
@@ -2436,6 +2665,9 @@ export function EditorView(props) {
2436
2665
  leftLabel: 'r' + revA,
2437
2666
  rightLabel: 'r' + revB,
2438
2667
  scheme: 'edrv-svn-pair-' + revA + '-' + revB,
2668
+ binary: outcome.binary === true,
2669
+ encodingHint: outcome.encodingHint === true,
2670
+ logTarget,
2439
2671
  })
2440
2672
  setStatus('已打开 r' + revA + ' ↔ r' + revB + ' 差异')
2441
2673
  })
@@ -2447,8 +2679,9 @@ export function EditorView(props) {
2447
2679
  * @author ddj 2026年09月17号
2448
2680
  * @param relPath 目标工作区相对路径
2449
2681
  * @param revision 左侧版本
2682
+ * @param logTarget 来自日志弹窗时的日志目标(不传 = 非日志入口)
2450
2683
  */
2451
- const runSvnDiffWorking = (relPath, revision) => {
2684
+ const runSvnDiffWorking = (relPath, revision, logTarget) => {
2452
2685
  if (!relPath) { setStatus('仅文件目标支持与工作副本比较'); return }
2453
2686
  setStatus('读取 r' + revision + ' ↔ 工作副本 差异…')
2454
2687
  void svnDiffWorking(sessionId, relPath, revision).then((outcome) => {
@@ -2462,11 +2695,42 @@ export function EditorView(props) {
2462
2695
  leftLabel: 'r' + revision,
2463
2696
  rightLabel: '工作区',
2464
2697
  scheme: 'edrv-svn-work-' + revision,
2698
+ binary: outcome.binary === true,
2699
+ encodingHint: outcome.encodingHint === true,
2700
+ logTarget,
2465
2701
  })
2466
2702
  setStatus('已打开 r' + revision + ' ↔ 工作副本 差异')
2467
2703
  })
2468
2704
  }
2469
2705
 
2706
+ /**
2707
+ * 打开双侧本地文件差异(W2-4 冲突副本 `.rN` ↔ `.mine`,只读并排;resolve 由用户自行执行)。
2708
+ * @author ddj 2026年09月20号
2709
+ * @param left 左侧工作区相对路径
2710
+ * @param right 右侧工作区相对路径
2711
+ * @param leftLabel 左侧标签(如 `.r293524`)
2712
+ * @param rightLabel 右侧标签(如 `.mine`)
2713
+ * @param title 标题(冲突文件路径)
2714
+ */
2715
+ const runSvnDiffLocalPair = (left, right, leftLabel, rightLabel, title) => {
2716
+ setStatus('读取冲突副本差异…')
2717
+ void svnDiffLocalPair(sessionId, left, right).then((outcome) => {
2718
+ if (!outcome.ok) { setStatus(outcome.message); return }
2719
+ setSvnDiff({
2720
+ path: title,
2721
+ base: outcome.leftText ?? null,
2722
+ working: outcome.rightText ?? '',
2723
+ message: '解决后执行 svn resolve --accept working(本插件不自动执行)',
2724
+ leftLabel,
2725
+ rightLabel,
2726
+ scheme: 'edrv-svn-local',
2727
+ binary: outcome.binary === true,
2728
+ encodingHint: outcome.encodingHint === true,
2729
+ })
2730
+ setStatus('已打开冲突副本对比')
2731
+ })
2732
+ }
2733
+
2470
2734
  /**
2471
2735
  * 打开日志弹窗(P3):加载该目标的提交历史。
2472
2736
  * @author ddj 2026年09月16号
@@ -2482,6 +2746,17 @@ export function EditorView(props) {
2482
2746
  ensureSvnLog(sessionId, scope, target, SVN_LOG_PAGE)
2483
2747
  }
2484
2748
 
2749
+ /**
2750
+ * 从日志弹窗打开的差异视图返回日志:按打开时记录的日志目标重开弹窗。
2751
+ * 日志数据走 scope 缓存(openSvnLog → ensureSvnLog 命中即不重查);选中项不复原。
2752
+ * @author ddj 2026年09月18号
2753
+ */
2754
+ const backToSvnLog = () => {
2755
+ const target = svnDiff?.logTarget ?? ''
2756
+ setSvnDiff(null)
2757
+ openSvnLog(target)
2758
+ }
2759
+
2485
2760
  /** 加入版本控制(活动文件;未纳入版本控制的文件才有意义,host 侧会再校验)。 */
2486
2761
  const runSvnAdd = (relPath) => {
2487
2762
  if (!relPath) { setStatus('无活动文件'); return }
@@ -2584,6 +2859,7 @@ export function EditorView(props) {
2584
2859
  path,
2585
2860
  notify: (message) => setStatus(message),
2586
2861
  openSvnDiff: (p) => runSvnDiffBase(p),
2862
+ openSvnLocalPair: runSvnDiffLocalPair,
2587
2863
  openSvnLog: (p) => openSvnLog(p ?? path),
2588
2864
  refreshChanges: () => refreshSvnChanges(sessionId, scope),
2589
2865
  })
@@ -2681,6 +2957,8 @@ export function EditorView(props) {
2681
2957
  */
2682
2958
  const openFileAt = (path, line, column, endLine, endColumn) => {
2683
2959
  if (!path) return
2960
+ const tracePath = tabPathOf(path, cwd)
2961
+ dapTrace('open-file-at', { input: path, normalized: tracePath, line, cwd })
2684
2962
  recordNav()
2685
2963
  // G9:同 openFile,先归一再进页签,保证待跳转路径与 active 同形态
2686
2964
  const normalized = addTabNorm(path, true)
@@ -2689,6 +2967,51 @@ export function EditorView(props) {
2689
2967
  setFocusRequest((value) => value + 1)
2690
2968
  }
2691
2969
 
2970
+ // 停帧跳转/调试命令监听(openFileAt 每渲染重建,经 ref 让空依赖监听读最新闭包)
2971
+ const dapOpenRef = React.useRef(null)
2972
+ dapOpenRef.current = openFileAt
2973
+ React.useEffect(() => {
2974
+ const onStopped = (e) => {
2975
+ const detail = e?.detail
2976
+ dapTrace('debug-stopped', detail)
2977
+ if (!detail?.file) {
2978
+ // 适配器只给了 chunkname 且工作区未定位到源文件:明确告知,不留静默无反应
2979
+ setStatus('已暂停:未定位到源文件')
2980
+ return
2981
+ }
2982
+ dapOpenRef.current?.(detail.file, detail.line, 1)
2983
+ }
2984
+ const onToggle = () => {
2985
+ const ed = editorRef.current
2986
+ const path = modelPathOf(ed)
2987
+ const line = ed?.getPosition?.()?.lineNumber
2988
+ if (path && line) dapToggleRef.current?.(path, line)
2989
+ else setStatus('无活动文件,无法切换断点')
2990
+ }
2991
+ const onToggleEvt = () => onToggle()
2992
+ const onStart = () => dapStore.startOrContinue()
2993
+ const onStop = () => { void dapStore.stop() }
2994
+ const onStepOver = () => { void dapStore.action('next') }
2995
+ const onStepIn = () => { void dapStore.action('stepIn') }
2996
+ const onStepOut = () => { void dapStore.action('stepOut') }
2997
+ window.addEventListener('edrv:debug-stopped', onStopped)
2998
+ window.addEventListener('edrv.command.debugToggleBreakpoint', onToggleEvt)
2999
+ window.addEventListener('edrv.command.debugStartContinue', onStart)
3000
+ window.addEventListener('edrv.command.debugStepOver', onStepOver)
3001
+ window.addEventListener('edrv.command.debugStepInto', onStepIn)
3002
+ window.addEventListener('edrv.command.debugStepOut', onStepOut)
3003
+ window.addEventListener('edrv.command.debugStop', onStop)
3004
+ return () => {
3005
+ window.removeEventListener('edrv:debug-stopped', onStopped)
3006
+ window.removeEventListener('edrv.command.debugToggleBreakpoint', onToggleEvt)
3007
+ window.removeEventListener('edrv.command.debugStartContinue', onStart)
3008
+ window.removeEventListener('edrv.command.debugStepOver', onStepOver)
3009
+ window.removeEventListener('edrv.command.debugStepInto', onStepIn)
3010
+ window.removeEventListener('edrv.command.debugStepOut', onStepOut)
3011
+ window.removeEventListener('edrv.command.debugStop', onStop)
3012
+ }
3013
+ }, [])
3014
+
2692
3015
  const openPath = () => {
2693
3016
  const p = (pathDraft || '').trim()
2694
3017
  if (!p) return
@@ -2900,6 +3223,7 @@ export function EditorView(props) {
2900
3223
  message: svnDiff.message,
2901
3224
  leftLabel: svnDiff.leftLabel,
2902
3225
  rightLabel: svnDiff.rightLabel,
3226
+ onBack: svnDiff.logTarget !== undefined ? backToSvnLog : undefined,
2903
3227
  onClose: () => setSvnDiff(null),
2904
3228
  })
2905
3229
  } else if (!monaco && !monacoErr) {
@@ -3064,14 +3388,125 @@ export function EditorView(props) {
3064
3388
  svnChanges,
3065
3389
  svnChangeMap: svnChangeMapOf(scope),
3066
3390
  openSvnDiff: (p) => runSvnDiffBase(p),
3391
+ openSvnLocalPair: runSvnDiffLocalPair,
3067
3392
  refreshSvnChanges: () => refreshSvnChanges(sessionId, scope),
3068
3393
  openSvnLog: (p) => openSvnLog(p),
3069
3394
  confirm: (message) => (typeof window === 'undefined' ? false : window.confirm(message)),
3070
3395
  notify: (message) => setStatus(message),
3071
3396
  }
3072
3397
 
3398
+ // 调试工具条(VS Code 浮动条紧凑版,图2 形态):配置下拉 + 启动/继续 + 单步组 + 停止 + 相位。
3399
+ // 配置来自工作区 .dsh/launch.json(插件专属,与 VS Code 互不干扰);无配置时下拉提示且按钮禁用。
3400
+ const dapPhaseTextOf = () => ({
3401
+ idle: '', starting: '启动中…', waiting: '等待目标…', running: '运行中', paused: '已暂停', terminated: '已结束',
3402
+ }[dapSnap.phase] || '')
3403
+ const dapActive = dapSnap.phase !== 'idle' && dapSnap.phase !== 'terminated'
3404
+ const dapPaused = dapSnap.phase === 'paused'
3405
+ const dapBtn = (label, title, disabled, onClick) => React.createElement('button', {
3406
+ key: title, className: 'edrv-dapbar-btn', title, disabled: disabled === true, onClick,
3407
+ }, label)
3408
+ const dapBarVisible = dapActive || Boolean(dapSnap.candidates)
3409
+ const dapBarStyle = dapBarPos
3410
+ ? { left: dapBarPos.left, top: dapBarPos.top, right: 'auto' }
3411
+ : undefined
3412
+ const dapBar = dapBarVisible ? React.createElement('div', {
3413
+ ref: dapBarRef,
3414
+ className: 'edrv-dapbar' + (dapBarVisible ? ' on' : '') + (dapBarDragging ? ' dragging' : ''),
3415
+ style: dapBarStyle,
3416
+ },
3417
+ React.createElement('span', {
3418
+ className: 'edrv-dapbar-grip',
3419
+ role: 'button',
3420
+ tabIndex: 0,
3421
+ title: '拖动调试工具条',
3422
+ 'aria-label': '拖动调试工具条',
3423
+ onPointerDown: startDapBarDrag,
3424
+ }, '⠿'),
3425
+ (dapSnap.candidates
3426
+ ? [
3427
+ React.createElement('select', {
3428
+ key: 'pick', className: 'edrv-dapbar-cfg', title: '选择附加目标进程(processName 命中多个候选)',
3429
+ value: String(dapPickPid || dapSnap.candidates[0]?.pid || ''),
3430
+ onChange: (e) => setDapPickPid(Number(e.target.value)),
3431
+ },
3432
+ dapSnap.candidates.map((it) => React.createElement('option', { key: it.pid, value: String(it.pid) },
3433
+ it.pid + ' : ' + it.name + (it.title ? ' — ' + it.title.slice(0, 44) : '')))),
3434
+ dapBtn('✓', '附加到选中进程', false, () => dapStore.confirmPick(dapPickPid || dapSnap.candidates![0]?.pid || 0)),
3435
+ dapBtn('✕', '取消', false, () => dapStore.cancelPick()),
3436
+ ]
3437
+ : React.createElement('select', {
3438
+ className: 'edrv-dapbar-cfg',
3439
+ title: '调试配置(读取工作区 .dsh/launch.json;适配器由扩展清单 contributes.debuggers 提供)',
3440
+ value: dapSnap.selectedConfig ?? '',
3441
+ onChange: (e) => dapStore.selectConfig(e.target.value),
3442
+ },
3443
+ dapSnap.configs.length
3444
+ ? dapSnap.configs.map((c) => {
3445
+ // 旧 host 无 adapters 数据时视为全部可用(保持兼容);有数据则按可用性置灰并给原因
3446
+ const adapter = dapSnap.adapters.find((a) => a.type === c.type)
3447
+ const ready = dapSnap.adapters.length === 0 || adapter?.available === true
3448
+ const suffix = ready ? '' : '(' + (adapter?.reason || '未发现该类型适配器') + ')'
3449
+ return React.createElement('option', { key: c.name, value: c.name, disabled: !ready }, c.name + suffix)
3450
+ })
3451
+ : React.createElement('option', { value: '' }, '无可用调试配置'))),
3452
+ dapBtn(dapPaused ? '▶' : '▶', '启动 / 继续(F5)', !dapPaused && (!dapSnap.configs.some((c) => dapSnap.adapters.length === 0 || dapSnap.adapters.find((a) => a.type === c.type)?.available === true) || dapActive), () => dapStore.startOrContinue()),
3453
+ dapBtn('⏸', '暂停', !dapActive, () => { void dapStore.action('pause') }),
3454
+ dapBtn('⤵', '单步跳过(F10)', !dapPaused, () => { void dapStore.action('next') }),
3455
+ dapBtn('↓', '单步步入(F11)', !dapPaused, () => { void dapStore.action('stepIn') }),
3456
+ dapBtn('↑', '单步步出(Shift+F11)', !dapPaused, () => { void dapStore.action('stepOut') }),
3457
+ dapBtn('⏹', '停止(Shift+F5)', !dapActive, () => { void dapStore.stop() }),
3458
+ (dapPhaseTextOf()
3459
+ ? React.createElement('span', { className: 'edrv-dapbar-phase' + (dapPaused ? ' paused' : '') },
3460
+ dapPhaseTextOf() + (dapSnap.pid ? ' · pid ' + dapSnap.pid : ''))
3461
+ : null),
3462
+ (dapSnap.error
3463
+ ? React.createElement('span', { className: 'edrv-dapbar-err', title: dapSnap.error }, '⚠ ' + dapSnap.error.slice(0, 40))
3464
+ : null)) : null
3465
+
3466
+ /**
3467
+ * 「添加配置」按钮点击:定位 configurations 数组插入点(数组开头行首),
3468
+ * 显式拉起 Monaco 补全下拉选择调试配置片段插入。
3469
+ * @author ddj 2026年09月22号
3470
+ */
3471
+ const addLaunchConfig = async () => {
3472
+ const ed = editorRef.current
3473
+ if (!ed) { setStatus('无活动编辑器'); return }
3474
+ const path = modelPathOf(ed)
3475
+ if (!path || !LAUNCH_JSON_RE.test(String(path).replace(/\\/g, '/'))) {
3476
+ setStatus('请先打开 .dsh/launch.json')
3477
+ return
3478
+ }
3479
+ const model = ed.getModel && ed.getModel()
3480
+ const pos = model ? findArrayPos(model.getValue()) : null
3481
+ if (!pos) { setStatus('未找到 configurations 数组'); return }
3482
+ // 预拉片段并预检可用条数:全不可用时不弹空 suggest(否则按钮点了无反馈)
3483
+ const snippets = await prefetchSnippets().catch(() => [])
3484
+ if (!snippets.some((snip) => snip.available !== false)) {
3485
+ setStatus('无可用适配器的配置模板(适配器入口缺失或未发现该类型适配器)')
3486
+ return
3487
+ }
3488
+ ed.focus()
3489
+ ed.setPosition({ lineNumber: pos.line, column: pos.column })
3490
+ if (typeof ed.revealPositionInCenterIfOutsideViewport === 'function') {
3491
+ ed.revealPositionInCenterIfOutsideViewport({ lineNumber: pos.line, column: pos.column })
3492
+ }
3493
+ ed.trigger('edrv-addcfg', 'editor.action.triggerSuggest', null)
3494
+ setStatus('选择要插入的调试配置')
3495
+ }
3496
+
3497
+ // 「添加配置」按钮(VSCode 同款形态,编辑区右下角):仅插件专属 launch.json 打开且非差异审阅态显示
3498
+ const addCfgBtn = (active && !svnDiff && LAUNCH_JSON_RE.test(String(active).replace(/\\/g, '/')))
3499
+ ? React.createElement('button', {
3500
+ className: 'edrv-addcfg',
3501
+ title: '添加调试配置(插入到 configurations 数组开头)',
3502
+ onClick: addLaunchConfig,
3503
+ }, '添加配置…')
3504
+ : null
3505
+
3073
3506
  // 主编辑列(侧边栏右侧):pathBar + tabRow + 编辑/差异区(底部整条留给 DSH 对话输入栏)
3074
3507
  const editorArea = React.createElement('div', { className: 'edrv-editor-area' },
3508
+ dapBar,
3509
+ addCfgBtn,
3075
3510
  body,
3076
3511
  hoverEl,
3077
3512
  overlay)
@@ -3216,31 +3651,124 @@ export function EditorView(props) {
3216
3651
  setSvnLog({ ...svnLog, limit: SVN_LOG_SHOW_ALL_LIMIT })
3217
3652
  ensureSvnLog(sessionId, scope, svnLog.target || '', SVN_LOG_SHOW_ALL_LIMIT, true, svnLogOptsOf(svnLog))
3218
3653
  },
3219
- onOpenDiff: (relPath, revision) => { setSvnLog(null); runSvnDiffRev(relPath, revision) },
3220
- onComparePair: (revA, revB) => { setSvnLog(null); runSvnDiffPair(svnLog.target || '', revA, revB) },
3221
- onCompareWorking: (revision) => { setSvnLog(null); runSvnDiffWorking(svnLog.target || '', revision) },
3654
+ onOpenDiff: (relPath, revision) => { setSvnLog(null); runSvnDiffRev(relPath, revision, svnLog.target || '') },
3655
+ onComparePair: (revA, revB) => { setSvnLog(null); runSvnDiffPair(svnLog.target || '', revA, revB, svnLog.target || '') },
3656
+ onCompareWorking: (revision) => { setSvnLog(null); runSvnDiffWorking(svnLog.target || '', revision, svnLog.target || '') },
3222
3657
  onOpenFile: (relPath) => { setSvnLog(null); openFile(relPath, false) },
3223
3658
  onClose: () => { setSvnLog(null); setSvnLogError('') },
3224
3659
  })
3225
3660
  : null
3661
+ // W2-1 补丁对话框 / W2-2 目录对比对话框:portal 到 body,与日志弹窗同一挂载方式
3662
+ const patchDialogEl = patchDialog
3663
+ ? React.createElement(SvnPatchDialog, {
3664
+ key: 'edrv-svn-patch',
3665
+ path: patchDialog.path,
3666
+ text: patchDialog.text,
3667
+ truncated: patchDialog.truncated === true,
3668
+ binary: patchDialog.binary === true,
3669
+ onNote: (message) => setStatus(message),
3670
+ onClose: () => setPatchDialog(null),
3671
+ })
3672
+ : null
3673
+ const sumDialogEl = sumDialog
3674
+ ? React.createElement(SvnSumDialog, {
3675
+ key: 'edrv-svn-sum',
3676
+ sessionId,
3677
+ path: sumDialog.path,
3678
+ onNote: (message) => setStatus(message),
3679
+ onClose: () => setSumDialog(null),
3680
+ })
3681
+ : null
3226
3682
  // 诊断日志弹窗:portal 到 body,与命令栏/SVN 日志弹窗同一挂载方式
3227
3683
  const dlogEl = dlogOpen
3228
3684
  ? React.createElement(LogDialog, { key: 'edrv-dlog', sessionId, onClose: () => setDlogOpen(false) })
3229
3685
  : null
3686
+ // 断点右键菜单(逐条对齐 CodeBuddy:4 项添加类 / 删除·编辑·启停 / 记录点名词 / 暂停追加运行到行)
3687
+ const runBpMenuAction = (action) => {
3688
+ const scope = dapScopeRef.current
3689
+ const cwd = dapCwdRef.current
3690
+ const menu = dapBpMenu
3691
+ if (!menu) return
3692
+ const { path, line } = menu
3693
+ if (action === 'add') { dapStore.toggleAt(scope, cwd, path, line); return }
3694
+ if (action === 'delete') { if (menu.hasBp) dapStore.toggleAt(scope, cwd, path, line); return }
3695
+ if (action === 'disable' || action === 'enable') { dapStore.setBpEnabled(scope, cwd, path, line, action === 'enable'); return }
3696
+ if (action === 'edit') { openBpEditor(path, line); return }
3697
+ if (action === 'run-to-line') { dapStore.runTo(cwd, path, line); return }
3698
+ if (action === 'add-triggered') { setStatus('当前 Lua 调试器不支持触发的断点'); return }
3699
+ // 添加条件断点 / 添加记录点:先建点,再开浮层并选中对应模式(0 表达式 / 2 日志消息)
3700
+ dapStore.toggleAt(scope, cwd, path, line)
3701
+ openBpEditor(path, line, action === 'add-logpoint' ? 2 : 0)
3702
+ }
3703
+ const openBpEditor = (path, line, mode) => {
3704
+ const entry = (dapStore.bpMapOf(dapScopeRef.current)[path] ?? []).find((it) => it.line === line)
3705
+ if (!entry) return
3706
+ const ed = editorRef.current
3707
+ if (!ed?.changeViewZones) return
3708
+ // 行内浮层(View Zone,整编辑区宽 + 推开内容,对齐 VS Code breakpointWidget):同一时刻只挂一个
3709
+ closeBpWidget()
3710
+ bpWidgetRef.current = { ...createBpWidget(ed, {
3711
+ line,
3712
+ entry,
3713
+ mode,
3714
+ onSave: (fields) => {
3715
+ dapStore.applyBpFields(dapScopeRef.current, dapCwdRef.current, path, line, fields)
3716
+ setStatus('断点已更新')
3717
+ closeBpWidget()
3718
+ },
3719
+ onClose: () => closeBpWidget(),
3720
+ }), path, line }
3721
+ }
3722
+ const closeBpWidget = () => {
3723
+ const held = bpWidgetRef.current
3724
+ if (!held) return
3725
+ bpWidgetRef.current = null
3726
+ try { held.close() } catch { /* 编辑器可能已销毁 */ }
3727
+ }
3728
+ const dapBpMenuEl = dapBpMenu
3729
+ ? React.createElement(ContextMenu, {
3730
+ x: dapBpMenu.x,
3731
+ y: dapBpMenu.y,
3732
+ onClose: () => setDapBpMenu(null),
3733
+ entries: buildBpMenu({
3734
+ hasBreakpoint: dapBpMenu.hasBp,
3735
+ enabled: dapBpMenu.enabled,
3736
+ isLogpoint: dapBpMenu.isLogpoint,
3737
+ paused: dapSnap.phase === 'paused',
3738
+ canRunTo: true,
3739
+ canTriggered: false,
3740
+ }).map((item) => ({
3741
+ id: 'dap-bp-' + item.id,
3742
+ label: item.label,
3743
+ hint: item.hint,
3744
+ separator: item.separator,
3745
+ onClick: () => runBpMenuAction(item.id),
3746
+ })),
3747
+ })
3748
+ : null
3749
+ const dapBpEditEl = null
3230
3750
  const rootEl = layout === 'side'
3231
3751
  ? React.createElement('div', { ref: viewRootRef, 'data-edrv-view': '1', 'data-edrv-layout': 'side', className: 'edrv-view-side', style: Object.assign({}, baseStyle, { height: '100%' }) },
3232
3752
  editorRow,
3233
3753
  tabMenuEl,
3234
3754
  paletteEl,
3235
3755
  svnLogEl,
3756
+ patchDialogEl,
3757
+ sumDialogEl,
3236
3758
  dlogEl,
3237
- snippetPickerEl)
3759
+ snippetPickerEl,
3760
+ dapBpMenuEl,
3761
+ dapBpEditEl)
3238
3762
  : React.createElement('div', { ref: viewRootRef, 'data-edrv-view': '1', style: Object.assign({}, baseStyle, { height: 'var(--edrv-editor-height, 100%)', maxHeight: 'var(--edrv-editor-height, 100%)' }) },
3239
3763
  editorRow,
3240
3764
  tabMenuEl,
3241
3765
  paletteEl,
3242
3766
  svnLogEl,
3767
+ patchDialogEl,
3768
+ sumDialogEl,
3243
3769
  dlogEl,
3244
- snippetPickerEl)
3770
+ snippetPickerEl,
3771
+ dapBpMenuEl,
3772
+ dapBpEditEl)
3245
3773
  return rootEl
3246
3774
  }