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
@@ -5,7 +5,7 @@
5
5
  @author ddj 2026-09-10 */
6
6
  [data-edrv-view] { box-sizing: border-box; --edrv-tab-h: 34px; }
7
7
 
8
- [data-edrv-view] .edrv-tabs { display: flex; align-items: center; gap: 2px; padding: 2px 8px 0; background: var(--dsw-alias-bg-layer-1, transparent); overflow-x: auto; scrollbar-width: thin; flex-shrink: 0; }
8
+ [data-edrv-view] .edrv-tabs { display: flex; align-items: center; gap: 2px; padding: 2px 8px 0; background: var(--dsw-alias-bg-layer-1, transparent); overflow-x: auto; overflow-y: hidden; scrollbar-width: thin; flex-shrink: 0; overscroll-behavior-x: contain; } /* overscroll-behavior-x: 横滚到边界不把滚动链传给页面(滚轮横滚接管更可控)@author ddj 2026年09月18号 */
9
9
  [data-edrv-view] .edrv-tab { display: inline-flex; align-items: center; gap: 6px; height: 28px; padding: 0 8px 0 12px; border: none; border-radius: 6px 6px 0 0; background: transparent; color: var(--dsw-alias-label-secondary, #aaa); font-size: 12px; cursor: pointer; white-space: nowrap; flex-shrink: 0; }
10
10
  [data-edrv-view] .edrv-tab:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,.06)); }
