dsh-vscode-mode 0.5.3 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +72 -2
  2. package/lib/client.js +12082 -6420
  3. package/lib/client.js.map +1 -1
  4. package/lib/index.js +2750 -147
  5. package/lib/index.js.map +1 -1
  6. package/package.json +1 -1
  7. package/src/client/copyText.ts +28 -0
  8. package/src/client/dap/BpWidget.ts +262 -0
  9. package/src/client/dap/bpMenu.ts +91 -0
  10. package/src/client/dap/bpRowMenu.ts +53 -0
  11. package/src/client/dap/breakpoints.ts +222 -0
  12. package/src/client/dap/decorate.ts +128 -0
  13. package/src/client/dap/hintLine.ts +29 -0
  14. package/src/client/dap/hover.ts +166 -0
  15. package/src/client/dap/hoverMode.ts +159 -0
  16. package/src/client/dap/hoverTree.ts +722 -0
  17. package/src/client/dap/launchInsert.ts +229 -0
  18. package/src/client/dap/launchSnippetProvider.ts +206 -0
  19. package/src/client/dap/modelPath.ts +23 -0
  20. package/src/client/dap/panelSplit.ts +105 -0
  21. package/src/client/dap/pidPick.ts +24 -0
  22. package/src/client/dap/store.ts +571 -0
  23. package/src/client/dap/toolbarDrag.ts +51 -0
  24. package/src/client/dap/trace.ts +22 -0
  25. package/src/client/dap/variableTree.ts +62 -0
  26. package/src/client/fileClipboard.ts +46 -0
  27. package/src/client/index.ts +12 -3
  28. package/src/client/monaco/lsp/index.ts +15 -0
  29. package/src/client/monaco/lsp/providers.ts +2 -0
  30. package/src/client/monaco/theme.ts +15 -0
  31. package/src/client/openWithDialog.ts +66 -0
  32. package/src/client/searchSeed.ts +36 -0
  33. package/src/client/sidebar/contextMenu.ts +51 -10
  34. package/src/client/sidebar/menuItems.ts +473 -50
  35. package/src/client/sidebar/panels/DebugPanel.ts +613 -0
  36. package/src/client/sidebar/panels/SearchPanel.ts +24 -3
  37. package/src/client/sidebar/panels/SvnPanel.ts +167 -23
  38. package/src/client/sidebar/panels/index.ts +19 -0
  39. package/src/client/sidebar/types.ts +8 -0
  40. package/src/client/styles/editor.css +176 -0
  41. package/src/client/svnActions.ts +18 -1
  42. package/src/client/svnLog.ts +3 -3
  43. package/src/client/svnStatus.ts +140 -2
  44. package/src/client/tabActions.ts +22 -10
  45. package/src/client/ui/EditorView.ts +625 -23
  46. package/src/client/ui/OfficialSideTab.ts +1 -0
  47. package/src/client/ui/PromptDialog.ts +63 -0
  48. package/src/client/ui/SideBySideDiff.tsx +214 -29
  49. package/src/client/ui/SideEditorTab.ts +1 -0
  50. package/src/client/ui/SvnDiffPanel.ts +21 -8
  51. package/src/client/ui/SvnLogDialog.ts +44 -2
  52. package/src/client/ui/SvnPatchDialog.ts +63 -0
  53. package/src/client/ui/SvnSumDialog.ts +77 -0
  54. package/src/client/ui/commandCatalog.ts +63 -0
  55. package/src/client/ui/svnExport.ts +71 -0
  56. package/src/dap/configSnippets.ts +269 -0
  57. package/src/dap/discovery.ts +198 -0
  58. package/src/dap/launchConfig.ts +127 -0
  59. package/src/dap/manager.ts +588 -0
  60. package/src/dap/processList.ts +76 -0
  61. package/src/dap/protocol.ts +80 -0
  62. package/src/dap/provider.ts +49 -0
  63. package/src/dap/rpc.ts +192 -0
  64. package/src/dap/sourcePath.ts +82 -0
  65. package/src/index.ts +7 -3
  66. package/src/lsp/providers.ts +3 -2
  67. package/src/reveal.ts +31 -20
  68. package/src/rpc.ts +209 -5
  69. package/src/search/ripgrep.ts +127 -11
  70. package/src/shared/dap.ts +262 -0
  71. package/src/shared/fsNames.ts +115 -0
  72. package/src/shared/keybindings.ts +9 -0
  73. package/src/shared/rpc.ts +52 -2
  74. package/src/shared/svn.ts +147 -0
  75. package/src/shared/svnActions.ts +20 -0
  76. package/src/svn.ts +320 -11
  77. package/src/workspace.ts +0 -75
@@ -50,7 +50,7 @@ import { bindingsOf, chordOf, matchEvent, useKeybindingsVersion } from '../keybi
50
50
  import { getSidebarMinWidth } from '../sidebarMin.js'
51
51
  import { navHistoryFor } from '../navHistory.js'
52
52
  import { statusOfAdd } from '../addToConversation.js'
53
- import { setSearchSeed } from '../searchSeed.js'
53
+ import { setSearchScope, setSearchSeed } from '../searchSeed.js'
54
54
  import { CACHE_KEY } from '../paths.js'
55
55
  import { runGoToDefinition, runFindReferences, hideReferencesOverlay } from '../monaco/lsp/providers.js'
56
56
  import { bindLspUnderline } from '../monaco/lsp/underline.js'
@@ -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
- evictPlan, insertTab, isTreeRevealable, normalizeTabs, pickActive, relativeOf, tabPathOf, togglePin,
65
+ evictPlan, insertTab, isTreeRevealable, normalizeTabs, pickActive, relativeOf, remapPathOf, 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'
@@ -73,8 +75,22 @@ import { isSvnDiffable } from '../../shared/svn.js'
73
75
  import type { SvnAction } from '../../shared/svn.js'
74
76
  import { createSaveTimer } from '../saveDebounce.js'
75
77
  import { ContextMenu } from './ContextMenu.js'
78
+ import { promptName } from './PromptDialog.js'
79
+ import { openWithDialog } from '../openWithDialog.js'
80
+ import { copyText as copyClipText } from '../copyText.js'
76
81
  import { SvnLogDialog } from './SvnLogDialog.js'
82
+ import { SvnPatchDialog } from './SvnPatchDialog.js'
83
+ import { SvnSumDialog } from './SvnSumDialog.js'
77
84
  import { LogDialog } from './LogDialog.js'
85
+ import { dapStore } from '../dap/store.js'
86
+ import { bpDisabledLinesOf, bpLinesOf } from '../dap/breakpoints.js'
87
+ import { applyBpDecorations, setEditingLine, setHintLine, setPausedLine } from '../dap/decorate.js'
88
+ import { hintLineFor } from '../dap/hintLine.js'
89
+ import { createBpWidget } from '../dap/BpWidget.js'
90
+ import { buildBpMenu } from '../dap/bpMenu.js'
91
+ import { clampToolbarPosition, loadToolbarPosition, saveToolbarPosition } from '../dap/toolbarDrag.js'
92
+ import { editorModelPathOf } from '../dap/modelPath.js'
93
+ import { dapTrace } from '../dap/trace.js'
78
94
 
79
95
  /** 日志弹窗每次加载条数(「加载更多」按此步长递增;上限由 host 的 SVN_LOG_SHOW_ALL_CAP 约束)。 */
80
96
  const SVN_LOG_PAGE = 100
@@ -90,6 +106,18 @@ function svnLogOptsOf(svnLog) {
90
106
  return { stopOnCopy: svnLog.soc === true, showMerged: svnLog.merged === true, range: svnLog.range ?? null }
91
107
  }
92
108
 
