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
@@ -11,6 +11,7 @@ import '../styles/mcp.css'
11
11
  import { availableOpeners, AUTO_OPEN_TOOL } from '../fileOpeners.js'
12
12
  import { SettingsContext } from '../settingsContext.js'
13
13
  import { normalizeSidebarMinWidth, SIDEBAR_MIN_DEFAULT } from '../sidebarMin.js'
14
+ import { EDITOR_LIMIT_CEIL, EDITOR_LIMIT_DEFAULT, normalizeMaxOpenEditors } from '../../shared/editorLimit.js'
14
15
  import { TORTOISE_DIR_DEFAULT } from '../../shared/svn.js'
15
16
  import { KeybindingsPanel } from './KeybindingsPanel.js'
16
17
  import { LspSettings } from './LspSettings.js'
@@ -136,6 +137,8 @@ function GeneralSettings({ registry }) {
136
137
  const [devMessage, setDevMessage] = React.useState('')
137
138
  const [minW, setMinW] = React.useState(SIDEBAR_MIN_DEFAULT)
138
139
  const [minDraft, setMinDraft] = React.useState(null) // 输入草稿(null=跟随已保存值;blur/Enter 提交)
140
+ const [limit, setLimit] = React.useState(EDITOR_LIMIT_DEFAULT)
141
+ const [limitDraft, setLimitDraft] = React.useState(null) // 页签上限草稿(同上提交语义)
139
142
  const settings = React.useContext(SettingsContext)
140
143
  const snapshot = settings?.getSnapshot?.()
141
144
  const loading = !snapshot || snapshot.status === 'loading'
@@ -147,6 +150,7 @@ function GeneralSettings({ registry }) {
147
150
  const next = snap?.value?.fileOpenTool
148
151
  if (typeof next === 'string') setTool(next)
149
152
  setMinW(normalizeSidebarMinWidth(snap?.value?.sidebarMinWidth))
153
+ setLimit(normalizeMaxOpenEditors(snap?.value?.maxOpenEditors))
150
154
  }
151
155
  onChange()
152
156
  return settings?.subscribe?.(onChange)
@@ -174,6 +178,19 @@ function GeneralSettings({ registry }) {
174
178
  setMinDraft(null)
175
179
  saveMinW(minDraft)
176
180
  }
181
+ /** 提交页签数量上限(归一化夹取 0–50 后持久化;0 = 不限制)。 */
182
+ const saveLimit = (raw) => {
183
+ const next = normalizeMaxOpenEditors(raw)
184
+ setLimit(next); setBusy(true); setError('')
185
+ if (!settings?.set) { setError('设置服务不可用'); setBusy(false); return }
186
+ settings.set('maxOpenEditors', next).catch((e) => setError(String(e))).finally(() => setBusy(false))
187
+ }
188
+ /** 结束输入(blur/Enter)时提交页签上限草稿。 */
189
+ const commitLimit = () => {
190
+ if (limitDraft === null) return
191
+ setLimitDraft(null)
192
+ saveLimit(limitDraft)
193
+ }
177
194
  const closeDevForm = () => {
178
195
  if (!window.confirm('关闭开发形态:插件将切换为正式版安装(版本依赖 + 删除工作区链接),pnpm 装配后需重启 DSH 生效。确认关闭?')) return
179
196
  setDevBusy(true)
@@ -220,6 +237,18 @@ function GeneralSettings({ registry }) {
220
237
  onKeyDown: (event) => { if (event.key === 'Enter') commitMinW() },
221
238
  }),
222
239
  React.createElement('small', null, '180–560 px;拖拽低于该宽度自动隐藏')),
240
+ React.createElement('label', { className: 'vsm-general-row' },
241
+ React.createElement('span', null, '页签数量上限'),
242
+ React.createElement('input', {
243
+ type: 'number', min: 0, max: EDITOR_LIMIT_CEIL, step: 1,
244
+ value: limitDraft ?? limit,
245
+ disabled: loading || unavailable || notReady || busy || snapshot?.writable === false,
246
+ title: '0 = 不限制;超出上限时自动关闭最久未使用的页签(固定页签不受影响)',
247
+ onChange: (event) => setLimitDraft(event.target.value),
248
+ onBlur: commitLimit,
249
+ onKeyDown: (event) => { if (event.key === 'Enter') commitLimit() },
250
+ }),
251
+ React.createElement('small', null, '0 = 不限制;超限时关闭最久未使用的页签(固定页签除外)')),
223
252
  ),
224
253
  ),
225
254
  React.createElement(SvnSettingsSection, null),
@@ -53,51 +53,195 @@ function modelFor(monaco, uri, text, language, created) {
53
53
  }
54
54
 
55
55
  /**
56
- * 挂载只读并排差异编辑器;返回卸载函数。
57
- * @author ddj 2026年09月16号
58
- * @param props.host 宿主 DOM 节点
59
- * @param props.monaco Monaco 实例
60
- * @param props.path 文件路径(决定语法高亮与 model URI)
61
- * @param props.left 左侧内容(null 表示该侧无内容)
62
- * @param props.right 右侧内容
63
- * @param props.scheme model URI scheme 前缀(区分不同来源,避免跨视图复用串内容)
64
- * @returns 卸载函数
56
+ * 差异视图选项(W1-1:选项条驱动的非破坏视图开关)。
65
57
  */