11
11
  [data-edrv-view] .edrv-tab.edrv-tab-active { background: var(--dsw-alias-bg-base, #111); color: var(--dsw-alias-label-primary, #eee); box-shadow: inset 0 -2px 0 var(--dsw-alias-brand-primary, #4f8cff); }
@@ -84,6 +84,15 @@
84
84
  [data-edrv-view] .edrv-pdf-mode-on { background: var(--dsw-alias-interactive-bg-hover, rgba(15,157,88,.12)); color: var(--dsw-alias-brand-primary, #0f9d58); border-color: var(--dsw-alias-brand-primary, #0f9d58); }
85
85
  [data-edrv-view] .edrv-pdf-dirty { color: var(--dsw-alias-state-warn-primary, #d97706); border-color: var(--dsw-alias-state-warn-primary, #d97706); }
86
86
  [data-edrv-view] .edrv-pdf-stage { position: absolute; top: 34px; left: 0; right: 0; bottom: 0; overflow: auto; }
87
+ /* Markdown 预览(需求 4):工具条 + 居中阅读栏;正文排版由官方 MarkdownText 自带的 --dsw-* 样式负责 */
88
+ [data-edrv-view] .edrv-mdview { flex: 1 1 auto; min-width: 0; min-height: 0; display: flex; flex-direction: column; overflow: hidden; }
89
+ [data-edrv-view] .edrv-mdview-bar { display: flex; align-items: center; gap: 8px; height: 34px; padding: 0 10px; flex-shrink: 0; border-bottom: 1px solid var(--dsw-alias-border-l1, #e0e6e8); background: var(--dsw-alias-bg-layer-1, transparent); }
90
+ [data-edrv-view] .edrv-mdview-meta { font-size: 11px; color: var(--dsw-alias-label-tertiary, #9aa5b1); white-space: nowrap; }
91
+ [data-edrv-view] .edrv-mdview-stage { flex: 1 1 auto; min-height: 0; overflow: auto; padding: 14px 20px 28px; }
92
+ [data-edrv-view] .edrv-mdview-doc { max-width: 880px; margin: 0 auto; width: 100%; }
93
+ [data-edrv-view] .edrv-mdview-fallback { font-size: 12px; line-height: 1.7; color: var(--dsw-alias-label-secondary, #52606d); }
94
+ [data-edrv-view] .edrv-mdview-pre { margin: 8px 0 0; padding: 10px 12px; border-radius: 8px; background: var(--dsw-alias-bg-layer-2, #f0f4f4); color: var(--dsw-alias-label-primary, #1f2933); font-family: var(--ds-font-family-code, ui-monospace, monospace); font-size: 11px; line-height: 1.6; white-space: pre-wrap; word-break: break-word; }
95
+ [data-edrv-view] .edrv-pb-mdbtn { height: 20px; padding: 0 8px; font-size: 11px; flex-shrink: 0; }
87
96
  [data-edrv-view] .edrv-pdf-stage .edrv-pdf-viewer { margin: 0 auto; }
88
97
  [data-edrv-view] .edrv-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; height: 100%; color: var(--dsw-alias-label-tertiary, #777); font-size: 13px; }
89
98
  [data-edrv-view] .edrv-diffbar { position: absolute; left: 0; right: 0; bottom: 10px; margin: 0 auto; z-index: 25; width: fit-content; max-width: calc(100% - 20px); border-radius: 12px; background: var(--dsw-specific-tip, var(--dsw-alias-bg-layer-2, #f0f4f4)); border: 1px solid var(--dsw-alias-border-l1, #e0e6e8); box-shadow: none; overflow: visible; display: flex; flex-direction: column; }
@@ -321,6 +330,44 @@
321
330
  [data-edrv-view] .edrv-svndiff-tag { padding: 1px 6px; border-radius: 999px; background: var(--dsw-alias-bg-layer-2, #f0f4f4); color: var(--dsw-alias-label-tertiary, #9aa5b1); }
322
331
  [data-edrv-view] .edrv-svndiff-host { flex: 1 1 auto; min-height: 0; }
323
332
  [data-edrv-view] .edrv-svndiff-empty { display: flex; flex: 1 1 auto; align-items: center; justify-content: center; padding: 24px; color: var(--dsw-alias-label-tertiary, #9aa5b1); font-size: 12px; text-align: center; }
333
+ /* 差异整行色块与行首 +/- 指示符:vendored Monaco(diffEditorWidget2)静态 CSS 无
334
+ .line-insert/.line-delete 背景规则,整行色块靠主题键 + 此处固定色兜底保证可见。
335
+ 注意不要用 --dsw-alias-interactive-bg-hover 类 token:实测为 92% 透明中性色,肉眼不可见。
336
+ 固定色取自研差异 UI 同色相(绿 #0f9d58 / 红 #d9534f),alpha 提到肉眼可辨。 */
337
+ [data-edrv-view] .monaco-editor .line-insert { background: rgba(15, 157, 88, 0.18); }
338
+ [data-edrv-view] .monaco-editor .line-delete { background: rgba(217, 83, 79, 0.16); }
339
+ [data-edrv-view] .monaco-editor .gutter-insert { background: rgba(15, 157, 88, 0.35); }
340
+ [data-edrv-view] .monaco-editor .gutter-delete { background: rgba(217, 83, 79, 0.35); }
341
+ [data-edrv-view] .monaco-editor .insert-sign { color: var(--dsw-alias-state-success-primary, #0f9d58); }
342
+ [data-edrv-view] .monaco-editor .delete-sign { color: var(--dsw-alias-state-error-primary, #d9534f); }
343
+ /* 差异条快速跳转簇(对齐自研审查条 ↑ i/N ↓ 的形态) */
344
+ [data-edrv-view] .edrv-svndiff-nav { display: flex; align-items: center; gap: 2px; flex-shrink: 0; }
345
+ [data-edrv-view] .edrv-svndiff-nav-count { min-width: 36px; text-align: center; font-size: 11px; color: var(--dsw-alias-label-secondary, #52606d); }
346
+ /* W1-3 差异统计徽标(+新增/修改 −删除/修改;色相与行块绿红一致) @author ddj 2026-09-20 */
347
+ [data-edrv-view] .edrv-svndiff-stat { display: inline-flex; align-items: center; gap: 4px; flex-shrink: 0; font-family: var(--ds-font-family-code, ui-monospace, monospace); font-size: 11px; font-variant-numeric: tabular-nums; }
348
+ [data-edrv-view] .edrv-svndiff-stat-add { color: var(--dsw-alias-state-success-primary, #0f9d58); }
349
+ [data-edrv-view] .edrv-svndiff-stat-del { color: var(--dsw-alias-state-error-primary, #d9534f); }
350
+ /* W1-1 差异视图选项条(右端开关组;复用 .edrv-svn-check 的 label 形态) */
351
+ [data-edrv-view] .edrv-svndiff-opts { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
352
+ /* W1-2 变更面板名称过滤输入框(工具条行内,紧凑) @author ddj 2026-09-20 */
353
+ [data-edrv-view] .edrv-svn-name-filter { width: 168px; padding: 1px 6px; border: 1px solid var(--dsw-alias-border-l1, #e0e6e8); border-radius: 5px; background: var(--dsw-alias-bg-layer-1, #fff); color: var(--dsw-alias-label-primary, #1f2933); font-size: 11px; line-height: 14px; }
354
+ [data-edrv-view] .edrv-svn-name-filter:focus { outline: none; border-color: var(--dsw-alias-brand-primary, #0f9d58); }
355
+ [data-edrv-view] .edrv-svn-name-filter::placeholder { color: var(--dsw-alias-label-tertiary, #9aa5b1); }
356
+ /* W2-3 远端更新提示条(工具条上方;warn = 失败降级) @author ddj 2026-09-20 */
357
+ [data-edrv-view] .edrv-svn-hintbar { display: flex; align-items: center; gap: 6px; margin: 0 10px 4px 12px; padding: 3px 8px; border-radius: 5px; background: var(--dsw-alias-bg-layer-2, #f0f4f4); color: var(--dsw-alias-label-secondary, #52606d); font-size: 11px; flex-shrink: 0; }
358
+ [data-edrv-view] .edrv-svn-hintbar-warn { background: rgba(217, 83, 79, 0.12); color: var(--dsw-alias-state-error-primary, #d9534f); }
359
+ /* W2-5 疑似改名标记(行内 ⇄;hover 见 tooltip) */
360
+ [data-edrv-view] .edrv-svn-pair-mark { flex-shrink: 0; color: var(--dsw-alias-state-warn-primary, #7c3aed); font-size: 11px; cursor: help; }
361
+ /* W2-1 补丁对话框 / W2-2 目录对比对话框 @author ddj 2026-09-20 */
362
+ [data-edrv-view] .edrv-svn-patch-dialog { max-width: calc(100vw - 48px); }
363
+ [data-edrv-view] .edrv-svn-patch-head { display: flex; align-items: center; gap: 8px; padding-bottom: 8px; }
364
+ [data-edrv-view] .edrv-svn-patch-notes { display: flex; flex-direction: column; gap: 4px; padding-bottom: 6px; }
365
+ [data-edrv-view] .edrv-svn-patch-warn { color: var(--dsw-alias-state-error-primary, #d9534f); font-size: 11px; }
366
+ [data-edrv-view] .edrv-svn-patch-pre { max-height: 46vh; overflow: auto; margin: 0 0 8px; padding: 8px; border: 1px solid var(--dsw-alias-border-l1, #e0e6e8); border-radius: 6px; background: var(--dsw-alias-bg-layer-2, #f7f9f9); color: var(--dsw-alias-label-primary, #1f2933); font-family: var(--ds-font-family-code, ui-monospace, monospace); font-size: 11px; line-height: 1.5; white-space: pre; user-select: text; }
367
+ [data-edrv-view] .edrv-svn-patch-foot { display: flex; align-items: center; gap: 8px; }
368
+ [data-edrv-view] .edrv-svn-sum-form { display: flex; align-items: center; gap: 10px; padding-bottom: 8px; }
369
+ [data-edrv-view] .edrv-svn-sum-list { max-height: 44vh; overflow: auto; border: 1px solid var(--dsw-alias-border-l1, #e0e6e8); border-radius: 6px; padding: 2px; margin-bottom: 8px; }
370
+ [data-edrv-view] .edrv-svnlog-path-kind { flex-shrink: 0; padding: 0 5px; border-radius: 999px; background: var(--dsw-alias-bg-layer-2, #f0f4f4); color: var(--dsw-alias-label-tertiary, #9aa5b1); font-size: 10px; }
324
371
 
325
372
  /* ===== 通用弹窗外壳(ModalShell)+ SVN 日志弹窗 @author ddj 2026-09-16 ===== */
326
373
  /* z-index 190:落在既有阶梯内(命令栏 200 > 片段二级 197 > 片段一级 195 > 190),
@@ -614,3 +661,141 @@
614
661
 
615
662
 
616
663
 
664
+
665
+ /* 调试(DAP):断点圆点 / 停帧行 / 浮动调试工具条。
666
+ 圆点挂在 Monaco glyphMargin(glyphMarginClassName),行背景为 isWholeLine className;
667
+ 工具条绝对定位于编辑区右上(editorArea relative 由 .edrv-editor-area 自身定位)。 */
668
+ /* 断点圆点:装饰类占满整行,圆点经 ::before 在本行盒子内居中(行归属明确、不跨行界) */
669
+ [data-edrv-view] .edrv-dap-bp { cursor: pointer; }
670
+ [data-edrv-view] .edrv-dap-bp::before { content: ''; display: block; width: 7px; height: 7px; margin: 6px auto 0; border-radius: 50%; background: var(--dsw-alias-state-error-primary, #e51400); }
671
+ [data-edrv-view] .edrv-dap-bp-off { cursor: pointer; }
672
+ [data-edrv-view] .edrv-dap-bp-off::before { content: ''; display: block; box-sizing: border-box; width: 7px; height: 7px; margin: 6px auto 0; border-radius: 50%; border: 1px solid var(--dsw-alias-label-tertiary, #9aa5b1); opacity: .6; }
673
+ /* 未验证断点(适配器真实回执 verified:false):空心红圈,对齐 VS Code 空心圆语义 */
674
+ [data-edrv-view] .edrv-dap-bp-unverified { cursor: pointer; }
675
+ [data-edrv-view] .edrv-dap-bp-unverified::before { content: ''; display: block; box-sizing: border-box; width: 7px; height: 7px; margin: 6px auto 0; border-radius: 50%; border: 1px solid var(--dsw-alias-state-error-primary, #e51400); opacity: .75; }
676
+ [data-edrv-view] .edrv-dap-paused-line { background: var(--dsw-alias-state-warn-tertiary, rgba(229, 185, 63, .22)); }
677
+ [data-edrv-view] .edrv-dap-paused-glyph { cursor: pointer; }
678
+ [data-edrv-view] .edrv-dap-paused-glyph::before { content: ''; display: block; width: 5px; height: 12px; margin: 4px auto 0; border-radius: 2px; background: var(--dsw-alias-state-warn-primary, #e5b93f); }
679
+ [data-edrv-view] .edrv-editor-area { position: relative; }
680
+ /* launch.json 右下角「添加配置」按钮(VSCode 同款形态):仅 launch.json 打开时渲染 */
681
+ [data-edrv-view] .edrv-addcfg { position: absolute; right: 14px; bottom: 10px; z-index: 13; padding: 4px 12px; border: 1px solid var(--dsw-alias-border-l1, #e0e6e8); border-radius: 6px; background: var(--dsw-alias-bg-layer-1, #f7f9fa); color: var(--dsw-alias-label-secondary, #52606d); font-size: 12px; line-height: 18px; cursor: pointer; box-shadow: 0 1px 4px rgba(0, 0, 0, .1); }
682
+ [data-edrv-view] .edrv-addcfg:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(127, 127, 127, .15)); color: var(--dsw-alias-label-primary, #1f2933); }
683
+ [data-edrv-view] .edrv-dapbar { position: absolute; top: 6px; right: 14px; z-index: 12; display: none; align-items: center; gap: 3px; padding: 3px 5px; border-radius: 8px; border: 1px solid var(--dsw-alias-border-l1, #e0e6e8); background: var(--dsw-alias-bg-layer-1, #f7f9fa); box-shadow: 0 2px 8px rgba(0, 0, 0, .12); }
684
+ [data-edrv-view] .edrv-dapbar.on { display: flex; border-color: var(--dsw-alias-border-l2, #cdd6da); }
685
+ [data-edrv-view] .edrv-dapbar-grip { flex: 0 0 auto; width: 16px; height: 22px; display: inline-flex; align-items: center; justify-content: center; cursor: grab; user-select: none; color: var(--dsw-alias-label-tertiary, #7b8794); font-size: 12px; }
686
+ [data-edrv-view] .edrv-dapbar-grip:active, [data-edrv-view] .edrv-dapbar.dragging .edrv-dapbar-grip { cursor: grabbing; }
687
+ [data-edrv-view] .edrv-dapbar.dragging { user-select: none; }
688
+ [data-edrv-view] .edrv-dapbar-cfg { max-width: 220px; height: 22px; font-size: 11px; border: 1px solid var(--dsw-alias-border-l1, #e0e6e8); border-radius: 6px; background: var(--dsw-alias-bg-base, transparent); color: var(--dsw-alias-label-primary, inherit); }
689
+ [data-edrv-view] .edrv-dapbar-btn { min-width: 24px; height: 22px; padding: 0 6px; border: none; border-radius: 6px; background: transparent; color: var(--dsw-alias-label-primary, inherit); font-size: 12px; line-height: 1; cursor: pointer; }
690
+ [data-edrv-view] .edrv-dapbar-btn:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover, rgba(127, 127, 127, .15)); }
691
+ [data-edrv-view] .edrv-dapbar-btn:disabled { opacity: .35; cursor: default; }
692
+ [data-edrv-view] .edrv-dapbar-phase { padding: 0 4px; font-size: 11px; white-space: nowrap; color: var(--dsw-alias-label-secondary, #52606d); }
693
+ [data-edrv-view] .edrv-dapbar-phase.paused { color: var(--dsw-alias-state-warn-primary, #b7791f); font-weight: 600; }
694
+ [data-edrv-view] .edrv-dapbar-err { max-width: 260px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 11px; color: var(--dsw-alias-state-error-primary, #e05252); }
695
+
696
+ /* 调试面板(活动栏「调试」页签):四段折叠区 + 断点列表 + REPL。 */
697
+ [data-edrv-view] .edrv-dappanel { display: flex; flex-direction: column; gap: 10px; height: 100%; min-height: 0; overflow: auto; padding: 2px 2px 8px; }
698
+ [data-edrv-view] .edrv-dappanel-head { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; row-gap: 4px; font-size: 12px; color: var(--dsw-alias-label-secondary, #52606d); }
699
+ [data-edrv-view] .edrv-dappanel-phase { padding: 2px 8px; border-radius: 999px; background: var(--dsw-alias-bg-layer-2, #f0f4f4); }
700
+ [data-edrv-view] .edrv-dappanel-phase.paused { background: var(--dsw-alias-state-warn-tertiary, rgba(229, 185, 63, .18)); color: var(--dsw-alias-state-warn-primary, #b7791f); font-weight: 600; }
701
+ [data-edrv-view] .edrv-dappanel-cfg { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
702
+ [data-edrv-view] .edrv-dappanel-pid { font-family: var(--ds-font-family-code, ui-monospace, monospace); font-size: 11px; }
703
+ [data-edrv-view] .edrv-dappanel-err { color: var(--dsw-alias-state-error-primary, #e05252); }
704
+ [data-edrv-view] .edrv-dapseg { display: flex; flex-direction: column; gap: 2px; min-height: 0; }
705
+ [data-edrv-view] .edrv-dapseg-body { min-height: 0; overflow-y: auto; overflow-x: hidden; overscroll-behavior: contain; }
706
+ /* 段间分隔条(splitview sash):拖动调整相邻两段高度占比;各段高度由 DebugPanel 内联接管。 */
707
+ [data-edrv-view] .edrv-dapseg-sash { flex: 0 0 auto; height: 4px; margin: 2px 0; border-radius: 2px; background: transparent; cursor: row-resize; }
708
+ [data-edrv-view] .edrv-dapseg-sash:hover, [data-edrv-view] .edrv-dapseg-sash.dragging { background: var(--dsw-alias-border-l2, #cdd6da); }
709
+ /* 折叠段头:chevron + 标题 + 悬浮动作区(动作 hover 显隐;REPL 头不参与折叠)。 */
710
+ [data-edrv-view] .edrv-dapseg-head { display: flex; align-items: center; gap: 4px; font-size: 11px; font-weight: 600; color: var(--dsw-alias-label-tertiary, #7b8794); padding: 2px 2px; cursor: pointer; border-radius: 4px; }
711
+ [data-edrv-view] .edrv-dapseg-head:hover { color: var(--dsw-alias-label-secondary, #52606d); }
712
+ [data-edrv-view] .edrv-daprepl .edrv-dapseg-head { cursor: default; padding: 2px 0; }
713
+ [data-edrv-view] .edrv-dapseg-chevron { flex: 0 0 auto; width: 14px; text-align: center; font-size: 10px; }
714
+ [data-edrv-view] .edrv-dapseg-title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
715
+ [data-edrv-view] .edrv-dapseg-act { flex: 0 0 auto; min-width: 20px; height: 18px; padding: 0 4px; border: none; border-radius: 4px; background: transparent; color: var(--dsw-alias-label-tertiary, #7b8794); font-size: 11px; line-height: 1; cursor: pointer; opacity: 0; }
716
+ [data-edrv-view] .edrv-dapseg-head:hover .edrv-dapseg-act { opacity: 1; }
717
+ [data-edrv-view] .edrv-dapseg-act:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(127, 127, 127, .15)); color: var(--dsw-alias-label-primary, inherit); }
718
+ [data-edrv-view] .edrv-dapempty { font-size: 12px; color: var(--dsw-alias-label-tertiary, #9aa5b1); padding: 4px 2px; }
719
+ [data-edrv-view] .edrv-dapframe { display: flex; align-items: baseline; gap: 8px; padding: 3px 4px; border-radius: 6px; cursor: pointer; font-size: 12px; }
720
+ [data-edrv-view] .edrv-dapframe:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(127, 127, 127, .12)); }
721
+ [data-edrv-view] .edrv-dapframe-name { color: var(--dsw-alias-label-primary, inherit); font-weight: 600; }
722
+ [data-edrv-view] .edrv-dapframe-loc { color: var(--dsw-alias-label-tertiary, #9aa5b1); font-family: var(--ds-font-family-code, ui-monospace, monospace); font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
723
+ [data-edrv-view] .edrv-dapscope { font-size: 11px; font-weight: 600; color: var(--dsw-alias-label-secondary, #52606d); padding: 4px 2px 1px; }
724
+ [data-edrv-view] .edrv-dapvar { display: flex; align-items: baseline; gap: 6px; width: 100%; box-sizing: border-box; font-size: 12px; padding: 1px 2px; min-width: 0; }
725
+ [data-edrv-view] .edrv-dapvar-tw { flex: 0 0 auto; width: 14px; border: none; background: transparent; color: var(--dsw-alias-label-tertiary, #9aa5b1); cursor: pointer; font-size: 10px; padding: 0; }
726
+ [data-edrv-view] span.edrv-dapvar-tw { cursor: default; }
727
+ [data-edrv-view] .edrv-dapvar-name { flex: 0 1 34%; min-width: 0; overflow: hidden; text-overflow: ellipsis; color: var(--dsw-alias-label-primary, inherit); font-family: var(--ds-font-family-code, ui-monospace, monospace); white-space: nowrap; }
728
+ [data-edrv-view] .edrv-dapvar-val { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsw-alias-label-secondary, #52606d); }
729
+ [data-edrv-view] .edrv-dapvar-type { flex: 0 0 auto; font-size: 10px; color: var(--dsw-alias-label-tertiary, #9aa5b1); }
730
+ [data-edrv-view] .edrv-dapframe.selected { background: var(--dsw-alias-interactive-bg-hover, rgba(127, 127, 127, .16)); outline: 1px solid var(--dsw-alias-border-l2, #cdd6da); }
731
+ [data-edrv-view] .edrv-dapwatch { display: flex; align-items: baseline; gap: 6px; width: 100%; box-sizing: border-box; min-width: 0; font-size: 12px; padding: 1px 2px; }
732
+ [data-edrv-view] .edrv-dapwatch-x { border: none; background: transparent; color: var(--dsw-alias-label-tertiary, #9aa5b1); cursor: pointer; width: 14px; }
733
+ [data-edrv-view] .edrv-dapwatch-x:hover { color: var(--dsw-alias-state-error-primary, #e05252); }
734
+ [data-edrv-view] .edrv-dapwatch-add { padding: 2px 0; }
735
+ [data-edrv-view] .edrv-dapbp { display: flex; align-items: center; gap: 6px; font-size: 12px; padding: 2px 2px; border-radius: 6px; cursor: pointer; }
736
+ [data-edrv-view] .edrv-dapbp:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(127, 127, 127, .12)); }
737
+ [data-edrv-view] .edrv-dapbp:active { background: var(--dsw-alias-interactive-bg-pressed, rgba(127, 127, 127, .2)); }
738
+ [data-edrv-view] .edrv-dapbp-dot { flex: 0 0 auto; width: 9px; height: 9px; border-radius: 50%; background: var(--dsw-alias-state-error-primary, #e51400); }
739
+ [data-edrv-view] .edrv-dapbp-dot.off { background: transparent; border: 1.5px solid var(--dsw-alias-label-tertiary, #9aa5b1); opacity: .6; }
740
+ [data-edrv-view] .edrv-dapbp-file { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; border: none; background: transparent; color: var(--dsw-alias-label-primary, inherit); cursor: pointer; padding: 0; font-size: 12px; }
741
+ [data-edrv-view] .edrv-dapbp-dir { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsw-alias-label-tertiary, #9aa5b1); font-size: 11px; text-align: right; }
742
+ [data-edrv-view] .edrv-dapbp-line { flex: 0 0 auto; font-size: 10px; padding: 1px 6px; border-radius: 999px; background: var(--dsw-alias-bg-layer-2, #f0f4f4); color: var(--dsw-alias-label-secondary, #52606d); }
743
+ [data-edrv-view] .edrv-dapbp-x { flex: 0 0 auto; border: none; background: transparent; color: transparent; cursor: pointer; width: 14px; }
744
+ [data-edrv-view] .edrv-dapbp:hover .edrv-dapbp-x { color: var(--dsw-alias-label-tertiary, #9aa5b1); }
745
+ [data-edrv-view] .edrv-dapbp-x:hover { color: var(--dsw-alias-state-error-primary, #e05252) !important; }
746
+ [data-edrv-view] .edrv-daprepl { display: flex; flex-direction: column; gap: 4px; }
747
+ [data-edrv-view] .edrv-daprepl-log { display: flex; flex-direction: column; gap: 1px; max-height: 180px; overflow: auto; }
748
+ [data-edrv-view] .edrv-daprepl-line { font-size: 11px; font-family: var(--ds-font-family-code, ui-monospace, monospace); color: var(--dsw-alias-label-secondary, #52606d); white-space: pre-wrap; word-break: break-all; display: flex; gap: 6px; }
749
+ [data-edrv-view] .edrv-daprepl-line.out { color: var(--dsw-alias-label-tertiary, #9aa5b1); }
750
+ [data-edrv-view] .edrv-daprepl-in { width: 100%; box-sizing: border-box; height: 26px; padding: 0 8px; font-size: 12px; border: 1px solid var(--dsw-alias-border-l1, #e0e6e8); border-radius: 6px; background: var(--dsw-alias-bg-base, transparent); color: var(--dsw-alias-label-primary, inherit); }
751
+ [data-edrv-view] .edrv-dappanel-run { flex: 0 0 auto; width: 24px; height: 24px; border: none; border-radius: 6px; background: transparent; color: var(--dsw-alias-label-primary, inherit); font-size: 13px; line-height: 1; cursor: pointer; }
752
+ [data-edrv-view] .edrv-dappanel-run:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(127, 127, 127, .15)); }
753
+ [data-edrv-view] .edrv-dappanel-run:disabled { opacity: .35; cursor: default; background: transparent; }
754
+ /* 面板顶部配置下拉(与编辑器浮动条 .edrv-dapbar-cfg 同视觉)。 */
755
+ [data-edrv-view] .edrv-dappanel-cfg-select { max-width: 190px; height: 24px; padding: 0 4px; font-size: 11px; border: 1px solid var(--dsw-alias-border-l1, #e0e6e8); border-radius: 6px; background: var(--dsw-alias-bg-base, transparent); color: var(--dsw-alias-label-primary, inherit); }
756
+ /* 断点行内编辑行(面板内展开编辑,控件复用 .edrv-bpwidget-* 视觉;左缩进对齐断点行文字列)。 */
757
+ [data-edrv-view] .edrv-dapbp-edit { display: flex; align-items: center; gap: 6px; padding: 3px 2px 3px 20px; border-radius: 6px; background: var(--dsw-alias-bg-layer-2, #f0f4f4); }
758
+ [data-edrv-view] .edrv-dapbp-edit .edrv-bpwidget-mode { height: 20px; }
759
+ [data-edrv-view] .edrv-dapbp-edit .edrv-bpwidget-input { height: 20px; }
760
+ /* 断点回执:未验证空心红圈(须排在 .off 之后,禁用态优先展示灰圈语义)。 */
761
+ [data-edrv-view] .edrv-dapbp-dot.unverified { background: transparent; border: 1.5px solid var(--dsw-alias-state-error-primary, #e51400); opacity: .75; }
762
+ /* 断点行内编辑浮层(Monaco content widget,锚定断点行上方;对齐 VS Code breakpointWidget 形态)。 */
763
+ /* View Zone 浮层:宽度/左移由 BpWidget.syncWidth 按 layout 内联设置
764
+ (Monaco .view-zones 仅内容区宽,需补偿 contentLeft 与滚动条),此处只管视觉。 */
765
+ [data-edrv-view] .edrv-bpwidget { position: relative; box-sizing: border-box; display: flex; align-items: center; gap: 6px; padding: 3px 8px; border-top: 1px solid var(--dsw-alias-border-l2, #cdd6da); border-bottom: 1px solid var(--dsw-alias-border-l2, #cdd6da); background: var(--dsw-alias-bg-layer-1, #f7f9fa); }
766
+ /* 断点编辑中的目标行高亮(浮层打开期间) */
767
+ [data-edrv-view] .edrv-dap-editing-line { background: var(--dsw-alias-interactive-bg-hover, rgba(127, 127, 127, .12)); }
768
+ [data-edrv-view] .edrv-bpwidget-mode { flex: 0 0 auto; height: 22px; padding: 0 4px; font-size: 12px; border: 1px solid var(--dsw-alias-border-l1, #e0e6e8); border-radius: 3px; background: var(--dsw-alias-bg-base, transparent); color: var(--dsw-alias-label-primary, inherit); cursor: pointer; }
769
+ [data-edrv-view] .edrv-bpwidget-input { flex: 1 1 auto !important; min-width: 0; width: auto !important; box-sizing: border-box; height: 22px; padding: 0 6px; font-size: 12px; font-family: var(--ds-font-family-code, ui-monospace, monospace); border: 1px solid var(--dsw-alias-border-l1, #e0e6e8); border-radius: 3px; background: var(--dsw-alias-bg-base, transparent); color: var(--dsw-alias-label-primary, inherit); }
770
+ [data-edrv-view] .edrv-bpwidget-input:focus { outline: 1px solid var(--dsw-alias-border-l2, #cdd6da); }
771
+
772
+ /* 断点 hover 预览(glyph 区):半透明小红点 + pointer 光标(对齐 VS Code .codicon-debug-hint)。 */
773
+ [data-edrv-view] .edrv-dap-bp-hint { cursor: pointer; }
774
+ [data-edrv-view] .edrv-dap-bp-hint::before { content: ''; display: block; width: 7px; height: 7px; margin: 6px auto 0; border-radius: 50%; background: var(--dsw-alias-state-error-primary, #e51400); opacity: .4; }
775
+ /* 行号区也给出可点提示(与 VS Code 一致:glyph 区与行号区都可点/可 hover);
776
+ 不拦截折叠箭头等自有控件的光标。 */
777
+ [data-edrv-view] .edrv-editor-row .monaco-editor .margin-view-overlays .line-numbers:hover { cursor: pointer; }
778
+
779
+ /* 暂停态 hover 调试块 + 变量树(成员全量直显 + table 成员折叠展开)。
780
+ ⚠️ 选择器一律用 data-*:本仓 vendored Monaco 的 markdown→DOM 管线会剥掉我们 HTML 里的
781
+ class(supportHtml/isTrusted 都不豁免),只有 data-* 存活;用 class 会静默失效。 */
782
+ [data-edrv-view] [data-edrv-dap] { display: block; min-width: 0; }
783
+ [data-edrv-view] [data-edrv-head] { display: flex; align-items: baseline; gap: 2px; }
784
+ [data-edrv-view] [data-edrv-head] [data-edrv-expr] { font-weight: 600; color: var(--dsw-alias-label-primary, #1f2933); }
785
+ [data-edrv-view] [data-edrv-head] [data-edrv-val] { font-family: var(--ds-font-family-code, ui-monospace, monospace); word-break: break-all; }
786
+ [data-edrv-view] [data-edrv-type] { margin-top: 2px; font-size: 12px; color: var(--dsw-alias-label-tertiary, #8a949b); }
787
+ [data-edrv-view] [data-edrv-tree] { margin: 2px 0 0; padding: 0; list-style: none; font-variant-numeric: tabular-nums; }
788
+ [data-edrv-view] [data-edrv-line] { display: flex; align-items: baseline; gap: 4px; padding: 1px 2px; border-radius: 3px; white-space: nowrap; }
789
+ [data-edrv-view] [data-edrv-line]:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(127, 127, 127, .12)); }
790
+ /* 折叠按钮(对齐 CodeBuddy 调试浮窗变量树的 twist):热区 14px + hover 反馈,未可展开时留空占位 */
791
+ [data-edrv-view] [data-edrv-twist] { flex: 0 0 auto; box-sizing: border-box; width: 14px; height: 14px; line-height: 13px; text-align: center; border-radius: 3px; color: var(--dsw-alias-label-tertiary, #8a949b); }
792
+ [data-edrv-view] [data-edrv-twist]:not(:empty) { cursor: pointer; user-select: none; }
793
+ [data-edrv-view] [data-edrv-twist]:not(:empty):hover { background: var(--dsw-alias-interactive-bg-hover, rgba(127, 127, 127, .2)); color: var(--dsw-alias-label-primary, #1f2933); }
794
+ [data-edrv-view] [data-edrv-name] { flex: 0 0 auto; color: var(--dsw-alias-label-primary, #1f2933); }
795
+ [data-edrv-view] [data-edrv-line] [data-edrv-eq] { flex: 0 0 auto; color: var(--dsw-alias-label-tertiary, #8a949b); }
796
+ [data-edrv-view] [data-edrv-line] [data-edrv-val] { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; color: var(--dsw-alias-label-secondary, #52606d); }
797
+ [data-edrv-view] [data-edrv-note] { color: var(--dsw-alias-label-tertiary, #8a949b); font-style: italic; }
798
+ /* Alt 提示层:由 hoverTree 注入到 .monaco-hover(滚动区之外)贴底常驻;容器预留同高内边距避免遮挡末行。
799
+ 不再用 sticky / `:has()`:滚动链在 .monaco-hover-content 上,sticky 会被滚走。 */
800
+ [data-edrv-view] .monaco-hover[data-edrv-hint-host="1"] { padding-bottom: 22px; }
801
+ [data-edrv-view] [data-edrv-hint="1"] { position: absolute; left: 0; right: 0; bottom: 0; box-sizing: border-box; height: 22px; padding: 2px 8px; font-size: 11px; line-height: 18px; color: var(--dsw-alias-label-tertiary, #8a949b); background: var(--dsw-alias-bg-overlay, #ffffff); border-top: 1px solid var(--dsw-alias-border-l1, #e0e6e8); }
@@ -11,7 +11,7 @@
11
11
  */
12
12
  import type { SvnActionDef } from '../shared/svnActions.js'
13
13
  import { svnCleanup } from './svnLog.js'
14
- import { refreshSvnChanges, svnAdd, svnDiffBase, svnRevert, svnTortoise, svnUpdate } from './svnStatus.js'
14
+ import { refreshSvnChanges, svnAdd, svnDiffBase, svnPatchText, svnRevert, svnTortoise, svnUpdate } from './svnStatus.js'
15
15
 
16
16
  /** 动作执行上下文(各入口把自身能力投影进来)。 */
17
17
  export interface SvnActionRunCtx {
@@ -29,6 +29,10 @@ export interface SvnActionRunCtx {
29
29
  openSvnDiffRev?: (path: string, revision: number) => void
30
30
  /** 打开日志弹窗(log 动作)。 */
31
31
  openSvnLog?: (path?: string) => void
32
+ /** 打开补丁对话框(create-patch 动作;执行器完成 RPC 后把载荷交回 UI)。 */
33
+ openPatchDialog?: (payload: { path: string; text: string; truncated: boolean; binary: boolean }) => void
34
+ /** 打开目录对比对话框(diff-summarize 动作;目标目录相对路径,'' = 工作副本根)。 */
35
+ openSumDialog?: (path: string) => void
32
36
  /** 重查变更清单(写操作后)。 */
33
37
  refreshChanges?: () => void
34
38
  /** 就地打开文件(日志面板点击变更文件)。 */
@@ -92,6 +96,19 @@ export const SVN_ACTION_RUNNERS: Record<string, SvnActionRunner> = {
92
96
  }, log: (ctx) => {
93
97
  ctx.openSvnLog?.(ctx.path || undefined)
94
98
  },
99
+ // W2-1:补丁生成(只读,仅生成不落盘应用);RPC 后把载荷交给 UI 对话框
100
+ 'create-patch': (ctx) => {
101
+ if (!ctx.path) { ctx.notify?.('无活动文件'); return }
102
+ const target = ctx.path
103
+ void svnPatchText(ctx.sessionId, target).then((outcome) => {
104
+ if (!outcome.ok || typeof outcome.text !== 'string') { ctx.notify?.(outcome.message); return }
105
+ ctx.openPatchDialog?.({ path: target, text: outcome.text, truncated: outcome.truncated === true, binary: outcome.binary === true })
106
+ })
107
+ },
108
+ // W2-2:目录对比(RPC 在对话框内做,执行器只负责打开并传入默认目标目录)
109
+ 'diff-summarize': (ctx) => {
110
+ ctx.openSumDialog?.(ctx.path ?? '')
111
+ },
95
112
  cleanup: (ctx) => {
96
113
  const options = ctx.cleanupOptions ?? {}
97
114
  const destructive = options.removeUnversioned === true || options.removeIgnored === true
@@ -258,7 +258,7 @@ export async function svnDiffRev(sessionId: string | undefined, path: string, re
258
258
  try {
259
259
  const res = await rpc('svn.diffRev', { sessionId, path, revision })
260
260
  if (!res.ok) return { ok: false, left: null, right: null, error: res.error }
261
- return { ok: true, left: res.left ?? null, right: res.right ?? null, reason: res.reason, error: res.error }
261
+ return { ok: true, left: res.left ?? null, right: res.right ?? null, reason: res.reason, error: res.error, binary: res.binary === true, encodingHint: res.encodingHint === true }
262
262
  } catch (error) {
263
263
  return { ok: false, left: null, right: null, error: String(error) }
264
264
  }
@@ -278,7 +278,7 @@ export async function svnDiffPair(sessionId: string | undefined, path: string, r
278
278
  try {
279
279
  const res = await rpc('svn.diffPair', { sessionId, path, revA, revB })
280
280
  if (!res.ok) return { ok: false, left: null, right: null, error: res.error }
281
- return { ok: true, left: res.left ?? null, right: res.right ?? null, reason: res.reason, error: res.error }
281
+ return { ok: true, left: res.left ?? null, right: res.right ?? null, reason: res.reason, error: res.error, binary: res.binary === true, encodingHint: res.encodingHint === true }
282
282
  } catch (error) {
283
283
  return { ok: false, left: null, right: null, error: String(error) }
284
284
  }
@@ -297,7 +297,7 @@ export async function svnDiffWorking(sessionId: string | undefined, path: string
297
297
  try {
298
298
  const res = await rpc('svn.diffWorking', { sessionId, path, revision })
299
299
  if (!res.ok) return { ok: false, left: null, right: null, error: res.error }
300
- return { ok: true, left: res.left ?? null, right: res.right ?? null, reason: res.reason, error: res.error }
300
+ return { ok: true, left: res.left ?? null, right: res.right ?? null, reason: res.reason, error: res.error, binary: res.binary === true, encodingHint: res.encodingHint === true }
301
301
  } catch (error) {
302
302
  return { ok: false, left: null, right: null, error: String(error) }
303
303
  }
@@ -211,6 +211,10 @@ export interface SvnDiffBaseOutcome {
211
211
  reason?: string
212
212
  /** 失败提示文案(ok=false,或 ok=true 时的补充说明)。 */
213
213
  message: string
214
+ /** W2-6 护栏:内容含 NUL(二进制)。 */
215
+ binary?: boolean
216
+ /** W2-6 护栏:内容可能非 UTF-8。 */
217
+ encodingHint?: boolean
214
218
  }
215
219
 
216
220
  /**
@@ -228,9 +232,9 @@ export async function svnDiffBase(sessionId: string | undefined, path: string):
228
232
  const message = res.reason === 'no-pristine'
229
233
  ? '该文件尚无基线版本(未提交的新增文件)'
230
234
  : '读取基线失败' + (res.error ? ':' + res.error : '')
231
- return { ok: true, base: null, working: res.working, message }
235
+ return { ok: true, base: null, working: res.working, message, binary: res.binary === true, encodingHint: res.encodingHint === true }
232
236
  }
233
- return { ok: true, base: res.base, working: res.working, message: '已打开基线差异' }
237
+ return { ok: true, base: res.base, working: res.working, message: '已打开基线差异', binary: res.binary === true, encodingHint: res.encodingHint === true }
234
238
  } catch (error) {
235
239
  return { ok: false, base: null, working: '', message: '读取基线失败:' + String(error) }
236
240
  }
@@ -270,6 +274,140 @@ export async function svnAdd(sessionId: string | undefined, paths: string[]): Pr
270
274
  }
271
275
  }
272
276
 
277
+ // --region W2 差异管理补全(补丁/目录对比/远端检查/冲突/配对)
278
+
279
+ /**
280
+ * 生成统一格式补丁文本(W2-1,只读不落盘)。
281
+ * @author ddj 2026年09月20号
282
+ * @param sessionId 会话 id
283
+ * @param path 工作区相对路径(文件/目录)
284
+ * @param opts 扩展选项(空白语义/EOL/上下文行数;缺省 = svn 默认)
285
+ * @returns 补丁文本 + 截断/二进制标记,或错误文案
286
+ */
287
+ export async function svnPatchText(
288
+ sessionId: string | undefined,
289
+ path: string,
290
+ opts: { whitespace?: 'none' | 'b' | 'w'; ignoreEol?: boolean; unified?: number } = {},
291
+ ): Promise<{ ok: boolean; text?: string; truncated?: boolean; binary?: boolean; message: string }> {
292
+ try {
293
+ const res = await rpc('svn.patchText', { sessionId, path, ...opts })
294
+ if (!res.ok) return { ok: false, message: '生成补丁失败:' + res.error }
295
+ return { ok: true, text: res.text, truncated: res.truncated, binary: res.binary, message: '已生成补丁' }
296
+ } catch (error) {
297
+ return { ok: false, message: '生成补丁失败:' + String(error) }
298
+ }
299
+ }
300
+
301
+ /**
302
+ * 目录对比(W2-2,任意两修订的 summarize 变更列表)。
303
+ * @author ddj 2026年09月20号
304
+ * @param sessionId 会话 id
305
+ * @param revA 起始修订(较早)
306
+ * @param revB 结束修订(较晚)
307
+ * @param path 目标相对路径('' = 工作副本根)
308
+ * @returns 变更条目或错误文案
309
+ */
310
+ export async function svnDiffSum(
311
+ sessionId: string | undefined,
312
+ revA: number,
313
+ revB: number,
314
+ path = '',
315
+ ): Promise<{ ok: boolean; entries?: Array<{ path: string; item: string; kind: string; props: string }>; message: string }> {
316
+ try {
317
+ const res = await rpc('svn.diffSum', { sessionId, path, revA, revB })
318
+ if (!res.ok) return { ok: false, message: '目录对比失败:' + res.error }
319
+ return { ok: true, entries: res.entries, message: '已读取 ' + res.entries.length + ' 条变更' }
320
+ } catch (error) {
321
+ return { ok: false, message: '目录对比失败:' + String(error) }
322
+ }
323
+ }
324
+
325
+ /**
326
+ * 远端更新检查(W2-3,`svn status -u`;host 侧 15s 短超时,离线/超时走本函数错误分支)。
327
+ * @author ddj 2026年09月20号
328
+ * @param sessionId 会话 id
329
+ * @param path 目标相对路径('' = 工作副本根)
330
+ * @returns 落后条目 + against 修订,或错误文案
331
+ */
332
+ export async function svnRemoteStatus(
333
+ sessionId: string | undefined,
334
+ path = '',
335
+ ): Promise<{ ok: boolean; outdated?: Array<{ path: string; item: string }>; againstRev?: number | null; message: string }> {
336
+ try {
337
+ const res = await rpc('svn.remoteStatus', { sessionId, path })
338
+ if (!res.ok) return { ok: false, message: '远端检查失败:' + res.error }
339
+ return { ok: true, outdated: res.outdated, againstRev: res.againstRev, message: '远端检查完成' }
340
+ } catch (error) {
341
+ return { ok: false, message: '远端检查失败:' + String(error) }
342
+ }
343
+ }
344
+
345
+ /**
346
+ * 扫描冲突副本文件(W2-4,`.mine`/`.working`/`.rN`,纯本地只读)。
347
+ * @author ddj 2026年09月20号
348
+ * @param sessionId 会话 id
349
+ * @param path 冲突文件的工作区相对路径
350
+ * @returns 副本清单或错误文案
351
+ */
352
+ export async function svnConflictArtifacts(
353
+ sessionId: string | undefined,
354
+ path: string,
355
+ ): Promise<{ ok: boolean; artifacts?: Array<{ path: string; name: string; kind: 'mine' | 'working' | 'rev'; rev?: number }>; message: string }> {
356
+ try {
357
+ const res = await rpc('svn.conflictArtifacts', { sessionId, path })
358
+ if (!res.ok) return { ok: false, message: '扫描冲突副本失败:' + res.error }
359
+ return { ok: true, artifacts: res.artifacts, message: '发现 ' + res.artifacts.length + ' 个冲突副本' }
360
+ } catch (error) {
361
+ return { ok: false, message: '扫描冲突副本失败:' + String(error) }
362
+ }
363
+ }
364
+
365
+ /**
366
+ * 双侧本地文件差异(W2-4,冲突副本 `.rN` ↔ `.mine` 对比)。
367
+ * @author ddj 2026年09月20号
368
+ * @param sessionId 会话 id
369
+ * @param left 左侧工作区相对路径
370
+ * @param right 右侧工作区相对路径
371
+ * @returns 双侧内容(含护栏标记)或错误文案
372
+ */
373
+ export async function svnDiffLocalPair(
374
+ sessionId: string | undefined,
375
+ left: string,
376
+ right: string,
377
+ ): Promise<{ ok: boolean; leftText?: string | null; rightText?: string | null; binary?: boolean; encodingHint?: boolean; message: string }> {
378
+ try {
379
+ const res = await rpc('svn.diffLocalPair', { sessionId, left, right })
380
+ if (!res.ok) return { ok: false, message: '读取冲突副本失败:' + res.error }
381
+ return { ok: true, leftText: res.left, rightText: res.right, binary: res.binary === true, encodingHint: res.encodingHint === true, message: '已读取双侧内容' }
382
+ } catch (error) {
383
+ return { ok: false, message: '读取冲突副本失败:' + String(error) }
384
+ }
385
+ }
386
+
387
+ /**
388
+ * 批量取文件大小(W2-5,改名配对启发式用;未知/失败按 null)。
389
+ * @author ddj 2026年09月20号
390
+ * @param sessionId 会话 id
391
+ * @param paths 工作区相对路径列表(host 上限 200)
392
+ * @returns 路径 → 大小表
393
+ */
394
+ export async function svnFileSizes(
395
+ sessionId: string | undefined,
396
+ paths: string[],
397
+ ): Promise<Record<string, number | null>> {
398
+ try {
399
+ const res = await rpc('svn.fileSizes', { sessionId, paths })
400
+ if (!res.ok) return {}
401
+ const table: Record<string, number | null> = {}
402
+ for (const item of res.sizes) table[item.path] = item.size
403
+ return table
404
+ } catch (error) {
405
+ return {}
406
+ }
407
+ }
408
+
409
+ // --endregion
410
+
273
411
  /**
274
412
  * Monaco 右键菜单 SVN 动作条目(编辑区动态注册用)。
275
413
  *
@@ -255,6 +255,60 @@ export function pickActive(tabs: TabLike[], wanted: unknown): string | null {
255
255
  }
256
256
  // --endregion
257
257
 
258
+ // --region 上限淘汰
259
+
260
+ /** 淘汰判定输入:活动页签、使用序、额外保留集。 */
261
+ export interface EvictOptions {
262
+ /** 当前活动页签路径(**绝不被淘汰**)。 */
263
+ active: string | null
264
+ /** 路径 → 最近使用序号(单调递增;越大越新)。缺记录按最久未用处理。 */
265
+ used: Record<string, number>
266
+ /** 额外保留集(调用方按需保护,如「恢复中刚载入的页签」)。 */
267
+ keep?: ReadonlySet<string>
268
+ }
269
+
270
+ /**
271
+ * 计算超限时应淘汰的页签路径(最久未使用者优先)。
272
+ *
273
+ * 保护规则(不满足则允许溢出而非强关,调用方据此保持「溢出态」:
274
+ * 用户显式固定过、或正在查看的页签,都不该被上限悄悄夺走):
275
+ * - `limit <= 0` → 上限关闭,返回空数组;
276
+ * - 未超限 → 返回空数组;
277
+ * - 固定页签(`pinned === true`)不是候选;
278
+ * - 活动页签不是候选;
279
+ * - `keep` 命中的路径不是候选。
280
+ *
281
+ * 脏页签**仍是候选**:其未保存内容的落盘由调用方在关闭前完成(EditorView 走
282
+ * closeTabs → persistDirty),本函数不感知磁盘,避免把 IO 语义掺进纯判定。
283
+ *
284
+ * @author ddj 2026年09月18号
285
+ * @param tabs 当前页签
286
+ * @param limit 页签上限(0 = 不限制)
287
+ * @param options 活动页签、使用序与额外保留集
288
+ * @returns 待淘汰路径(按最久未用在前);无需淘汰时为 `[]`
289
+ */
290
+ export function evictPlan(tabs: TabLike[], limit: number, options: EvictOptions): string[] {
291
+ if (!Array.isArray(tabs) || !Number.isFinite(limit) || limit <= 0) return []
292
+ const excess = tabs.length - Math.floor(limit)
293
+ if (excess <= 0) return []
294
+ const active = options?.active ?? null
295
+ const used = options?.used ?? {}
296
+ const keep = options?.keep
297
+ const candidates: Array<{ path: string; at: number; at0: number }> = []
298
+ for (let i = 0; i < tabs.length; i += 1) {
299
+ const tab = tabs[i]
300
+ if (tab.pinned === true) continue
301
+ if (tab.path === active) continue
302
+ if (keep?.has(tab.path) === true) continue
303
+ const order = used[tab.path]
304
+ candidates.push({ path: tab.path, at: Number.isFinite(order) ? order : -1, at0: i })
305
+ }
306
+ // 使用序升序 = 最久未用在前;同序(含全部无记录)按页签原下标,保证确定性
307
+ candidates.sort((a, b) => (a.at === b.at ? a.at0 - b.at0 : a.at - b.at))
308
+ return candidates.slice(0, excess).map((item) => item.path)
309
+ }
310
+ // --endregion
311
+
258
312
  // --region 路径推导
259
313
 
260
314
  /** Windows 盘符 / UNC / POSIX 根:视为工作区外的绝对路径。 */