109
+ /**
110
+ * 工作区相对路径 → 所在目录相对路径(W2-2 目录对比的默认目标;无目录 = 工作副本根)。
111
+ * @author ddj 2026年09月20号
112
+ * @param relPath 工作区相对路径(可空)
113
+ * @returns 目录相对路径('' = 工作副本根)
114
+ */
115
+ function dirOfRel(relPath) {
116
+ const value = String(relPath ?? '').replace(/\\/g, '/')
117
+ const cut = value.lastIndexOf('/')
118
+ return cut > 0 ? value.slice(0, cut) : ''
119
+ }
120
+
93
121
  /** 跳转目标高亮的保留时长(LSP/搜索跳转落地后给用户的位置提示,到期自动清除)。 */
94
122
  const NAV_FLASH_MS = 1200
95
123
 
@@ -194,7 +222,9 @@ export function EditorView(props) {
194
222
  const [tabMenu, setTabMenu] = React.useState(null) // Tab 右键菜单 { x,y,path }(编辑区右键走 Monaco 原生菜单,无此浮层)
195
223
  const [svnStatus, setSvnStatus] = React.useState(null) // SVN 能力状态(SvnStatusPayload;null=未加载,SVN 入口隐藏)
196
224
  const [svnChanges, setSvnChanges] = React.useState(null) // SVN 变更清单(null=未加载;徽标/菜单判定)
197
- const [svnDiff, setSvnDiff] = React.useState(null) // 差异视图 { path, base, working, reason, message, leftLabel, rightLabel, scheme }(null=关闭)
225
+ const [svnDiff, setSvnDiff] = React.useState(null) // 差异视图 { path, base, working, reason, message, leftLabel, rightLabel, scheme, logTarget?, binary?, encodingHint? }(null=关闭;logTarget 存在 = 来自日志弹窗,差异条可返回日志)
226
+ const [patchDialog, setPatchDialog] = React.useState(null) // W2-1 补丁对话框 { path, text, truncated, binary }(null = 关闭)
227
+ const [sumDialog, setSumDialog] = React.useState(null) // W2-2 目录对比对话框 { path }(null = 关闭)
198
228
  const [svnLog, setSvnLog] = React.useState(null) // 日志弹窗 { target, limit, soc?, range? }(null=关闭;P1-4/P1-6 增选项维度)
199
229
  const [svnLogData, setSvnLogData] = React.useState(null) // 日志条目(null=未加载)
200
230
  const [svnLogBusy, setSvnLogBusy] = React.useState(false) // 日志在途
@@ -262,6 +292,82 @@ export function EditorView(props) {
262
292
  const batchBusyRef = React.useRef(false) // 批量 Keep All/Undo All 防重入
263
293
  const menuHandlersRef = React.useRef(null) // 右键菜单动作的最新闭包(Monaco addAction 空依赖回调读取)
264
294
  const svnMenuDisposersRef = React.useRef([]) // Monaco 右键 SVN 组当前 disposables(同步时先注销旧组)
295
+ // 调试(DAP):store 订阅 tick 驱动重渲 + gutter/F9 切换闭包(ensureEditor 空依赖读取)
296
+ const [dapTick, setDapTick] = React.useState(0)
297
+ React.useEffect(() => dapStore.subscribe(() => setDapTick((t) => t + 1)), [])
298
+ const dapSnap = dapStore.getSnapshot()
299
+ const [dapBarPos, setDapBarPos] = React.useState(() => loadToolbarPosition(scope))
300
+ const [dapBarDragging, setDapBarDragging] = React.useState(false)
301
+ const dapBarRef = React.useRef(null)
302
+ const dapDragRef = React.useRef(null)
303
+ React.useEffect(() => {
304
+ setDapBarPos(loadToolbarPosition(scope))
305
+ }, [scope])
306
+ React.useEffect(() => {
307
+ const onMove = (event) => {
308
+ const drag = dapDragRef.current
309
+ const bar = dapBarRef.current
310
+ const area = bar?.parentElement
311
+ if (!drag || !bar || !area) return
312
+ const areaRect = area.getBoundingClientRect()
313
+ const barRect = bar.getBoundingClientRect()
314
+ const next = clampToolbarPosition({
315
+ left: event.clientX - areaRect.left - drag.offsetX,
316
+ top: event.clientY - areaRect.top - drag.offsetY,
317
+ }, {
318
+ width: areaRect.width,
319
+ height: areaRect.height,
320
+ barWidth: barRect.width,
321
+ barHeight: barRect.height,
322
+ })
323
+ setDapBarPos(next)
324
+ }
325
+ const onUp = () => {
326
+ if (!dapDragRef.current) return
327
+ dapDragRef.current = null
328
+ setDapBarDragging(false)
329
+ const latest = dapBarPosRef.current
330
+ if (latest) saveToolbarPosition(scope, latest)
331
+ }
332
+ document.addEventListener('pointermove', onMove)
333
+ document.addEventListener('pointerup', onUp)
334
+ return () => {
335
+ document.removeEventListener('pointermove', onMove)
336
+ document.removeEventListener('pointerup', onUp)
337
+ }
338
+ }, [scope])
339
+ const dapBarPosRef = React.useRef(dapBarPos)
340
+ dapBarPosRef.current = dapBarPos
341
+ const startDapBarDrag = (event) => {
342
+ if (event.button !== 0 || !dapBarRef.current) return
343
+ const rect = dapBarRef.current.getBoundingClientRect()
344
+ event.preventDefault()
345
+ event.stopPropagation()
346
+ dapDragRef.current = { offsetX: event.clientX - rect.left, offsetY: event.clientY - rect.top }
347
+ setDapBarDragging(true)
348
+ event.currentTarget.setPointerCapture?.(event.pointerId)
349
+ }
350
+ // 进程选择器选中项(多候选附加时;候选集变化回落首个)
351
+ const [dapPickPid, setDapPickPid] = React.useState(0)
352
+ React.useEffect(() => { setDapPickPid(dapSnap.candidates?.[0]?.pid ?? 0) }, [dapSnap.candidates])
353
+ // 断点右键菜单(glyph 区右键)与行内断点编辑浮层(Monaco content widget)
354
+ const [dapBpMenu, setDapBpMenu] = React.useState(null)
355
+ const bpWidgetRef = React.useRef(null) // { close, path, line } | null
356
+ const dapScopeRef = React.useRef(scope)
357
+ const dapCwdRef = React.useRef(cwd)
358
+ dapScopeRef.current = scope
359
+ dapCwdRef.current = cwd
360
+ const dapToggleRef = React.useRef(null)
361
+ dapToggleRef.current = (path, line) => {
362
+ if (!cwd || !path || !line) return
363
+ // 严格对齐 CodeBuddy:任意文件都可切换断点(不再限定 .lua 调试目标)
364
+ dapStore.toggleAt(scope, cwd, path, line)
365
+ }
366
+ // 工作区变化同步调试 store(配置清单/启动基准);有 cwd 时预拉 launch.json 配置
367
+ React.useEffect(() => {
368
+ dapStore.setWorkspace(cwd || '')
369
+ if (cwd) void dapStore.ensureConfigs()
370
+ }, [cwd])
265
371
  const svnStatusRef = React.useRef(null) // svnStatus 渲染值镜像(ensureEditor 空依赖闭包读取最新状态)
266
372
  const [snippetPicker, setSnippetPicker] = React.useState(null) // 'configure' | 'insert' | null(代码片段浮层)
267
373
  const doSaveRef = React.useRef(null) // 保存动作的最新闭包(窗口级保存监听读取)
@@ -527,6 +633,59 @@ export function EditorView(props) {
527
633
  setStatus(label + '(' + closing.length + ' 个)')
528
634
  }
529
635
 
636
+ /**
637
+ * 删除落盘后的页签收尾:关闭目标及其子树页签。
638
+ * 不走 closeTab 的「先落盘」:文件已删除,落盘会把删除的文件写回磁盘。
639
+ * @author ddj 2026年09月22号
640
+ * @param path 已删除的工作区相对路径
641
+ */
642
+ const dropDeletedTabs = (path) => {
643
+ const doomed = tabsRef.current.filter((t) => t.path === path || t.path.startsWith(path + '/'))
644
+ if (!doomed.length) return
645
+ for (const tab of doomed) releaseTab(tab.path)
646
+ commitClose(applyClose(tabsRef.current, new Set(doomed.map((t) => t.path)), activeRef.current))
647
+ }
648
+
649
+ /**
650
+ * 重命名落盘后的页签改写:目标及其子树页签路径整体改写(含脏标与活动页签)。
651
+ * @author ddj 2026年09月22号
652
+ * @param from 原路径
653
+ * @param to 新路径
654
+ */
655
+ const remapRenamedTabs = (from, to) => {
656
+ setTabs((prev) => prev.map((tab) => ({ ...tab, path: remapPathOf(tab.path, from, to) })))
657
+ setDirtyMap((prev) => {
658
+ const next = {}
659
+ for (const key of Object.keys(prev)) next[remapPathOf(key, from, to)] = prev[key]
660
+ return next
661
+ })
662
+ setActive((prev) => (prev ? remapPathOf(prev, from, to) : prev))
663
+ }
664
+
665
+ // 文件树右键的重命名/删除 → 页签同步(动作在 sidebar/menuItems.ts,经窗口事件桥接;
666
+ // 经 ref 读最新闭包,避免空依赖 effect 捕获陈旧状态)
667
+ const tabFsSyncRef = React.useRef({ rename: () => {}, drop: () => {} })
668
+ tabFsSyncRef.current = { rename: remapRenamedTabs, drop: dropDeletedTabs }
669
+ React.useEffect(() => {
670
+ const onRenamed = (event) => {
671
+ const from = event?.detail?.from
672
+ const to = event?.detail?.to
673
+ if (typeof from !== 'string' || !from || typeof to !== 'string' || !to || from === to) return
674
+ tabFsSyncRef.current.rename(from, to)
675
+ }
676
+ const onDeleted = (event) => {
677
+ const path = event?.detail?.path
678
+ if (typeof path !== 'string' || !path) return
679
+ tabFsSyncRef.current.drop(path)
680
+ }
681
+ window.addEventListener('edrv:path-renamed', onRenamed)
682
+ window.addEventListener('edrv:path-deleted', onDeleted)
683
+ return () => {
684
+ window.removeEventListener('edrv:path-renamed', onRenamed)
685
+ window.removeEventListener('edrv:path-deleted', onDeleted)
686
+ }
687
+ }, [])
688
+
530
689
  /**
531
690
  * 在已打开页签间循环切换(文件分页归编辑器自带页签栏)。
532
691
  * @author ddj 2026年09月10号
@@ -640,8 +799,9 @@ export function EditorView(props) {
640
799
  setStatus('已加载')
641
800
  } else {
642
801
  const base = res?.error ? String(res.error) : '读取失败'
802
+ dapTrace('read-failed', { path, error: base, resolvedPath: res?.resolvedPath, cwd })
643
803
  // 带出 host 解析后的真实路径:跳转失败时一眼看出是路径解析错还是目标不存在
644
- const message = res?.resolvedPath ? base + ':' + String(res.resolvedPath) : base
804
+ const message = res?.resolvedPath ? base + ':' + relativeOf(String(res.resolvedPath), cwd) : base
645
805
  setLoadError(message)
646
806
  setError(message)
647
807
  setStatus('读取失败')
@@ -1324,6 +1484,20 @@ export function EditorView(props) {
1324
1484
  ['edrv.command.svnAdd', () => runSvnAdd(activeRef.current)],
1325
1485
  ['edrv.command.svnRevertCli', () => runSvnRevertCli(activeRef.current)],
1326
1486
  ['edrv.command.svnLog', () => openSvnLog(activeRef.current || undefined)],
1487
+ ['edrv.command.svnCreatePatch', () => runSvnAction(SVN_ACTION_BY_ID['create-patch'], {
1488
+ sessionId,
1489
+ scope,
1490
+ path: activeRef.current ?? '',
1491
+ notify: (message) => setStatus(message),
1492
+ openPatchDialog: (payload) => setPatchDialog(payload),
1493
+ })],
1494
+ ['edrv.command.svnDiffSum', () => runSvnAction(SVN_ACTION_BY_ID['diff-summarize'], {
1495
+ sessionId,
1496
+ scope,
1497
+ path: dirOfRel(activeRef.current ?? ''),
1498
+ notify: (message) => setStatus(message),
1499
+ openSumDialog: (target) => setSumDialog({ path: target }),
1500
+ })],
1327
1501
  ['edrv.command.svnCleanup', () => runSvnAction(SVN_ACTION_BY_ID.cleanup, {
1328
1502
  sessionId,
1329
1503
  scope,
@@ -1335,6 +1509,11 @@ export function EditorView(props) {
1335
1509
  ['edrv.command.svnTortoiseDiff', () => runSvnTortoise('diff', activeRef.current)],
1336
1510
  ['edrv.command.svnTortoiseBlame', () => runSvnTortoise('blame', activeRef.current)],
1337
1511
  ['edrv.command.svnTortoiseRevert', () => runSvnTortoise('revert', activeRef.current)],
1512
+ // 转到行:转发 Monaco 原生 action(widget 本体/占位/跳转全原生,插件只补键位与命令栏入口)
1513
+ ['edrv.command.goToLine', () => {
1514
+ const ed = editorRef.current
1515
+ if (ed?.getModel?.()) ed.trigger('edrv-goto', 'editor.action.gotoLine', null)
1516
+ }],
1338
1517
  // 诊断日志弹窗(同片段选择器:执行一条命令即关命令栏,避免残浮层遮挡)
1339
1518
  ['edrv.command.showLogs', () => { closeCommandPalette(); setDlogOpen(true) }],
1340
1519
  ]
@@ -1428,6 +1607,8 @@ export function EditorView(props) {
1428
1607
  setupAiInline(m)
1429
1608
  // 代码片段补全 provider 注册(幂等;条目按会话工作区懒加载)
1430
1609
  setupSnippets(m)
1610
+ // launch.json「添加配置」补全 provider 注册(幂等;仅 launch.json configurations 数组内出条目)
1611
+ setupLaunchJson(m)
1431
1612
  }).catch((e) => {
1432
1613
  if (alive) {
1433
1614
  setMonacoErr(String(e?.message ?? e))
@@ -1461,10 +1642,7 @@ export function EditorView(props) {
1461
1642
  monacoRef.current = monaco
1462
1643
 
1463
1644
  /** 从编辑器实例 model URI 解析活动文件路径(工作区相对;无模型返回 null)。 */
1464
- const modelPathOf = (edx) => {
1465
- const uriPath = edx?.getModel?.()?.uri?.path
1466
- return uriPath ? decodeURIComponent(String(uriPath).replace(/^\//, '')) : null
1467
- }
1645
+ const modelPathOf = (edx) => editorModelPathOf(edx)
1468
1646
 
1469
1647
  /**
1470
1648
  * 同步 Monaco 右键菜单 SVN 组:先注销旧组,再按最新 svn 状态注册(空状态 = 全部移除)。
@@ -1503,6 +1681,7 @@ export function EditorView(props) {
1503
1681
  path: def.id === 'update' ? (relPath ?? '') : relPath,
1504
1682
  notify: (message) => setStatus(message),
1505
1683
  openSvnDiff: (p) => runSvnDiffBase(p),
1684
+ openSvnLocalPair: runSvnDiffLocalPair,
1506
1685
  openSvnLog: (p) => openSvnLog(p ?? relPath),
1507
1686
  refreshChanges: () => refreshSvnChanges(sessionId, scope),
1508
1687
  })
@@ -1887,9 +2066,105 @@ export function EditorView(props) {
1887
2066
  if (hideTimerRef.current) { clearTimeout(hideTimerRef.current); hideTimerRef.current = null }
1888
2067
  setHoverAct({ region: hit, top, right })
1889
2068
  })
2069
+ // 调试断点:行首 glyph 圆点区**左键**点击切换(右键走下方 contextmenu 菜单,
2070
+ // 必须区分按键——否则右键会既 toggle 断点又弹菜单)。
2071
+ // ⚠️ 枚举在 monaco.editor 子命名空间(monaco.editor.MouseTargetType,值为 2);
2072
+ // 取顶层 m.MouseTargetType 是 undefined,会让点击永远静默返回(实测踩坑)。
2073
+ ed.onMouseDown((e) => {
2074
+ const target = e?.target
2075
+ if (!e?.event?.leftButton) return
2076
+ const GUTTER = m.editor?.MouseTargetType?.GUTTER_GLYPH_MARGIN ?? 2
2077
+ if (!target || target.type !== GUTTER) return
2078
+ const line = target.position?.lineNumber
2079
+ const path = pathOf(ed)
2080
+ if (line && path) dapToggleRef.current?.(path, line)
2081
+ })
2082
+ // 断点 hover 预览(对齐 VS Code `ensureBreakpointHintDecoration`):
2083
+ // 鼠标移到 glyph 区(type=2)或行号区(type=3)的可下断点行 → 半透明红点 + pointer 光标;
2084
+ // 移开/离开编辑器 → 清除。已有断点的行不显示预览(避免与实心红点重叠)。
2085
+ ed.onMouseMove((e) => {
2086
+ const target = e?.target
2087
+ const path = pathOf(ed)
2088
+ const list = path ? (dapStore.bpMapOf(dapScopeRef.current)[path] ?? []) : []
2089
+ const line = hintLineFor(target?.type, target?.position?.lineNumber, list.map((it) => it.line))
2090
+ setHintLine(ed, line || null)
2091
+ })
2092
+ ed.onMouseLeave(() => setHintLine(ed, null))
2093
+ // 断点右键菜单:glyph 区右键**始终**由本插件接管(严格对齐 CodeBuddy:任意文件都出菜单,
2094
+ // 不再限定 .lua)。命中行改用 Monaco 官方 getTargetAtClientPoint——
2095
+ // 手算 floor((relY+scrollTop)/lineHeight)+1 **未扣编辑器 padding-top**(实测 6px),
2096
+ // 点在某行框下半部会被算成下一行 → 对空行显示"添加"菜单(实测踩坑)。
2097
+ const glyphHitLine = (ev) => {
2098
+ const edi = editorRef.current
2099
+ if (!edi?.getTargetAtClientPoint) return null
2100
+ const rect = edi.getDomNode?.()?.getBoundingClientRect?.()
2101
+ if (!rect) return null
2102
+ const layout = edi.getLayoutInfo?.()
2103
+ if (!layout) return null
2104
+ const relX = ev.clientX - rect.left
2105
+ if (relX < layout.glyphMarginLeft || relX > layout.glyphMarginLeft + layout.glyphMarginWidth) return null
2106
+ const GUTTER = m.editor?.MouseTargetType?.GUTTER_GLYPH_MARGIN ?? 2
2107
+ const target = edi.getTargetAtClientPoint(ev.clientX, ev.clientY)
2108
+ if (!target || target.type !== GUTTER) return null
2109
+ return target.position?.lineNumber ?? null
2110
+ }
2111
+ const onBpCtxCapture = (ev) => {
2112
+ const line = glyphHitLine(ev)
2113
+ if (!line) return
2114
+ const path = pathOf(editorRef.current)
2115
+ if (!path) return
2116
+ const entry = (dapStore.bpMapOf(dapScopeRef.current)[path] ?? []).find((it) => it.line === line)
2117
+ ev.preventDefault()
2118
+ ev.stopPropagation()
2119
+ setDapBpMenu({
2120
+ x: ev.clientX,
2121
+ y: ev.clientY,
2122
+ path,
2123
+ line,
2124
+ hasBp: !!entry,
2125
+ enabled: entry ? entry.enabled !== false : true,
2126
+ isLogpoint: !!(entry && entry.logMessage),
2127
+ })
2128
+ }
2129
+ node.addEventListener('contextmenu', onBpCtxCapture, true)
1890
2130
  editorRef.current = ed
1891
2131
  }, [])
1892
2132
 
2133
+ // 调试断点装饰:断点表/活动文件变化时全量替换行首圆点(独立 collection,与差异/下划线互不干扰);
2134
+ // 适配器真实回执 verified:false 的行渲染空心红圈(对齐 VS Code 未验证语义)
2135
+ React.useEffect(() => {
2136
+ const ed = editorRef.current
2137
+ if (!ed || !active) return
2138
+ const map = dapStore.bpMapOf(scope)
2139
+ const unverified = bpLinesOf(map, active).filter((line) => dapStore.ackOf(active, line)?.verified === false)
2140
+ applyBpDecorations(ed, bpLinesOf(map, active), bpDisabledLinesOf(map, active), unverified)
2141
+ }, [scope, active, contentPath, dapSnap.bpVersion, dapSnap.bpAckVersion, monaco])
2142
+
2143
+ // 行内断点编辑浮层:切换文件 / 编辑器重建时关闭(浮层锚定旧 model 的行号,留着会错位)
2144
+ React.useEffect(() => {
2145
+ closeBpWidget()
2146
+ }, [active, contentPath])
2147
+
2148
+ // 编辑浮层的目标断点被删除(如 agent 清理埋点/用户在面板删除)→ 浮层自动关闭
2149
+ React.useEffect(() => {
2150
+ const held = bpWidgetRef.current
2151
+ if (!held) return
2152
+ const entry = (dapStore.bpMapOf(scope)[held.path] ?? []).some((it) => it.line === held.line)
2153
+ if (!entry) closeBpWidget()
2154
+ }, [scope, dapSnap.bpVersion])
2155
+
2156
+ // 编辑器卸载时移除浮层(content widget 不随 React 树回收)
2157
+ React.useEffect(() => () => closeBpWidget(), [])
2158
+
2159
+ // 调试停帧行:paused 且首帧属于当前文件时高亮该行;继续/停止/切走自动清除
2160
+ React.useEffect(() => {
2161
+ const ed = editorRef.current
2162
+ if (!ed) return
2163
+ const top = dapSnap.topFrame
2164
+ const hit = top && top.file && sameFile(top.file, active)
2165
+ setPausedLine(ed, hit ? top.line : null)
2166
+ }, [dapSnap.phase, dapSnap.topFrame, active, contentPath, sameFile])
2167
+
1893
2168
  /**
1894
2169
  * 跳转落地后给目标区域挂临时高亮(默认 1.2s 后自动清除)。
1895
2170
  *
@@ -1921,6 +2196,14 @@ export function EditorView(props) {
1921
2196
 
1922
2197
  // 打开文件后的跳转(导航历史恢复/差异聚焦/搜索与 LSP 行列跳转,目标内容就绪后执行一次)
1923
2198
  //
2199
+ // ⚠️ 依赖必须含 monaco:contentReady 在**文本到达**时即为真,可能早于 Monaco 就绪;
2200
+ // 那一帧 editorRef.current 仍是 null → 本 effect 提前返回并保留 pendingFocus,随后
2201
+ // Monaco 载入、ensureEditor 建好编辑器,若 effect 不因 monaco 变化重跑,pendingFocus
2202
+ // 就永久滞留 —— 观感是「首次跳转只打开文件不定位,手动再触发一次才落到目标行」
2203
+ // (实测复现:Monaco 未就绪时派发 line=500,首次停在 1:1,二次触发才到 500)。
2204
+ // ⚠️ 依赖也必须含 mdPreviewing:预览态切换会替换 Monaco host 并**重建**编辑器实例,
2205
+ // 而 active/content 都没变;缺此依赖同样不重跑(同下方 model 同步 effect)。
2206
+ //
1924
2207
  // ⚠️ 门控必须用 contentReady(contentPath === active),不能用 `content === null`:
1925
2208
  // 跨文件跳转时切页签的那一帧 content 仍是**上一个文件**的内容(非 null),据此判定
1926
2209
  // 「已就绪」会提前定位;随后真实内容到达触发 model 重设/内容写入,Monaco 会重置光标,
@@ -1962,7 +2245,7 @@ export function EditorView(props) {
1962
2245
  ed.revealLineInCenter(Math.max(1, target.start ?? 1))
1963
2246
  ed.setPosition({ lineNumber: Math.max(1, target.start ?? 1), column: 1 })
1964
2247
  ed.focus()
1965
- }, [active, content, contentReady, pendingRegions, focusRequest])
2248
+ }, [active, content, contentReady, pendingRegions, focusRequest, monaco, mdPreviewing])
1966
2249
 
1967
2250
  const jumpTo = (region) => {
1968
2251
  if (editorRef.current) {
@@ -2379,6 +2662,8 @@ export function EditorView(props) {
2379
2662
  leftLabel: 'BASE',
2380
2663
  rightLabel: '工作区',
2381
2664
  scheme: 'edrv-svn-base',
2665
+ binary: outcome.binary === true,
2666
+ encodingHint: outcome.encodingHint === true,
2382
2667
  })
2383
2668
  setStatus(outcome.message)
2384
2669
  })
@@ -2390,8 +2675,9 @@ export function EditorView(props) {
2390
2675
  * @author ddj 2026年09月16号
2391
2676
  * @param relPath 目标工作区相对路径
2392
2677
  * @param revision 目标版本
2678
+ * @param logTarget 来自日志弹窗时的日志目标(不传 = 非日志入口)
2393
2679
  */
2394
- const runSvnDiffRev = (relPath, revision) => {
2680
+ const runSvnDiffRev = (relPath, revision, logTarget) => {
2395
2681
  if (!relPath) { setStatus('无目标文件'); return }
2396
2682
  setStatus('读取 r' + revision + ' 差异…')
2397
2683
  void svnDiffRev(sessionId, relPath, revision).then((outcome) => {
@@ -2406,6 +2692,9 @@ export function EditorView(props) {
2406
2692
  leftLabel: 'r' + Math.max(1, revision - 1),
2407
2693
  rightLabel: 'r' + revision,
2408
2694
  scheme: 'edrv-svn-rev' + revision,
2695
+ binary: outcome.binary === true,
2696
+ encodingHint: outcome.encodingHint === true,
2697
+ logTarget,
2409
2698
  })
2410
2699
  setStatus('已打开 r' + revision + ' 差异')
2411
2700
  })
@@ -2418,8 +2707,9 @@ export function EditorView(props) {
2418
2707
  * @param relPath 目标工作区相对路径
2419
2708
  * @param revA 左侧版本(选中顺序在前)
2420
2709
  * @param revB 右侧版本(选中顺序在后)
2710
+ * @param logTarget 来自日志弹窗时的日志目标(不传 = 非日志入口)
2421
2711
  */
2422
- const runSvnDiffPair = (relPath, revA, revB) => {
2712
+ const runSvnDiffPair = (relPath, revA, revB, logTarget) => {
2423
2713
  if (!relPath) { setStatus('仅文件目标支持比较两个修订'); return }
2424
2714
  setStatus('读取 r' + revA + ' ↔ r' + revB + ' 差异…')
2425
2715
  void svnDiffPair(sessionId, relPath, revA, revB).then((outcome) => {
@@ -2436,6 +2726,9 @@ export function EditorView(props) {
2436
2726
  leftLabel: 'r' + revA,
2437
2727
  rightLabel: 'r' + revB,
2438
2728
  scheme: 'edrv-svn-pair-' + revA + '-' + revB,
2729
+ binary: outcome.binary === true,
2730
+ encodingHint: outcome.encodingHint === true,
2731
+ logTarget,
2439
2732
  })
2440
2733
  setStatus('已打开 r' + revA + ' ↔ r' + revB + ' 差异')
2441
2734
  })
@@ -2447,8 +2740,9 @@ export function EditorView(props) {
2447
2740
  * @author ddj 2026年09月17号
2448
2741
  * @param relPath 目标工作区相对路径
2449
2742
  * @param revision 左侧版本
2743
+ * @param logTarget 来自日志弹窗时的日志目标(不传 = 非日志入口)
2450
2744
  */
2451
- const runSvnDiffWorking = (relPath, revision) => {
2745
+ const runSvnDiffWorking = (relPath, revision, logTarget) => {
2452
2746
  if (!relPath) { setStatus('仅文件目标支持与工作副本比较'); return }
2453
2747
  setStatus('读取 r' + revision + ' ↔ 工作副本 差异…')
2454
2748
  void svnDiffWorking(sessionId, relPath, revision).then((outcome) => {
@@ -2462,11 +2756,42 @@ export function EditorView(props) {
2462
2756
  leftLabel: 'r' + revision,
2463
2757
  rightLabel: '工作区',
2464
2758
  scheme: 'edrv-svn-work-' + revision,
2759
+ binary: outcome.binary === true,
2760
+ encodingHint: outcome.encodingHint === true,
2761
+ logTarget,
2465
2762
  })
2466
2763
  setStatus('已打开 r' + revision + ' ↔ 工作副本 差异')
2467
2764
  })
2468
2765
  }
2469
2766
 
2767
+ /**
2768
+ * 打开双侧本地文件差异(W2-4 冲突副本 `.rN` ↔ `.mine`,只读并排;resolve 由用户自行执行)。
2769
+ * @author ddj 2026年09月20号
2770
+ * @param left 左侧工作区相对路径
2771
+ * @param right 右侧工作区相对路径
2772
+ * @param leftLabel 左侧标签(如 `.r293524`)
2773
+ * @param rightLabel 右侧标签(如 `.mine`)
2774
+ * @param title 标题(冲突文件路径)
2775
+ */
2776
+ const runSvnDiffLocalPair = (left, right, leftLabel, rightLabel, title) => {
2777
+ setStatus('读取冲突副本差异…')
2778
+ void svnDiffLocalPair(sessionId, left, right).then((outcome) => {
2779
+ if (!outcome.ok) { setStatus(outcome.message); return }
2780
+ setSvnDiff({
2781
+ path: title,
2782
+ base: outcome.leftText ?? null,
2783
+ working: outcome.rightText ?? '',
2784
+ message: '解决后执行 svn resolve --accept working(本插件不自动执行)',
2785
+ leftLabel,
2786
+ rightLabel,
2787
+ scheme: 'edrv-svn-local',
2788
+ binary: outcome.binary === true,
2789
+ encodingHint: outcome.encodingHint === true,
2790
+ })
2791
+ setStatus('已打开冲突副本对比')
2792
+ })
2793
+ }
2794
+
2470
2795
  /**
2471
2796
  * 打开日志弹窗(P3):加载该目标的提交历史。
2472
2797
  * @author ddj 2026年09月16号
@@ -2482,6 +2807,17 @@ export function EditorView(props) {
2482
2807
  ensureSvnLog(sessionId, scope, target, SVN_LOG_PAGE)
2483
2808
  }
2484
2809
 
2810
+ /**
2811
+ * 从日志弹窗打开的差异视图返回日志:按打开时记录的日志目标重开弹窗。
2812
+ * 日志数据走 scope 缓存(openSvnLog → ensureSvnLog 命中即不重查);选中项不复原。
2813
+ * @author ddj 2026年09月18号
2814
+ */
2815
+ const backToSvnLog = () => {
2816
+ const target = svnDiff?.logTarget ?? ''
2817
+ setSvnDiff(null)
2818
+ openSvnLog(target)
2819
+ }
2820
+
2485
2821
  /** 加入版本控制(活动文件;未纳入版本控制的文件才有意义,host 侧会再校验)。 */
2486
2822
  const runSvnAdd = (relPath) => {
2487
2823
  if (!relPath) { setStatus('无活动文件'); return }
@@ -2523,17 +2859,15 @@ export function EditorView(props) {
2523
2859
 
2524
2860
  /**
2525
2861
  * 复制文本到剪贴板(状态栏反馈;浏览器拒绝时提示,不抛异常)。
2526
- * @author ddj 2026年09月11号
2862
+ * 剪贴板写入与降级文案收敛到 client/copyText.ts(文件树菜单同源共用)。
2863
+ * @author ddj 2026年09月11号 / 2026年09月22号
2527
2864
  * @param text 待复制文本
2528
2865
  * @param okText 成功文案
2529
2866
  */
2530
2867
  const copyText = (text, okText) => {
2531
2868
  const value = String(text ?? '')
2532
2869
  if (!value) { setStatus('无可复制内容'); return }
2533
- if (!navigator.clipboard?.writeText) { setStatus('剪贴板不可用'); return }
2534
- navigator.clipboard.writeText(value)
2535
- .then(() => setStatus(okText + ':' + value))
2536
- .catch(() => setStatus('复制失败(浏览器拒绝剪贴板写入)'))
2870
+ void copyClipText(value, okText + ':' + value, setStatus)
2537
2871
  }
2538
2872
 
2539
2873
  /**
@@ -2584,6 +2918,7 @@ export function EditorView(props) {
2584
2918
  path,
2585
2919
  notify: (message) => setStatus(message),
2586
2920
  openSvnDiff: (p) => runSvnDiffBase(p),
2921
+ openSvnLocalPair: runSvnDiffLocalPair,
2587
2922
  openSvnLog: (p) => openSvnLog(p ?? path),
2588
2923
  refreshChanges: () => refreshSvnChanges(sessionId, scope),
2589
2924
  })
@@ -2681,6 +3016,8 @@ export function EditorView(props) {
2681
3016
  */
2682
3017
  const openFileAt = (path, line, column, endLine, endColumn) => {
2683
3018
  if (!path) return
3019
+ const tracePath = tabPathOf(path, cwd)
3020
+ dapTrace('open-file-at', { input: path, normalized: tracePath, line, cwd })
2684
3021
  recordNav()
2685
3022
  // G9:同 openFile,先归一再进页签,保证待跳转路径与 active 同形态
2686
3023
  const normalized = addTabNorm(path, true)
@@ -2689,6 +3026,51 @@ export function EditorView(props) {
2689
3026
  setFocusRequest((value) => value + 1)
2690
3027
  }
2691
3028
 
3029
+ // 停帧跳转/调试命令监听(openFileAt 每渲染重建,经 ref 让空依赖监听读最新闭包)
3030
+ const dapOpenRef = React.useRef(null)
3031
+ dapOpenRef.current = openFileAt
3032
+ React.useEffect(() => {
3033
+ const onStopped = (e) => {
3034
+ const detail = e?.detail
3035
+ dapTrace('debug-stopped', detail)
3036
+ if (!detail?.file) {
3037
+ // 适配器只给了 chunkname 且工作区未定位到源文件:明确告知,不留静默无反应
3038
+ setStatus('已暂停:未定位到源文件')
3039
+ return
3040
+ }
3041
+ dapOpenRef.current?.(detail.file, detail.line, 1)
3042
+ }
3043
+ const onToggle = () => {
3044
+ const ed = editorRef.current
3045
+ const path = modelPathOf(ed)
3046
+ const line = ed?.getPosition?.()?.lineNumber
3047
+ if (path && line) dapToggleRef.current?.(path, line)
3048
+ else setStatus('无活动文件,无法切换断点')
3049
+ }
3050
+ const onToggleEvt = () => onToggle()
3051
+ const onStart = () => dapStore.startOrContinue()
3052
+ const onStop = () => { void dapStore.stop() }
3053
+ const onStepOver = () => { void dapStore.action('next') }
3054
+ const onStepIn = () => { void dapStore.action('stepIn') }
3055
+ const onStepOut = () => { void dapStore.action('stepOut') }
3056
+ window.addEventListener('edrv:debug-stopped', onStopped)
3057
+ window.addEventListener('edrv.command.debugToggleBreakpoint', onToggleEvt)
3058
+ window.addEventListener('edrv.command.debugStartContinue', onStart)
3059
+ window.addEventListener('edrv.command.debugStepOver', onStepOver)
3060
+ window.addEventListener('edrv.command.debugStepInto', onStepIn)
3061
+ window.addEventListener('edrv.command.debugStepOut', onStepOut)
3062
+ window.addEventListener('edrv.command.debugStop', onStop)
3063
+ return () => {
3064
+ window.removeEventListener('edrv:debug-stopped', onStopped)
3065
+ window.removeEventListener('edrv.command.debugToggleBreakpoint', onToggleEvt)
3066
+ window.removeEventListener('edrv.command.debugStartContinue', onStart)
3067
+ window.removeEventListener('edrv.command.debugStepOver', onStepOver)
3068
+ window.removeEventListener('edrv.command.debugStepInto', onStepIn)
3069
+ window.removeEventListener('edrv.command.debugStepOut', onStepOut)
3070
+ window.removeEventListener('edrv.command.debugStop', onStop)
3071
+ }
3072
+ }, [])
3073
+
2692
3074
  const openPath = () => {
2693
3075
  const p = (pathDraft || '').trim()
2694
3076
  if (!p) return
@@ -2900,6 +3282,7 @@ export function EditorView(props) {
2900
3282
  message: svnDiff.message,
2901
3283
  leftLabel: svnDiff.leftLabel,
2902
3284
  rightLabel: svnDiff.rightLabel,
3285
+ onBack: svnDiff.logTarget !== undefined ? backToSvnLog : undefined,
2903
3286
  onClose: () => setSvnDiff(null),
2904
3287
  })
2905
3288
  } else if (!monaco && !monacoErr) {
@@ -3064,14 +3447,140 @@ export function EditorView(props) {
3064
3447
  svnChanges,
3065
3448
  svnChangeMap: svnChangeMapOf(scope),
3066
3449
  openSvnDiff: (p) => runSvnDiffBase(p),
3450
+ openSvnLocalPair: runSvnDiffLocalPair,
3067
3451
  refreshSvnChanges: () => refreshSvnChanges(sessionId, scope),
3068
3452
  openSvnLog: (p) => openSvnLog(p),
3069
3453
  confirm: (message) => (typeof window === 'undefined' ? false : window.confirm(message)),
3070
3454
  notify: (message) => setStatus(message),
3455
+ // 名称输入弹窗(资源管理器右键的新建文件/新建文件夹/重命名)
3456
+ prompt: promptName,
3457
+ // 「打开方式…」(资源管理器右键、文件目标):打开器选择弹窗(注册表由 index 装配传入)
3458
+ openWith: (p) => {
3459
+ const openers = props.fileOpeners
3460
+ if (!openers) return
3461
+ openWithDialog(openers, { sessionId, cwd: cwd ?? undefined }, p, (opener) => {
3462
+ if (opener) setStatus('已用「' + opener.label + '」打开:' + baseNameOf(p))
3463
+ })
3464
+ },
3465
+ // 「在文件夹中查找…」(资源管理器右键、目录目标):目录过滤种子 + 跳搜索面板
3466
+ searchInFolder: (dir) => {
3467
+ setSearchScope(dir)
3468
+ openSearchPanel()
3469
+ },
3470
+ }
3471
+
3472
+ // 调试工具条(VS Code 浮动条紧凑版,图2 形态):配置下拉 + 启动/继续 + 单步组 + 停止 + 相位。
3473
+ // 配置来自工作区 .dsh/launch.json(插件专属,与 VS Code 互不干扰);无配置时下拉提示且按钮禁用。
3474
+ const dapPhaseTextOf = () => ({
3475
+ idle: '', starting: '启动中…', waiting: '等待目标…', running: '运行中', paused: '已暂停', terminated: '已结束',
3476
+ }[dapSnap.phase] || '')
3477
+ const dapActive = dapSnap.phase !== 'idle' && dapSnap.phase !== 'terminated'
3478
+ const dapPaused = dapSnap.phase === 'paused'
3479
+ const dapBtn = (label, title, disabled, onClick) => React.createElement('button', {
3480
+ key: title, className: 'edrv-dapbar-btn', title, disabled: disabled === true, onClick,
3481
+ }, label)
3482
+ const dapBarVisible = dapActive || Boolean(dapSnap.candidates)
3483
+ const dapBarStyle = dapBarPos
3484
+ ? { left: dapBarPos.left, top: dapBarPos.top, right: 'auto' }
3485
+ : undefined
3486
+ const dapBar = dapBarVisible ? React.createElement('div', {
3487
+ ref: dapBarRef,
3488
+ className: 'edrv-dapbar' + (dapBarVisible ? ' on' : '') + (dapBarDragging ? ' dragging' : ''),
3489
+ style: dapBarStyle,
3490
+ },
3491
+ React.createElement('span', {
3492
+ className: 'edrv-dapbar-grip',
3493
+ role: 'button',
3494
+ tabIndex: 0,
3495
+ title: '拖动调试工具条',
3496
+ 'aria-label': '拖动调试工具条',
3497
+ onPointerDown: startDapBarDrag,
3498
+ }, '⠿'),
3499
+ (dapSnap.candidates
3500
+ ? [
3501
+ React.createElement('select', {
3502
+ key: 'pick', className: 'edrv-dapbar-cfg', title: '选择附加目标进程(processName 命中多个候选)',
3503
+ value: String(dapPickPid || dapSnap.candidates[0]?.pid || ''),
3504
+ onChange: (e) => setDapPickPid(Number(e.target.value)),
3505
+ },
3506
+ dapSnap.candidates.map((it) => React.createElement('option', { key: it.pid, value: String(it.pid) },
3507
+ it.pid + ' : ' + it.name + (it.title ? ' — ' + it.title.slice(0, 44) : '')))),
3508
+ dapBtn('✓', '附加到选中进程', false, () => dapStore.confirmPick(dapPickPid || dapSnap.candidates![0]?.pid || 0)),
3509
+ dapBtn('✕', '取消', false, () => dapStore.cancelPick()),
3510
+ ]
3511
+ : React.createElement('select', {
3512
+ className: 'edrv-dapbar-cfg',
3513
+ title: '调试配置(读取工作区 .dsh/launch.json;适配器由扩展清单 contributes.debuggers 提供)',
3514
+ value: dapSnap.selectedConfig ?? '',
3515
+ onChange: (e) => dapStore.selectConfig(e.target.value),
3516
+ },
3517
+ dapSnap.configs.length
3518
+ ? dapSnap.configs.map((c) => {
3519
+ // 旧 host 无 adapters 数据时视为全部可用(保持兼容);有数据则按可用性置灰并给原因
3520
+ const adapter = dapSnap.adapters.find((a) => a.type === c.type)
3521
+ const ready = dapSnap.adapters.length === 0 || adapter?.available === true
3522
+ const suffix = ready ? '' : '(' + (adapter?.reason || '未发现该类型适配器') + ')'
3523
+ return React.createElement('option', { key: c.name, value: c.name, disabled: !ready }, c.name + suffix)
3524
+ })
3525
+ : React.createElement('option', { value: '' }, '无可用调试配置'))),
3526
+ 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()),
3527
+ dapBtn('⏸', '暂停', !dapActive, () => { void dapStore.action('pause') }),
3528
+ dapBtn('⤵', '单步跳过(F10)', !dapPaused, () => { void dapStore.action('next') }),
3529
+ dapBtn('↓', '单步步入(F11)', !dapPaused, () => { void dapStore.action('stepIn') }),
3530
+ dapBtn('↑', '单步步出(Shift+F11)', !dapPaused, () => { void dapStore.action('stepOut') }),
3531
+ dapBtn('⏹', '停止(Shift+F5)', !dapActive, () => { void dapStore.stop() }),
3532
+ (dapPhaseTextOf()
3533
+ ? React.createElement('span', { className: 'edrv-dapbar-phase' + (dapPaused ? ' paused' : '') },
3534
+ dapPhaseTextOf() + (dapSnap.pid ? ' · pid ' + dapSnap.pid : ''))
3535
+ : null),
3536
+ (dapSnap.error
3537
+ ? React.createElement('span', { className: 'edrv-dapbar-err', title: dapSnap.error }, '⚠ ' + dapSnap.error.slice(0, 40))
3538
+ : null)) : null
3539
+
3540
+ /**
3541
+ * 「添加配置」按钮点击:定位 configurations 数组插入点(数组开头行首),
3542
+ * 显式拉起 Monaco 补全下拉选择调试配置片段插入。
3543
+ * @author ddj 2026年09月22号
3544
+ */
3545
+ const addLaunchConfig = async () => {
3546
+ const ed = editorRef.current
3547
+ if (!ed) { setStatus('无活动编辑器'); return }
3548
+ const path = modelPathOf(ed)
3549
+ if (!path || !LAUNCH_JSON_RE.test(String(path).replace(/\\/g, '/'))) {
3550
+ setStatus('请先打开 .dsh/launch.json')
3551
+ return
3552
+ }
3553
+ const model = ed.getModel && ed.getModel()
3554
+ const pos = model ? findArrayPos(model.getValue()) : null
3555
+ if (!pos) { setStatus('未找到 configurations 数组'); return }
3556
+ // 预拉片段并预检可用条数:全不可用时不弹空 suggest(否则按钮点了无反馈)
3557
+ const snippets = await prefetchSnippets().catch(() => [])
3558
+ if (!snippets.some((snip) => snip.available !== false)) {
3559
+ setStatus('无可用适配器的配置模板(适配器入口缺失或未发现该类型适配器)')
3560
+ return
3561
+ }
3562
+ ed.focus()
3563
+ ed.setPosition({ lineNumber: pos.line, column: pos.column })
3564
+ if (typeof ed.revealPositionInCenterIfOutsideViewport === 'function') {
3565
+ ed.revealPositionInCenterIfOutsideViewport({ lineNumber: pos.line, column: pos.column })
3566
+ }
3567
+ ed.trigger('edrv-addcfg', 'editor.action.triggerSuggest', null)
3568
+ setStatus('选择要插入的调试配置')
3071
3569
  }
3072
3570
 
3571
+ // 「添加配置」按钮(VSCode 同款形态,编辑区右下角):仅插件专属 launch.json 打开且非差异审阅态显示
3572
+ const addCfgBtn = (active && !svnDiff && LAUNCH_JSON_RE.test(String(active).replace(/\\/g, '/')))
3573
+ ? React.createElement('button', {
3574
+ className: 'edrv-addcfg',
3575
+ title: '添加调试配置(插入到 configurations 数组开头)',
3576
+ onClick: addLaunchConfig,
3577
+ }, '添加配置…')
3578
+ : null
3579
+
3073
3580
  // 主编辑列(侧边栏右侧):pathBar + tabRow + 编辑/差异区(底部整条留给 DSH 对话输入栏)
3074
3581
  const editorArea = React.createElement('div', { className: 'edrv-editor-area' },
3582
+ dapBar,
3583
+ addCfgBtn,
3075
3584
  body,
3076
3585
  hoverEl,
3077
3586
  overlay)
@@ -3216,31 +3725,124 @@ export function EditorView(props) {
3216
3725
  setSvnLog({ ...svnLog, limit: SVN_LOG_SHOW_ALL_LIMIT })
3217
3726
  ensureSvnLog(sessionId, scope, svnLog.target || '', SVN_LOG_SHOW_ALL_LIMIT, true, svnLogOptsOf(svnLog))
3218
3727
  },
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) },
3728
+ onOpenDiff: (relPath, revision) => { setSvnLog(null); runSvnDiffRev(relPath, revision, svnLog.target || '') },
3729
+ onComparePair: (revA, revB) => { setSvnLog(null); runSvnDiffPair(svnLog.target || '', revA, revB, svnLog.target || '') },
3730
+ onCompareWorking: (revision) => { setSvnLog(null); runSvnDiffWorking(svnLog.target || '', revision, svnLog.target || '') },
3222
3731
  onOpenFile: (relPath) => { setSvnLog(null); openFile(relPath, false) },
3223
3732
  onClose: () => { setSvnLog(null); setSvnLogError('') },
3224
3733
  })
3225
3734
  : null
3735
+ // W2-1 补丁对话框 / W2-2 目录对比对话框:portal 到 body,与日志弹窗同一挂载方式
3736
+ const patchDialogEl = patchDialog
3737
+ ? React.createElement(SvnPatchDialog, {
3738
+ key: 'edrv-svn-patch',
3739
+ path: patchDialog.path,
3740
+ text: patchDialog.text,
3741
+ truncated: patchDialog.truncated === true,
3742
+ binary: patchDialog.binary === true,
3743
+ onNote: (message) => setStatus(message),
3744
+ onClose: () => setPatchDialog(null),
3745
+ })
3746
+ : null
3747
+ const sumDialogEl = sumDialog
3748
+ ? React.createElement(SvnSumDialog, {
3749
+ key: 'edrv-svn-sum',
3750
+ sessionId,
3751
+ path: sumDialog.path,
3752
+ onNote: (message) => setStatus(message),
3753
+ onClose: () => setSumDialog(null),
3754
+ })
3755
+ : null
3226
3756
  // 诊断日志弹窗:portal 到 body,与命令栏/SVN 日志弹窗同一挂载方式
3227
3757
  const dlogEl = dlogOpen
3228
3758
  ? React.createElement(LogDialog, { key: 'edrv-dlog', sessionId, onClose: () => setDlogOpen(false) })
3229
3759
  : null
3760
+ // 断点右键菜单(逐条对齐 CodeBuddy:4 项添加类 / 删除·编辑·启停 / 记录点名词 / 暂停追加运行到行)
3761
+ const runBpMenuAction = (action) => {
3762
+ const scope = dapScopeRef.current
3763
+ const cwd = dapCwdRef.current
3764
+ const menu = dapBpMenu
3765
+ if (!menu) return
3766
+ const { path, line } = menu
3767
+ if (action === 'add') { dapStore.toggleAt(scope, cwd, path, line); return }
3768
+ if (action === 'delete') { if (menu.hasBp) dapStore.toggleAt(scope, cwd, path, line); return }
3769
+ if (action === 'disable' || action === 'enable') { dapStore.setBpEnabled(scope, cwd, path, line, action === 'enable'); return }
3770
+ if (action === 'edit') { openBpEditor(path, line); return }
3771
+ if (action === 'run-to-line') { dapStore.runTo(cwd, path, line); return }
3772
+ if (action === 'add-triggered') { setStatus('当前 Lua 调试器不支持触发的断点'); return }
3773
+ // 添加条件断点 / 添加记录点:先建点,再开浮层并选中对应模式(0 表达式 / 2 日志消息)
3774
+ dapStore.toggleAt(scope, cwd, path, line)
3775
+ openBpEditor(path, line, action === 'add-logpoint' ? 2 : 0)
3776
+ }
3777
+ const openBpEditor = (path, line, mode) => {
3778
+ const entry = (dapStore.bpMapOf(dapScopeRef.current)[path] ?? []).find((it) => it.line === line)
3779
+ if (!entry) return
3780
+ const ed = editorRef.current
3781
+ if (!ed?.changeViewZones) return
3782
+ // 行内浮层(View Zone,整编辑区宽 + 推开内容,对齐 VS Code breakpointWidget):同一时刻只挂一个
3783
+ closeBpWidget()
3784
+ bpWidgetRef.current = { ...createBpWidget(ed, {
3785
+ line,
3786
+ entry,
3787
+ mode,
3788
+ onSave: (fields) => {
3789
+ dapStore.applyBpFields(dapScopeRef.current, dapCwdRef.current, path, line, fields)
3790
+ setStatus('断点已更新')
3791
+ closeBpWidget()
3792
+ },
3793
+ onClose: () => closeBpWidget(),
3794
+ }), path, line }
3795
+ }
3796
+ const closeBpWidget = () => {
3797
+ const held = bpWidgetRef.current
3798
+ if (!held) return
3799
+ bpWidgetRef.current = null
3800
+ try { held.close() } catch { /* 编辑器可能已销毁 */ }
3801
+ }
3802
+ const dapBpMenuEl = dapBpMenu
3803
+ ? React.createElement(ContextMenu, {
3804
+ x: dapBpMenu.x,
3805
+ y: dapBpMenu.y,
3806
+ onClose: () => setDapBpMenu(null),
3807
+ entries: buildBpMenu({
3808
+ hasBreakpoint: dapBpMenu.hasBp,
3809
+ enabled: dapBpMenu.enabled,
3810
+ isLogpoint: dapBpMenu.isLogpoint,
3811
+ paused: dapSnap.phase === 'paused',
3812
+ canRunTo: true,
3813
+ canTriggered: false,
3814
+ }).map((item) => ({
3815
+ id: 'dap-bp-' + item.id,
3816
+ label: item.label,
3817
+ hint: item.hint,
3818
+ separator: item.separator,
3819
+ onClick: () => runBpMenuAction(item.id),
3820
+ })),
3821
+ })
3822
+ : null
3823
+ const dapBpEditEl = null
3230
3824
  const rootEl = layout === 'side'
3231
3825
  ? React.createElement('div', { ref: viewRootRef, 'data-edrv-view': '1', 'data-edrv-layout': 'side', className: 'edrv-view-side', style: Object.assign({}, baseStyle, { height: '100%' }) },
3232
3826
  editorRow,
3233
3827
  tabMenuEl,
3234
3828
  paletteEl,
3235
3829
  svnLogEl,
3830
+ patchDialogEl,
3831
+ sumDialogEl,
3236
3832
  dlogEl,
3237
- snippetPickerEl)
3833
+ snippetPickerEl,
3834
+ dapBpMenuEl,
3835
+ dapBpEditEl)
3238
3836
  : 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
3837
  editorRow,
3240
3838
  tabMenuEl,
3241
3839
  paletteEl,
3242
3840
  svnLogEl,
3841
+ patchDialogEl,
3842
+ sumDialogEl,
3243
3843
  dlogEl,
3244
- snippetPickerEl)
3844
+ snippetPickerEl,
3845
+ dapBpMenuEl,
3846
+ dapBpEditEl)
3245
3847
  return rootEl
3246
3848
  }