66
- export function mountSideBySide(props) {
67
- const { host, monaco, path, left, right, scheme = 'edrv-diff' } = props
68
- const created = []
69
- const language = langOf(path)
70
- const leftModel = modelFor(monaco, uriOf(scheme, path, 'left'), left ?? '', language, created)
71
- const rightModel = modelFor(monaco, uriOf(scheme, path, 'right'), right ?? '', language, created)
72
- const diff = monaco.editor.createDiffEditor(host, {
73
- theme: themeNameOf(),
58
+ export interface DiffViewOpts {
59
+ /** 忽略行尾空白差异(Monaco 默认 true;关掉后行尾空格增删会算差异)。 */
60
+ trimWhitespace?: boolean
61
+ /** 折叠未变更区域(仅显示差异及其上下文;Monaco vendor 已支持,默认关)。 */
62
+ hideUnchanged?: boolean
63
+ /** 喂 model 前 EOL 规范化(CRLF/CR → LF;仅影响展示,不改变文件)。 */
64
+ eolNormalize?: boolean
65
+ }
66
+
67
+ /** 选项条默认值(与 Monaco 默认一致:不勾选时现状行为不变)。 */
68
+ export const DEFAULT_DIFF_VIEW_OPTS = { trimWhitespace: true, hideUnchanged: false, eolNormalize: false }
69
+
70
+ /**
71
+ * 比较前 EOL 规范化(W1-1):CRLF/CR → LF。混合换行会让 Monaco 把每行都判差;
72
+ * 规范化不改变行数,顶部跳转与统计行号不受影响。
73
+ * @author ddj 2026年09月20号
74
+ * @param text 原始文本
75
+ * @returns 规范化后的文本(非字符串原样返回)
76
+ */
77
+ export function normalizeEolOf(text) {
78
+ return typeof text === 'string' ? text.replace(/\r\n?/g, '\n') : text
79
+ }
80
+
81
+ /**
82
+ * 差异行数统计(W1-3):从 getLineChanges 清单聚合 +新增/修改行 与 −删除/修改行
83
+ * (git diff --stat 口径:修改行两侧各计;纯增删只计存在的一侧)。
84
+ * @author ddj 2026年09月20号
85
+ * @param changes Monaco diff 的行变更清单(ILineChange[])
86
+ * @returns {{ added: number, deleted: number }} 行数统计
87
+ */
88
+ export function diffStatsOf(changes) {
89
+ let added = 0
90
+ let deleted = 0
91
+ for (const change of changes || []) {
92
+ const origStart = Number(change?.originalStartLineNumber) || 1
93
+ const origEnd = Number(change?.originalEndLineNumber) || 0
94
+ const modStart = Number(change?.modifiedStartLineNumber) || 1
95
+ const modEnd = Number(change?.modifiedEndLineNumber) || 0
96
+ if (origEnd >= origStart) deleted += origEnd - origStart + 1
97
+ if (modEnd >= modStart) added += modEnd - modStart + 1
98
+ }
99
+ return { added, deleted }
100
+ }
101
+
102
+ /**
103
+ * 构造只读并排差异编辑器选项(集中一处,供单测守护)。
104
+ * 差异视觉三要素:renderIndicators 行首 +/- 指示符、lineDecorationsWidth 给指示符留足宽度
105
+ * (8px 会把 16px 的 codicon 裁没)、renderOverviewRuler 滚动条变化色标。
106
+ * W1-1 增补视图开关:缺省行为与 Monaco 默认一致(行尾空白忽略开、折叠未变更关)。
107
+ * @author ddj 2026年09月18号 / 2026年09月20号
108
+ * @param themeName Monaco 主题 id
109
+ * @param viewOpts 差异视图选项(缺省 = 默认视图行为)
110
+ * @returns createDiffEditor 选项对象
111
+ */
112
+ export function diffOptsOf(themeName, viewOpts) {
113
+ return {
114
+ theme: themeName,
74
115
  readOnly: true,
75
116
  originalEditable: false,
76
117
  renderSideBySide: true,
118
+ renderIndicators: true,
119
+ lineDecorationsWidth: 22,
77
120
  // 只读审阅:不要 minimap/字形栏等编辑态附属物
78
121
  minimap: { enabled: false },
79
122
  glyphMargin: false,
80
- lineDecorationsWidth: 8,
81
123
  scrollBeyondLastLine: false,
82
124
  automaticLayout: true,
83
- renderOverviewRuler: false,
125
+ renderOverviewRuler: true,
84
126
  wordWrap: 'off',
85
127
  fontSize: 13,
86
128
  lineHeight: 20,
87
129
  padding: { top: 6 },
88
- })
130
+ // W1-1 视图开关:行尾空白忽略 + 折叠未变更区(对象缺省字段回落 Monaco 内建默认值)
131
+ ignoreTrimWhitespace: !viewOpts || viewOpts.trimWhitespace !== false,
132
+ hideUnchangedRegions: { enabled: Boolean(viewOpts && viewOpts.hideUnchanged === true) },
133
+ }
134
+ }
135
+
136
+ /**
137
+ * 挂载只读并排差异编辑器;返回 { diff, dispose }(diff 供顶部快速跳转使用)。
138
+ * @author ddj 2026年09月16号 / 2026年09月18号 / 2026年09月20号
139
+ * @param props.host 宿主 DOM 节点
140
+ * @param props.monaco Monaco 实例
141
+ * @param props.path 文件路径(决定语法高亮与 model URI)
142
+ * @param props.left 左侧内容(null 表示该侧无内容)
143
+ * @param props.right 右侧内容
144
+ * @param props.scheme model URI scheme 前缀(区分不同来源,避免跨视图复用串内容)
145
+ * @param props.viewOpts 差异视图选项(W1-1:EOL 规范化/行尾空白/折叠未变更)
146
+ * @returns {{ diff: object, dispose: () => void }} diff 编辑器实例与卸载函数
147
+ */
148
+ export function mountSideBySide(props) {
149
+ const { host, monaco, path, left, right, scheme = 'edrv-diff', viewOpts } = props
150
+ const created = []
151
+ const language = langOf(path)
152
+ // W1-1:EOL 规范化开关只影响喂给 model 的展示文本(不落盘),行数不变故导航不受影响
153
+ const eolOn = viewOpts?.eolNormalize === true
154
+ const leftText = eolOn ? normalizeEolOf(left ?? '') : (left ?? '')
155
+ const rightText = eolOn ? normalizeEolOf(right ?? '') : (right ?? '')
156
+ const leftModel = modelFor(monaco, uriOf(scheme, path, 'left'), leftText, language, created)
157
+ const rightModel = modelFor(monaco, uriOf(scheme, path, 'right'), rightText, language, created)
158
+ const diff = monaco.editor.createDiffEditor(host, diffOptsOf(themeNameOf(), viewOpts))
89
159
  diff.setModel({ original: leftModel, modified: rightModel })
90
- return () => {
91
- try { diff.dispose() } catch (error) { /* 已随宿主卸载 */ }
92
- for (const model of created) {
93
- try { model.dispose() } catch (error) { /* 已被其他持有者销毁 */ }
94
- }
160
+ return {
161
+ diff,
162
+ dispose: () => {
163
+ try { diff.dispose() } catch (error) { /* 已随宿主卸载 */ }
164
+ for (const model of created) {
165
+ try { model.dispose() } catch (error) { /* 已被其他持有者销毁 */ }
166
+ }
167
+ },
95
168
  }
96
169
  }
97
170
 
171
+ /**
172
+ * 计算差异跳转目标侧与行区间(有修改侧行走修改侧;纯删除/空修改落原侧;
173
+ * inline 单栏模式下原侧是折叠残留,纯删除改为滚到修改侧的删除块锚点行)。
174
+ * @author ddj 2026年09月18号
175
+ * @param change getLineChanges 的单条(original/modified 的 Start/EndLineNumber)
176
+ * @param inline 是否 inline 单栏模式(缺省按并排处理)
177
+ * @returns {{ side: string, start: number, end: number }} side ∈ 'modified' | 'original'
178
+ */
179
+ export function navTargetOf(change, inline) {
180
+ const modStart = Number(change?.modifiedStartLineNumber) || 0
181
+ const modEnd = Number(change?.modifiedEndLineNumber) || 0
182
+ if (modEnd > 0 && modEnd >= modStart) return { side: 'modified', start: Math.max(1, modStart), end: modEnd }
183
+ if (inline) {
184
+ const anchor = Math.max(1, modStart)
185
+ return { side: 'modified', start: anchor, end: anchor }
186
+ }
187
+ const origStart = Math.max(1, Number(change?.originalStartLineNumber) || 1)
188
+ const origEnd = Math.max(origStart, Number(change?.originalEndLineNumber) || origStart)
189
+ return { side: 'original', start: origStart, end: origEnd }
190
+ }
191
+
192
+ /**
193
+ * 渲染差异条快速跳转簇(↑ i/N ↓;无差异时 0/0 且禁用,形态对齐自研审查条)。
194
+ * @author ddj 2026年09月18号
195
+ * @param nav 导航状态 { list: 差异清单, idx: 当前下标 }
196
+ * @param onStep 步进回调(传入 ±1)
197
+ * @returns 导航簇元素
198
+ */
199
+ function navClusterEl(nav, onStep) {
200
+ const total = nav.list.length
201
+ return React.createElement('span', { className: 'edrv-svndiff-nav' },
202
+ React.createElement('button', { className: 'edrv-pill edrv-pill-ghost', title: '上一个差异', disabled: !total, onClick: () => onStep(-1) }, '↑'),
203
+ React.createElement('span', { className: 'edrv-svndiff-nav-count', title: '差异位置' }, total ? (nav.idx + 1) + '/' + total : '0/0'),
204
+ React.createElement('button', { className: 'edrv-pill edrv-pill-ghost', title: '下一个差异', disabled: !total, onClick: () => onStep(1) }, '↓'))
205
+ }
206
+
207
+ /**
208
+ * 选项条开关元素(W1-1:复用面板 checkbox 形态,紧凑放置差异条右端)。
209
+ * @author ddj 2026年09月20号
210
+ * @param key DiffViewOpts 键(trimWhitespace/hideUnchanged/eolNormalize)
211
+ * @param label 开关文案
212
+ * @param title tooltip 说明
213
+ * @param viewOpts 当前选项
214
+ * @param onToggle 切换回调(传入 key)
215
+ * @returns label 元素
216
+ */
217
+ function optCheckEl(key, label, title, viewOpts, onToggle) {
218
+ return React.createElement('label', { className: 'edrv-svn-check edrv-svndiff-opt', title },
219
+ React.createElement('input', {
220
+ type: 'checkbox',
221
+ checked: Boolean(viewOpts[key]),
222
+ onChange: () => onToggle(key),
223
+ }), label)
224
+ }
225
+
226
+ /**
227
+ * 差异统计徽标元素(W1-3:+新增/修改 −删除/修改;无差异时不渲染)。
228
+ * @author ddj 2026年09月20号
229
+ * @param stats diffStatsOf 结果
230
+ * @returns 元素或 null
231
+ */
232
+ function statsEl(stats) {
233
+ if (!stats.added && !stats.deleted) return null
234
+ return React.createElement('span', {
235
+ className: 'edrv-svndiff-stat',
236
+ title: '差异行数:+新增/修改行 −删除/修改行(git diff --stat 口径)',
237
+ },
238
+ React.createElement('span', { className: 'edrv-svndiff-stat-add' }, '+' + stats.added),
239
+ React.createElement('span', { className: 'edrv-svndiff-stat-del' }, '-' + stats.deleted))
240
+ }
241
+
98
242
  /**
99
243
  * 只读并排差异视图(编辑器主体内的一个分支,非弹窗)。
100
- * @author ddj 2026年09月16号
244
+ * @author ddj 2026年09月16号 / 2026年09月20号
101
245
  * @param props.monaco Monaco 实例(未就绪为 null,显示加载态)
102
246
  * @param props.path 文件路径
103
247
  * @param props.left 左侧内容(null = 无内容)
@@ -108,30 +252,71 @@ export function mountSideBySide(props) {
108
252
  * @param props.emptyNote 左侧无内容时的说明
109
253
  * @param props.message 补充说明
110
254
  * @param props.scheme model URI scheme 前缀
255
+ * @param props.onBack 返回来源视图的回调(提供时渲染「← 返回日志」按钮)
111
256
  * @param props.onClose 关闭回调
112
257
  * @returns 差异视图元素
113
258
  */
114
259
  export function SideBySideDiff(props) {
115
260
  const {
116
- monaco, path, left, right, title, leftLabel, rightLabel, emptyNote, message, scheme, onClose,
261
+ monaco, path, left, right, title, leftLabel, rightLabel, emptyNote, message, scheme, onBack, onClose,
117
262
  } = props
118
263
  const hostRef = React.useRef(null)
264
+ const diffRef = React.useRef(null)
265
+ const [nav, setNav] = React.useState({ list: [], idx: 0 })
266
+ // W1-1 视图选项:会话内状态(不持久化),切换经 mount/dispose 链重建编辑器
267
+ const [viewOpts, setViewOpts] = React.useState(DEFAULT_DIFF_VIEW_OPTS)
119
268
 
120
269
  React.useEffect(() => {
121
270
  const host = hostRef.current
122
271
  if (!monaco || !host) return undefined
123
- return mountSideBySide({ host, monaco, path, left, right, scheme })
124
- }, [monaco, path, left, right, scheme])
272
+ const mounted = mountSideBySide({ host, monaco, path, left, right, scheme, viewOpts })
273
+ diffRef.current = mounted.diff
274
+ // diff 计算完成后取变更清单,驱动顶部 ↑ i/N ↓ 快速跳转
275
+ const listener = mounted.diff.onDidUpdateDiff(() => {
276
+ const list = mounted.diff.getLineChanges() || []
277
+ setNav((prev) => ({ list, idx: Math.min(prev.idx, Math.max(0, list.length - 1)) }))
278
+ })
279
+ return () => {
280
+ try { listener.dispose() } catch (error) { /* 随 diff 一并销毁 */ }
281
+ diffRef.current = null
282
+ mounted.dispose()
283
+ }
284
+ }, [monaco, path, left, right, scheme, viewOpts])
285
+
286
+ /** 切换一个视图选项开关(W1-1)。 */
287
+ const toggleOpt = (key) => setViewOpts((prev) => ({ ...prev, [key]: !prev[key] }))
288
+
289
+ /** 顶部快速跳转:按 ±1 循环步进并滚动到对应差异处(inline 模式纯删除锚到修改侧)。 */
290
+ const stepDiff = (delta) => {
291
+ const diff = diffRef.current
292
+ const total = nav.list.length
293
+ if (!diff || !total) return
294
+ const idx = (nav.idx + delta + total) % total
295
+ setNav({ list: nav.list, idx })
296
+ // DiffEditor 无 getDomNode:从自家宿主节点取 widget 根,按 side-by-side 类判定模式
297
+ const diffRoot = hostRef.current?.querySelector('.monaco-diff-editor')
298
+ const inline = diffRoot ? !diffRoot.classList.contains('side-by-side') : false
299
+ const target = navTargetOf(nav.list[idx], inline)
300
+ const editor = target.side === 'modified' ? diff.getModifiedEditor() : diff.getOriginalEditor()
301
+ try { editor.revealLinesInCenter(target.start, target.end) } catch (error) { /* 区间异常忽略 */ }
302
+ }
125
303
 
126
304
  const noLeft = left === null || left === undefined
127
305
  const noRight = right === null || right === undefined
128
306
 
129
307
  return React.createElement('div', { className: 'edrv-svndiff' },
130
308
  React.createElement('div', { className: 'edrv-svndiff-bar' },
309
+ (onBack ? React.createElement('button', { className: 'edrv-pill edrv-pill-ghost', title: '返回 SVN 日志', onClick: onBack }, '← 返回日志') : null),
310
+ navClusterEl(nav, stepDiff),
311
+ statsEl(diffStatsOf(nav.list)),
131
312
  React.createElement('span', { className: 'edrv-svndiff-title', title: title ?? path }, String(title ?? path ?? '')),
132
313
  React.createElement('span', { className: 'edrv-svndiff-tag' }, (leftLabel || '左') + ' ↔ ' + (rightLabel || '右')),
133
314
  (message ? React.createElement('span', { className: 'edrv-svndiff-tag' }, String(message)) : null),
134
315
  React.createElement('span', { style: { flex: 1 } }),
316
+ React.createElement('div', { className: 'edrv-svndiff-opts' },
317
+ optCheckEl('trimWhitespace', '行尾空白', '忽略行尾空白差异(对齐 svn diff -x --ignore-space-at-eol;Monaco 默认开启)', viewOpts, toggleOpt),
318
+ optCheckEl('hideUnchanged', '折叠未变更', '折叠未变更区域,仅显示差异及其上下文', viewOpts, toggleOpt),
319
+ optCheckEl('eolNormalize', 'EOL 规范化', '比较前把 CRLF/CR 规范化为 LF(仅影响展示;混合换行文件整行标差时勾选)', viewOpts, toggleOpt)),
135
320
  React.createElement('button', { className: 'edrv-pill edrv-pill-ghost', title: '关闭差异视图', onClick: onClose }, '✕ 关闭')),
136
321
  ((noLeft && noRight) || !monaco
137
322
  ? React.createElement('div', { className: 'edrv-svndiff-empty' }, emptyNote || '无可比较内容')
@@ -24,27 +24,40 @@ const NO_PRESTINE_NOTE = '该文件尚无基线版本(新增未提交的文件
24
24
  * @param props.leftLabel 左侧标签(如 BASE / r12)
25
25
  * @param props.rightLabel 右侧标签(如 工作区 / r13)
26
26
  * @param props.scheme model URI scheme 前缀(区分不同来源,避免跨视图串内容)
27
+ * @param props.binary W2-6 护栏:内容含 NUL(二进制),true 时不渲染编辑器只显示说明
28
+ * @param props.encodingHint W2-6 护栏:内容可能非 UTF-8,显示乱码风险提示
29
+ * @param props.onBack 返回来源视图的回调(来自日志弹窗时由 EditorView 提供)
27
30
  * @param props.onClose 关闭回调
28
31
  * @returns 差异视图元素
29
32
  */
30
33
  export function SvnDiffPanel(props) {
31
- const { monaco, path, base, working, reason, message, leftLabel, rightLabel, onClose } = props
34
+ const { monaco, path, base, working, reason, message, leftLabel, rightLabel, onBack, onClose, binary, encodingHint } = props
32
35
  const noBase = typeof base !== 'string'
33
- const note = noBase
34
- ? (reason === 'no-pristine' ? NO_PRESTINE_NOTE : (reason === 'not-exist' ? '该版本尚无此文件,无可比较内容' : '读取基线失败'))
35
- : ''
36
+ // W2-6 护栏:二进制(NUL 嗅探)不渲染编辑器(M4 实测 cat 对二进制原样透传字节,灌进 Monaco 只会乱码);
37
+ // 编码提示照常渲染(U+FFFD 提示乱码风险,是否继续看由用户决定)
38
+ const blocked = binary === true
39
+ const guardNote = blocked
40
+ ? '二进制文件(嗅探到 NUL 字节),无法做文本并排对比'
41
+ : (encodingHint ? '内容可能非 UTF-8 编码,文本显示可能乱码' : '')
42
+ const note = blocked
43
+ ? guardNote
44
+ : (noBase
45
+ ? (reason === 'no-pristine' ? NO_PRESTINE_NOTE : (reason === 'not-exist' ? '该版本尚无此文件,无可比较内容' : '读取基线失败'))
46
+ : '')
47
+ const shownMessage = blocked ? '' : (encodingHint ? '内容可能非 UTF-8,显示可能乱码' + (message ? ';' + message : '') : message)
36
48
  return React.createElement(SideBySideDiff, {
37
49
  monaco,
38
50
  path,
39
- // 左侧无内容时传 ''(而非 null):仍展示编辑器,让用户看到右侧内容
40
- left: noBase ? '' : base,
41
- right: working,
51
+ // 左侧无内容/护栏拦截时传 ''(而非 null):仍展示差异框体,让用户看到说明
52
+ left: blocked ? '' : (noBase ? '' : base),
53
+ right: blocked ? '' : working,
42
54
  title: path,
43
55
  leftLabel: leftLabel || 'BASE',
44
56
  rightLabel: rightLabel || '工作区',
45
57
  emptyNote: note,
46
- message: noBase ? '' : message,
58
+ message: shownMessage,
47
59
  scheme: props.scheme || 'edrv-svn-base',
60
+ onBack,
48
61
  onClose,
49
62
  })
50
63
  }
@@ -29,6 +29,7 @@ import { ModalShell } from './ModalShell.js'
29
29
  import { ContextMenu } from './ContextMenu.js'
30
30
  import type { ContextMenuEntry } from './ContextMenu.js'
31
31
  import { SvnLogStats } from './SvnLogStats.js'
32
+ import { csvOf, downloadText, htmlTableOf } from './svnExport.js'
32
33
 
33
34
  /** 弹窗默认宽(P0-14 可拖拽调整;窄屏按视口 clamp)。 */
34
35
  const DIALOG_W_DEFAULT = 920
@@ -735,14 +736,30 @@ function statusLineOf(list, filtered, shownCount, keyword, truncated, current, s
735
736
  + (truncated ? ' · 可能还有更多' : ' · 已全部加载')
736
737
  }
737
738
 
739
+ /**
740
+ * 日志条目 → 导出行(W1-4):修订/作者/日期/提交信息/路径(relPath 优先,分号拼接)。
741
+ * @author ddj 2026年09月20号
742
+ * @param rows 日志条目数组
743
+ * @returns 二维字符串表(不含表头)
744
+ */
745
+ function logExportRows(rows) {
746
+ return (rows || []).map((entry) => [
747
+ 'r' + String(entry.revision ?? ''),
748
+ String(entry.author ?? ''),
749
+ String(entry.date ?? ''),
750
+ String(entry.message ?? ''),
751
+ (entry.paths || []).map((item) => item.relPath || item.path).join('; '),
752
+ ])
753
+ }
754
+
738
755
  /**
739
756
  * 底部(状态行 P0-11 + 勾选项 P0-5/P1-4/P1-5 + 按钮组 P0-6:Next 100 / Show All / Refresh)。
740
- * @author ddj 2026年09月17号 / 2026年09月18号
757
+ * @author ddj 2026年09月17号 / 2026年09月18号 / 2026年09月20号
741
758
  * @param props 见内部解构
742
759
  * @returns 底部元素
743
760
  */
744
761
  function LogFoot(props) {
745
- const { list, filtered, shownCount, keyword, truncated, current, selectedCount, onlyAffected, onOnlyAffected, busy, onLoadMore, onShowAll, onRefresh, onStats, stopOnCopy, onStopCopy, includeMerged, onIncludeMerged, rangeActive, onOpenRange, onRangeReset } = props
762
+ const { list, filtered, shownCount, keyword, truncated, current, selectedCount, onlyAffected, onOnlyAffected, busy, onLoadMore, onShowAll, onRefresh, onStats, stopOnCopy, onStopCopy, includeMerged, onIncludeMerged, rangeActive, onOpenRange, onRangeReset, onExportCsv, onExportHtml } = props
746
763
  const status = statusLineOf(list, filtered, shownCount, keyword, truncated, current, selectedCount)
747
764
  return React.createElement('div', { className: 'edrv-svnlog-footwrap' },
748
765
  React.createElement('div', { className: 'edrv-svnlog-foot' },
@@ -778,6 +795,16 @@ function LogFoot(props) {
778
795
  (rangeActive
779
796
  ? React.createElement('button', { className: 'edrv-svn-act', disabled: busy, title: '清除区间,回到默认 HEAD 往前的窗口', onClick: onRangeReset }, '回到最新')
780
797
  : null),
798
+ React.createElement('button', {
799
+ className: 'edrv-svn-act', disabled: busy || !filtered || !filtered.length,
800
+ title: '导出当前过滤后的修订列表为 CSV(带 BOM,Excel 直开;修订/作者/日期/信息/路径)',
801
+ onClick: onExportCsv,
802
+ }, '导出CSV'),
803
+ React.createElement('button', {
804
+ className: 'edrv-svn-act', disabled: busy || !filtered || !filtered.length,
805
+ title: '导出当前过滤后的修订列表为 HTML 报表(仅本地查看,不外发)',
806
+ onClick: onExportHtml,
807
+ }, '导出HTML'),
781
808
  React.createElement('span', { style: { flex: 1 } }),
782
809
  React.createElement('button', { className: 'edrv-svn-act', disabled: busy, title: '重新读取日志(F5)', onClick: onRefresh }, 'Refresh'),
783
810
  React.createElement('button', {
@@ -875,6 +902,20 @@ export function SvnLogDialog(props) {
875
902
  }
876
903
  const onSortReset = () => { setSortKey(null); setSortDir('desc') }
877
904
 
905
+ /**
906
+ * 导出当前过滤后列表(W1-4):CSV 带 BOM 供 Excel 直开 / HTML 本地报表;失败静默。
907
+ * @author ddj 2026年09月20号
908
+ * @param kind 导出格式('csv' | 'html')
909
+ */
910
+ const onExport = (kind) => {
911
+ if (!sorted || !sorted.length) return
912
+ const headers = ['修订', '作者', '日期', '信息', '路径']
913
+ const rows = logExportRows(sorted)
914
+ const stamp = new Date().toISOString().slice(0, 10)
915
+ if (kind === 'html') downloadText('svn-log-' + stamp + '.html', htmlTableOf(headers, rows, 'SVN 日志 ' + stamp), 'text/html')
916
+ else downloadText('svn-log-' + stamp + '.csv', csvOf([headers, ...rows]), 'text/csv')
917
+ }
918
+
878
919
  /**
879
920
  * 多选点击解释(P1-3):单击=单选;Ctrl=切换;Shift=以 anchor 为基准在显示列表内范围选。
880
921
  * anchor 缺失或不在显示列表时 Shift 回落单选(与选中项消失回落首条同策略)。
@@ -1080,6 +1121,7 @@ export function SvnLogDialog(props) {
1080
1121
  list, filtered: sorted, shownCount: sorted ? sorted.length : 0, keyword: query.trim(), truncated, current,
1081
1122
  selectedCount: visibleSelected.length, onlyAffected, onOnlyAffected: setOnlyAffected, busy, onLoadMore, onShowAll, onRefresh,
1082
1123
  onStats: () => setStatsOpen(true), stopOnCopy, onStopCopy, includeMerged, onIncludeMerged,
1124
+ onExportCsv: () => onExport('csv'), onExportHtml: () => onExport('html'),
1083
1125
  rangeActive: rangeActive === true, onOpenRange: () => setRangeOpen(true), onRangeReset: () => onRangeReset?.(),
1084
1126
  }),
1085
1127
  React.createElement(Grip, { mode: 'r', onDragStart: startResize, onReset: () => { setDlgW(DIALOG_W_DEFAULT); setDlgH(DIALOG_H_DEFAULT) } }),
@@ -0,0 +1,63 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * dsh-vscode-mode client — SVN 补丁预览对话框(W2-1 Create Patch)。
4
+ *
5
+ * 承载 `svn.patchText` 的产物:只读 pre 展示 + 复制/保存两个出口。
6
+ * 文案强调「仅生成」——本对话框不做也不提供补丁应用(P5 另议);
7
+ * truncated = host 检测到 stdout 超限丢头(PROGRESS §六.1),明示「不完整」防误用;
8
+ * binary = 补丁里含二进制文件的 "Cannot display" 通知段(M4 实测形态)。
9
+ * 作者 ddj 2026年09月20号
10
+ */
11
+ import React from 'react'
12
+ import { ModalShell } from './ModalShell.js'
13
+ import { downloadText } from './svnExport.js'
14
+
15
+ /**
16
+ * 补丁预览对话框。
17
+ * @author ddj 2026年09月20号
18
+ * @param props.path 补丁目标(工作区相对路径)
19
+ * @param props.text 补丁文本(unified 格式)
20
+ * @param props.truncated host 检测到输出被截断(头部 Index: 丢失)
21
+ * @param props.binary 补丁含二进制文件通知段(无文本差异内容)
22
+ * @param props.onNote 轻量反馈(复制/保存结果;缺省静默)
23
+ * @param props.onClose 关闭回调
24
+ * @returns 弹窗元素
25
+ */
26
+ export function SvnPatchDialog(props) {
27
+ const { path, text, truncated, binary, onNote, onClose } = props
28
+ const fileName = 'svn-patch-' + String(path || 'wc').split(/[\\/]/).pop().replace(/[^\w.-]+/g, '_') + '.patch'
29
+
30
+ /** 复制补丁全文(剪贴板不可用时提示手动复制)。 */
31
+ const copyText = () => {
32
+ void navigator.clipboard?.writeText(text).then(
33
+ () => onNote?.('补丁已复制到剪贴板'),
34
+ () => onNote?.('复制失败:请点击文本区手动全选复制'),
35
+ )
36
+ }
37
+ /** 保存为 .patch 文件(复用 W1-4 前端下载,仅本地不外发)。 */
38
+ const saveFile = () => {
39
+ const done = downloadText(fileName, text, 'text/x-diff')
40
+ onNote?.(done ? '已保存 ' + fileName : '保存失败:浏览器下载能力不可用')
41
+ }
42
+
43
+ return React.createElement(ModalShell, {
44
+ key: 'edrv-svn-patch',
45
+ dialogClass: 'edrv-svn-patch-dialog',
46
+ width: 'min(760px, calc(100vw - 48px))',
47
+ onClose,
48
+ },
49
+ React.createElement('div', { className: 'edrv-svn-patch-head' },
50
+ React.createElement('h3', { className: 'edrv-svnlog-title' }, 'SVN 补丁(仅生成,不应用)'),
51
+ React.createElement('span', { className: 'edrv-svnlog-target', title: path || '' }, path || '(工作副本根)'),
52
+ React.createElement('span', { style: { flex: 1 } }),
53
+ React.createElement('button', { className: 'edrv-svn-act', title: '关闭', onClick: onClose }, '✕')),
54
+ React.createElement('div', { className: 'edrv-svn-patch-notes' },
55
+ (truncated ? React.createElement('span', { className: 'edrv-svn-patch-warn' }, '⚠ 补丁超出 16MB 上限已截断(头部丢失),内容不完整,请勿使用') : null),
56
+ (binary ? React.createElement('span', { className: 'edrv-svn-patch-warn' }, '⚠ 包含二进制文件:这些文件只有 "Cannot display" 占位段,无文本差异内容') : null)),
57
+ React.createElement('pre', { className: 'edrv-svn-patch-pre' }, text || '(空补丁:目标无差异)'),
58
+ React.createElement('div', { className: 'edrv-svn-patch-foot' },
59
+ React.createElement('button', { className: 'edrv-svn-act', title: '复制补丁全文到剪贴板', onClick: copyText }, '复制'),
60
+ React.createElement('button', { className: 'edrv-svn-act', title: '保存为本地 .patch 文件(仅本地,不外发)', onClick: saveFile }, '保存 .patch'),
61
+ React.createElement('span', { style: { flex: 1 } }),
62
+ React.createElement('span', { className: 'edrv-svn-count' }, String(text.length) + ' 字符')))
63
+ }
@@ -0,0 +1,77 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * dsh-vscode-mode client — SVN 目录对比对话框(W2-2,任意两修订的变更列表)。
4
+ *
5
+ * 形态对齐日志弹窗的 Show Range 输入 + 变更面板列表:修订区间输入 → `svn.diffSum`
6
+ * (summarize)→ 动作字母 + 相对路径列表。纯展示不做逐文件动作(与 TortoiseSVN
7
+ * 「Compare revisions」列表一致;文件级差异走既有日志弹窗比较入口)。
8
+ * 作者 ddj 2026年09月20号
9
+ */
10
+ import React from 'react'
11
+ import { SVN_SUM_LETTER } from '../../shared/svn.js'
12
+ import { ModalShell } from './ModalShell.js'
13
+ import { svnDiffSum } from '../svnStatus.js'
14
+
15
+ /**
16
+ * 目录对比对话框。
17
+ * @author ddj 2026年09月20号
18
+ * @param props.sessionId 会话 id
19
+ * @param props.path 默认对比目标(工作区相对路径;'' = 工作副本根)
20
+ * @param props.onNote 轻量反馈(缺省静默)
21
+ * @param props.onClose 关闭回调
22
+ * @returns 弹窗元素
23
+ */
24
+ export function SvnSumDialog(props) {
25
+ const { sessionId, path, onNote, onClose } = props
26
+ const [revA, setRevA] = React.useState('')
27
+ const [revB, setRevB] = React.useState('')
28
+ const [entries, setEntries] = React.useState(null)
29
+ const [busy, setBusy] = React.useState(false)
30
+ const [error, setError] = React.useState('')
31
+
32
+ /** 拉取区间 summarize(revA 必须小于 revB:svn 语义 -r 较早:较晚)。 */
33
+ const submit = () => {
34
+ const a = Number(revA)
35
+ const b = Number(revB)
36
+ if (!(a >= 1 && b >= 1)) { setError('请输入有效修订号'); return }
37
+ if (a >= b) { setError('起始修订应小于结束修订(svn 语义 -r 较早:较晚)'); return }
38
+ setBusy(true)
39
+ setError('')
40
+ void svnDiffSum(sessionId, a, b, path).then((outcome) => {
41
+ if (!outcome.ok) setError(outcome.message)
42
+ else setEntries(outcome.entries ?? [])
43
+ if (!outcome.ok) onNote?.(outcome.message)
44
+ }).finally(() => setBusy(false))
45
+ }
46
+
47
+ const listBody = () => {
48
+ if (entries === null) return React.createElement('div', { className: 'edrv-tree-loading' }, '输入修订区间后点「对比」')
49
+ if (!entries.length) return React.createElement('div', { className: 'edrv-tree-loading' }, '该区间无变更')
50
+ return entries.map((entry) => React.createElement('div', { key: entry.path, className: 'edrv-svn-row', title: entry.path },
51
+ React.createElement('span', { className: 'edrv-svn-ch' }, SVN_SUM_LETTER[entry.item] || ' '),
52
+ React.createElement('span', { className: 'edrv-svn-path' }, entry.path),
53
+ (entry.kind === 'dir' ? React.createElement('span', { className: 'edrv-svnlog-path-kind' }, '目录') : null)))
54
+ }
55
+
56
+ return React.createElement(ModalShell, {
57
+ key: 'edrv-svn-sum',
58
+ dialogClass: 'edrv-svn-sum-dialog',
59
+ width: 'min(680px, calc(100vw - 48px))',
60
+ onClose,
61
+ },
62
+ React.createElement('div', { className: 'edrv-svn-patch-head' },
63
+ React.createElement('h3', { className: 'edrv-svnlog-title' }, 'SVN 目录对比(两修订)'),
64
+ React.createElement('span', { className: 'edrv-svnlog-target', title: path || '(工作副本根)' }, path || '(工作副本根)'),
65
+ React.createElement('span', { style: { flex: 1 } }),
66
+ React.createElement('button', { className: 'edrv-svn-act', title: '关闭', onClick: onClose }, '✕')),
67
+ React.createElement('div', { className: 'edrv-svn-sum-form' },
68
+ React.createElement('label', { className: 'edrv-svnlog-check' }, '起始修订(较早)',
69
+ React.createElement('input', { className: 'edrv-svnlog-range-input', type: 'number', min: 1, value: revA, onChange: (event) => setRevA(event.target.value) })),
70
+ React.createElement('label', { className: 'edrv-svnlog-check' }, '结束修订(较晚)',
71
+ React.createElement('input', { className: 'edrv-svnlog-range-input', type: 'number', min: 1, value: revB, onChange: (event) => setRevB(event.target.value) })),
72
+ React.createElement('button', { className: 'edrv-svn-act', disabled: busy, onClick: submit }, busy ? '对比中…' : '对比')),
73
+ (error ? React.createElement('div', { className: 'edrv-svn-patch-warn' }, error) : null),
74
+ React.createElement('div', { className: 'edrv-svn-sum-list' }, listBody()),
75
+ React.createElement('div', { className: 'edrv-svn-patch-foot' },
76
+ React.createElement('span', { className: 'edrv-svn-count' }, entries === null ? '' : String(entries.length) + ' 项变更')))
77
+ }