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
@@ -12,6 +12,7 @@ import { rpc } from '../../rpc.js'
12
12
  import type { SidebarCtx } from '../types.js'
13
13
  import { CACHE_KEY } from '../../paths.js'
14
14
  import { workspaceScopeOf } from '../../state/scopeStore.js'
15
+ import { takeSearchSeed } from '../../searchSeed.js'
15
16
 
16
17
  const DEBOUNCE_MS = 250
17
18
  const INCLUDE_PLACEHOLDER = '例如 *.ts, src/**/include'
@@ -25,6 +26,23 @@ export function splitGlobs(text) {
25
26
  return String(text ?? '').split(',').map((item) => item.trim()).filter(Boolean)
26
27
  }
27
28
 
29
+ /**
30
+ * 读取按作用域持久化的搜索条件(损坏/缺失返回 null)。
31
+ * 抽成函数而非内联:恢复 effect 内有两处消费者(填状态、同步 requestRef),
32
+ * 且解析必须只做一次(JSON.parse 重复调用既慢又可能两次结果不一致)。
33
+ * @author ddj 2026年09月18号
34
+ * @param key 作用域缓存键
35
+ * @returns 已解析的搜索条件对象;无/损坏返回 null
36
+ */
37
+ export function savedSearchOf(key: string) {
38
+ try {
39
+ const parsed = JSON.parse(localStorage.getItem(key) || 'null')
40
+ return parsed && typeof parsed === 'object' ? parsed : null
41
+ } catch (e) {
42
+ return null
43
+ }
44
+ }
45
+
28
46
  /**
29
47
  * 命中行文本三段切分(before/命中/after,列为 1-based UTF-16)。
30
48
  * @param text 行文本
@@ -82,20 +100,31 @@ export function SearchPanel(props) {
82
100
  // 按作用域恢复上次查询、选项与过滤模式(VSCode 记忆搜索词)
83
101
  React.useEffect(() => {
84
102
  if (!scope) return
85
- try {
86
- const saved = JSON.parse(localStorage.getItem(CACHE_KEY.search + String(scope)) || 'null')
87
- if (saved && typeof saved === 'object') {
88
- if (typeof saved.query === 'string') setQuery(saved.query)
89
- if (typeof saved.matchCase === 'boolean') setMatchCase(saved.matchCase)
90
- if (typeof saved.wholeWord === 'boolean') setWholeWord(saved.wholeWord)
91
- if (typeof saved.regex === 'boolean') setRegex(saved.regex)
92
- if (typeof saved.includeText === 'string') setIncludeText(saved.includeText)
93
- if (typeof saved.excludeText === 'string') setExcludeText(saved.excludeText)
94
- if (typeof saved.onlyActive === 'boolean') setOnlyActive(saved.onlyActive)
95
- if (typeof saved.excludeOn === 'boolean') setExcludeOn(saved.excludeOn)
96
- if (typeof saved.sectionOpen === 'boolean') setSectionOpen(saved.sectionOpen)
103
+ const saved = savedSearchOf(CACHE_KEY.search + String(scope))
104
+ if (saved) {
105
+ if (typeof saved.query === 'string') setQuery(saved.query)
106
+ if (typeof saved.matchCase === 'boolean') setMatchCase(saved.matchCase)
107
+ if (typeof saved.wholeWord === 'boolean') setWholeWord(saved.wholeWord)
108
+ if (typeof saved.regex === 'boolean') setRegex(saved.regex)
109
+ if (typeof saved.includeText === 'string') setIncludeText(saved.includeText)
110
+ if (typeof saved.excludeText === 'string') setExcludeText(saved.excludeText)
111
+ if (typeof saved.onlyActive === 'boolean') setOnlyActive(saved.onlyActive)
112
+ if (typeof saved.excludeOn === 'boolean') setExcludeOn(saved.excludeOn)
113
+ if (typeof saved.sectionOpen === 'boolean') setSectionOpen(saved.sectionOpen)
114
+ // 显式把恢复后的选项同步进 requestRef:render 期的同步要等下一次渲染才生效,
115
+ // 而紧随其后的「选区种子」effect 会在本次 commit 内立即搜索,读到的会是陈旧选项。
116
+ requestRef.current = {
117
+ matchCase: saved.matchCase === true,
118
+ wholeWord: saved.wholeWord === true,
119
+ regex: saved.regex === true,
120
+ include: saved.onlyActive === true
121
+ ? (activePathRef.current ? [activePathRef.current] : [])
122
+ : splitGlobs(typeof saved.includeText === 'string' ? saved.includeText : ''),
123
+ exclude: saved.excludeOn === true
124
+ ? splitGlobs(typeof saved.excludeText === 'string' ? saved.excludeText : '')
125
+ : [],
97
126
  }
98
- } catch (e) { /* 损坏忽略 */ }
127
+ }
99
128
  }, [scope])
100
129
 
101
130
  // 查询/选项/过滤变化 → 持久化
@@ -191,9 +220,35 @@ export function SearchPanel(props) {
191
220
  })
192
221
  }
193
222
 
194
- // Ctrl+Shift+F 重复触发时聚焦输入框(EditorView 派发 edrv:search-focus)
223
+ /**
224
+ * 取用一次性选区种子(需求 1:Ctrl+Shift+F 时把编辑器选中文本填入搜索框)。
225
+ * 有种子即填入并立即搜索(不再等 250ms 防抖),同时全选便于直接改写;无种子为 no-op。
226
+ * @author ddj 2026年09月18号
227
+ * @returns 是否消费到种子
228
+ */
229
+ const applySeed = () => {
230
+ const seed = takeSearchSeed()
231
+ if (!seed) return false
232
+ if (timerRef.current) clearTimeout(timerRef.current)
233
+ queryRef.current = seed
234
+ setQuery(seed)
235
+ runSearch(seed)
236
+ inputRef.current?.select?.()
237
+ return true
238
+ }
239
+
240
+ // 挂载时消费种子:侧栏原本收起 → 派发 edrv:search-focus 时本面板尚未挂载,
241
+ // 事件无人接收,故必须由挂载路径兜底。
242
+ // ⚠️ 声明在「按作用域恢复」effect 之后:React 按声明序执行 effect,恢复值先落地,
243
+ // 种子再覆盖,避免被记忆的旧查询词盖掉用户刚选中的内容。
244
+ React.useEffect(() => { applySeed() }, [])
245
+
246
+ // Ctrl+Shift+F 重复触发:消费种子(有则填入)并聚焦输入框(EditorView 派发 edrv:search-focus)
195
247
  React.useEffect(() => {
196
- const onFocus = () => inputRef.current?.focus?.()
248
+ const onFocus = () => {
249
+ applySeed()
250
+ inputRef.current?.focus?.()
251
+ }
197
252
  window.addEventListener('edrv:search-focus', onFocus)
198
253
  return () => window.removeEventListener('edrv:search-focus', onFocus)
199
254
  }, [])
@@ -22,10 +22,12 @@ import {
22
22
  SVN_STATUS_LETTER,
23
23
  SVN_STATUS_TONE,
24
24
  isSvnDiffable,
25
+ pairMissingWithUnversioned,
25
26
  svnVisibleChanges,
26
27
  } from '../../../shared/svn.js'
27
28
  import { CACHE_KEY } from '../../paths.js'
28
- import { refreshSvnChanges, svnAdd, svnRevert } from '../../svnStatus.js'
29
+ import { refreshSvnChanges, svnAdd, svnConflictArtifacts, svnFileSizes, svnRemoteStatus, svnRevert } from '../../svnStatus.js'
30
+ import { csvOf, downloadText, htmlTableOf } from '../../ui/svnExport.js'
29
31
  import type { SidebarCtx } from '../types.js'
30
32
 
31
33
  /** 未分组条目的分组键(changelist 为空)。 */
@@ -44,7 +46,7 @@ const REVERT_CONFIRM_TAIL = ' 个文件的本地改动?新增(A)文件在
44
46
  * @returns 开关(默认:显示未版本控制、隐藏忽略项)
45
47
  */
46
48
  function loadFilter(scope) {
47
- const fallback = { unversioned: true, ignored: false }
49
+ const fallback = { unversioned: true, ignored: false, name: '' }
48
50
  if (!scope) return fallback
49
51
  try {
50
52
  const raw = window.localStorage.getItem(CACHE_KEY.svn + scope)
@@ -53,6 +55,8 @@ function loadFilter(scope) {
53
55
  return {
54
56
  unversioned: parsed?.unversioned !== false,
55
57
  ignored: parsed?.ignored === true,
58
+ // W1-2 名称过滤随开关一并持久化(旧数据缺字段 = 不过滤,向后兼容)
59
+ name: typeof parsed?.name === 'string' ? parsed.name : '',
56
60
  }
57
61
  } catch (error) {
58
62
  return fallback
@@ -121,16 +125,18 @@ export function groupByChangelist(entries) {
121
125
 
122
126
  /**
123
127
  * 单条变更行(状态字母 + 路径 + 行内动作;双击 = 与基线比较)。
124
- * @author ddj 2026年09月16号
128
+ * @author ddj 2026年09月16号 / 2026年09月20号
125
129
  * @param props.entry 变更条目
126
130
  * @param props.busy 批量动作进行中(按钮置灰)
127
131
  * @param props.onDiff 打开基线差异
128
132
  * @param props.onAdd 加入版本控制
129
133
  * @param props.onRevert 还原
134
+ * @param props.onConflict 冲突副本对比(W2-4;仅冲突行出现按钮)
135
+ * @param props.pairNote 疑似改名标记文案(W2-5;空 = 无配对)
130
136
  * @returns 行元素
131
137
  */
132
138
  function SvnRow(props) {
133
- const { entry, busy, onDiff, onAdd, onRevert } = props
139
+ const { entry, busy, onDiff, onAdd, onRevert, onConflict, pairNote } = props
134
140
  const diffable = isSvnDiffable(entry.path, entry.status)
135
141
  const act = (label, title, danger, run) => React.createElement('button', {
136
142
  className: 'edrv-svn-act' + (danger ? ' edrv-svn-act-danger' : ''),
@@ -145,7 +151,9 @@ function SvnRow(props) {
145
151
  },
146
152
  statusLetterEl(entry),
147
153
  React.createElement('span', { className: 'edrv-svn-path' }, entry.path),
154
+ (pairNote ? React.createElement('span', { className: 'edrv-svn-pair-mark', title: pairNote }, '⇄') : null),
148
155
  React.createElement('span', { className: 'edrv-svn-acts' },
156
+ (entry.status === 'conflicted' ? act('冲突对比', '与 .mine/.rN 冲突副本并排对比(解决指引见差异视图顶部)', false, () => onConflict(entry.path)) : null),
149
157
  (diffable ? act('比较', SVN_DIFF_BASE_LABEL + '(BASE 与工作区并排)', false, () => onDiff(entry.path)) : null),
150
158
  (!entry.versioned ? act('加入', SVN_ADD_LABEL, false, () => onAdd([entry.path])) : null),
151
159
  (entry.versioned ? act('还原', SVN_REVERT_LABEL + '(放弃本地改动)', true, () => onRevert([entry.path])) : null)))
@@ -161,7 +169,11 @@ function SvnRow(props) {
161
169
  function svnListBody(state, handlers) {
162
170
  if (!state.managed) return React.createElement('div', { className: 'edrv-tree-loading' }, '当前工作区不受 SVN 管理')
163
171
  if (state.entries === null) return React.createElement('div', { className: 'edrv-tree-loading' }, '读取变更中…')
164
- if (!state.visible.length) return React.createElement('div', { className: 'edrv-tree-loading' }, '无变更(工作副本干净)')
172
+ if (!state.visible.length) {
173
+ // W1-2:名称过滤生效时区分「无匹配」与「无变更」,提示清空过滤框
174
+ return React.createElement('div', { className: 'edrv-tree-loading' },
175
+ state.nameFiltered ? '无匹配条目(名称过滤生效中,清空过滤框可看全部)' : '无变更(工作副本干净)')
176
+ }
165
177
  const rows = []
166
178
  for (const group of state.groups) {
167
179
  if (state.groups.length > 1 || group.name !== NO_CHANGELIST) {
@@ -170,47 +182,64 @@ function svnListBody(state, handlers) {
170
182
  React.createElement('span', { className: 'edrv-svn-group-n' }, String(group.entries.length))))
171
183
  }
172
184
  for (const entry of group.entries) {
173
- rows.push(React.createElement(SvnRow, Object.assign({ key: entry.path, entry }, handlers)))
185
+ rows.push(React.createElement(SvnRow, Object.assign({ key: entry.path, entry, pairNote: handlers.pairNotes?.[entry.path] }, handlers)))
174
186
  }
175
187
  }
176
188
  return rows
177
189
  }
178
190
 
179
191
  /**
180
- * 工具条(显示开关 + 批量动作)。
181
- * @author ddj 2026年09月16号
182
- * @param props.filter 当前开关
183
- * @param props.count 可见条目数
184
- * @param props.paths 批量动作目标(add/revert 各自清单)
185
- * @param props.handlers 动作回调(toggle/add/revert/busy)
192
+ * 工具条(显示开关 + 名称过滤 + 批量动作 + 导出)。
193
+ * @author ddj 2026年09月16号 / 2026年09月20号
194
+ * @param props.filter 当前开关(含 W1-2 name)
195
+ * @param props.count 可见条目数(名称过滤后)
196
+ * @param props.countAll 全量条数(未按名称过滤,供「x/y 项」展示)
197
+ * @param props.paths 批量动作目标(add/revert 各自清单;按未过滤集合计算)
198
+ * @param props.handlers 动作回调(toggle/add/revert/onNameFilter/onExportCsv/onExportHtml/busy)
186
199
  * @returns 工具条元素数组
187
200
  */
188
201
  function svnToolbarEl(props) {
189
- const { filter, count, paths, handlers } = props
202
+ const { filter, count, countAll, paths, handlers } = props
190
203
  const check = (label, title, checked, onChange) => React.createElement('label', {
191
204
  className: 'edrv-svn-check', title,
192
205
  },
193
206
  React.createElement('input', { type: 'checkbox', checked, onChange }),
194
207
  React.createElement('span', null, label))
208
+ const nameFiltered = String(filter.name || '').trim() !== ''
195
209
  return [
196
210
  React.createElement('div', { key: 'tools', className: 'edrv-svn-toolbar' },
197
211
  check('未版本控制', '显示未纳入版本控制的文件', filter.unversioned, handlers.toggleUnversioned),
198
- check('忽略项', '显示被忽略的文件', filter.ignored, handlers.toggleIgnored)),
212
+ check('忽略项', '显示被忽略的文件', filter.ignored, handlers.toggleIgnored),
213
+ React.createElement('input', {
214
+ className: 'edrv-svn-name-filter',
215
+ placeholder: '名称过滤:* ? 通配或子串',
216
+ title: '按路径过滤条目(* 任意串、? 单字符;无通配符按子串包含,忽略大小写;只影响列表展示,不缩小批量动作范围)',
217
+ value: filter.name || '',
218
+ onChange: (event) => handlers.onNameFilter(event.target.value),
219
+ })),
199
220
  (count
200
221
  ? React.createElement('div', { key: 'bulk', className: 'edrv-svn-bulk' },
201
- React.createElement('span', { className: 'edrv-svn-count' }, String(count) + ' 项'),
222
+ React.createElement('span', { className: 'edrv-svn-count' }, String(count) + (nameFiltered ? '/' + countAll : '') + ' 项'),
202
223
  React.createElement('span', { style: { flex: 1 } }),
224
+ React.createElement('button', {
225
+ className: 'edrv-svn-act', title: '导出当前列表为 CSV(带 BOM,Excel 直开;状态/路径/changelist/修订)',
226
+ onClick: handlers.onExportCsv,
227
+ }, '导出CSV'),
228
+ React.createElement('button', {
229
+ className: 'edrv-svn-act', title: '导出当前列表为 HTML 报表(仅本地查看,不外发)',
230
+ onClick: handlers.onExportHtml,
231
+ }, '导出HTML'),
203
232
  (paths.unversioned.length
204
233
  ? React.createElement('button', {
205
234
  className: 'edrv-svn-act', disabled: handlers.busy,
206
- title: '把全部未版本控制文件加入版本控制',
235
+ title: '把全部未版本控制文件加入版本控制(不受名称过滤影响)',
207
236
  onClick: () => handlers.add(paths.unversioned),
208
237
  }, '全部加入')
209
238
  : null),
210
239
  (paths.revert.length
211
240
  ? React.createElement('button', {
212
241
  className: 'edrv-svn-act edrv-svn-act-danger', disabled: handlers.busy,
213
- title: '还原全部已受版本控制文件的本地改动',
242
+ title: '还原全部已受版本控制文件的本地改动(不受名称过滤影响)',
214
243
  onClick: () => handlers.revert(paths.revert),
215
244
  }, '全部还原')
216
245
  : null))
@@ -233,10 +262,17 @@ export function SvnPanel(props) {
233
262
  const managed = Boolean(ctx?.svn?.managed && ctx?.svn?.svnCli)
234
263
  const [filter, setFilter] = React.useState(() => loadFilter(scope))
235
264
  const [busy, setBusy] = React.useState(false)
265
+ // W2-3:远端检查结果(null = 未检查;{ failed: true } = 失败降级;否则 { outdated, againstRev })
266
+ const [remote, setRemote] = React.useState(null)
267
+ const [remoteBusy, setRemoteBusy] = React.useState(false)
268
+ // W2-5:配对候选的文件大小(path → size|null)与成对结果
269
+ const [sizes, setSizes] = React.useState({})
236
270
 
237
- // 作用域切换:重读该作用域自己的开关(同一工作区共享)
271
+ // 作用域切换:重读该作用域自己的开关(同一工作区共享)并清掉远端/配对的会话内状态
238
272
  React.useEffect(() => {
239
273
  setFilter(loadFilter(scope))
274
+ setRemote(null)
275
+ setSizes({})
240
276
  }, [scope])
241
277
 
242
278
  const applyFilter = (next) => {
@@ -264,40 +300,148 @@ export function SvnPanel(props) {
264
300
  }).finally(() => setBusy(false))
265
301
  }
266
302
 
267
- const visible = entries === null ? [] : svnVisibleChanges(entries, filter)
303
+ /**
304
+ * 远端更新检查(W2-3;host 侧 15s 短超时,离线/超时降级为提示条文案,不阻塞面板)。
305
+ * @author ddj 2026年09月20号
306
+ */
307
+ const checkRemote = () => {
308
+ if (remoteBusy) return
309
+ setRemoteBusy(true)
310
+ void svnRemoteStatus(sessionId, '').then((outcome) => {
311
+ if (!outcome.ok) {
312
+ setRemote({ failed: true })
313
+ ctx?.notify?.(outcome.message)
314
+ return
315
+ }
316
+ setRemote({ outdated: outcome.outdated ?? [], againstRev: outcome.againstRev ?? null })
317
+ }).finally(() => setRemoteBusy(false))
318
+ }
319
+
320
+ /**
321
+ * 冲突副本对比入口(W2-4,只读):扫描 .mine/.working/.rN → 默认首个 .rN ↔ .mine 并排;
322
+ * resolve 指引在差异视图顶部展示,本入口绝不自动执行 svn resolve(归 P2 破坏性批次)。
323
+ * @author ddj 2026年09月20号
324
+ * @param path 冲突文件的工作区相对路径
325
+ */
326
+ const openConflictDiff = (path) => {
327
+ void svnConflictArtifacts(sessionId, path).then((outcome) => {
328
+ if (!outcome.ok) { ctx?.notify?.(outcome.message); return }
329
+ const artifacts = outcome.artifacts ?? []
330
+ const revSide = artifacts.find((item) => item.kind === 'rev')
331
+ const mineSide = artifacts.find((item) => item.kind === 'mine')
332
+ if (!revSide || !mineSide) {
333
+ ctx?.notify?.('未发现冲突副本(.mine/.rN):' + artifacts.map((item) => item.name).join('、'))
334
+ return
335
+ }
336
+ if (!ctx?.openSvnLocalPair) { ctx?.notify?.('当前视图不支持冲突对比'); return }
337
+ ctx.openSvnLocalPair(revSide.path, mineSide.path, '.r' + (revSide.rev ?? '?'), '.mine', path)
338
+ })
339
+ }
340
+
341
+ // W1-2:展示集合(含名称过滤)与批量动作集合(不含名称过滤)分离,「全部加入/还原」不缩小范围
342
+ const shown = entries === null ? [] : svnVisibleChanges(entries, filter)
343
+ const actionable = entries === null ? [] : svnVisibleChanges(entries, { unversioned: filter.unversioned, ignored: filter.ignored })
344
+ const nameFiltered = String(filter.name || '').trim() !== ''
345
+
346
+ // W2-5:`!`(missing)与 `?`(unversioned)共存时才做大小查询(候选少,一次批量查完成对)
347
+ const missingEntries = entries === null ? [] : entries.filter((entry) => entry.status === 'missing')
348
+ const unversionedEntries = entries === null ? [] : entries.filter((entry) => entry.status === 'unversioned')
349
+ const candidates = [...missingEntries, ...unversionedEntries].map((entry) => entry.path)
350
+ const pairsKnown = missingEntries.length > 0 && unversionedEntries.length > 0
351
+ && candidates.every((path) => path in sizes)
352
+ React.useEffect(() => {
353
+ if (!pairsKnown && candidates.length) {
354
+ void svnFileSizes(sessionId, candidates).then((table) => setSizes(table))
355
+ }
356
+ }, [pairsKnown, candidates.join('\n'), sessionId]) // eslint-disable-line react-hooks/exhaustive-deps
357
+ const pairNoteOf = pairsKnown
358
+ ? (() => {
359
+ const notes = {}
360
+ for (const pair of pairMissingWithUnversioned(entries ?? [], sizes)) {
361
+ notes[pair.missingPath] = '疑似改名:原文件 → ' + pair.unversionedPath
362
+ notes[pair.unversionedPath] = '疑似改名:← 原文件 ' + pair.missingPath
363
+ }
364
+ return notes
365
+ })()
366
+ : {}
367
+
268
368
  const openSvnDiff = (p) => ctx?.openSvnDiff?.(p)
269
369
  const handlers = {
270
370
  busy,
271
371
  onDiff: openSvnDiff,
372
+ onConflict: openConflictDiff,
373
+ pairNotes: pairNoteOf,
272
374
  onAdd: (paths) => runBatch('add', paths),
273
375
  onRevert: (paths) => runBatch('revert', paths),
274
376
  }
275
- const listState = { managed, entries, visible, groups: groupByChangelist(visible) }
377
+
378
+ /**
379
+ * 导出当前展示列表(W1-4):CSV 带 BOM 供 Excel 直开;HTML 本地报表;失败经 notify 提示。
380
+ * @author ddj 2026年09月20号
381
+ * @param kind 导出格式('csv' | 'html')
382
+ */
383
+ const onExport = (kind) => {
384
+ const rows = shown.map((entry) => [
385
+ SVN_STATUS_LETTER[entry.status] || '',
386
+ entry.path,
387
+ entry.changelist || '',
388
+ entry.revision && entry.revision > 0 ? String(entry.revision) : '',
389
+ ])
390
+ const headers = ['状态', '路径', 'changelist', '修订']
391
+ const stamp = new Date().toISOString().slice(0, 10)
392
+ const done = kind === 'html'
393
+ ? downloadText('svn-changes-' + stamp + '.html', htmlTableOf(headers, rows, 'SVN 变更列表 ' + stamp), 'text/html')
394
+ : downloadText('svn-changes-' + stamp + '.csv', csvOf([headers, ...rows]), 'text/csv')
395
+ if (!done) ctx?.notify?.('导出失败:浏览器下载能力不可用')
396
+ }
397
+
398
+ const listState = { managed, entries, visible: shown, groups: groupByChangelist(shown), nameFiltered }
276
399
  const toolbar = svnToolbarEl({
277
400
  filter,
278
- count: visible.length,
401
+ count: shown.length,
402
+ countAll: actionable.length,
279
403
  paths: {
280
- unversioned: visible.filter((entry) => !entry.versioned).map((entry) => entry.path),
281
- revert: visible.filter((entry) => entry.versioned).map((entry) => entry.path),
404
+ unversioned: actionable.filter((entry) => !entry.versioned).map((entry) => entry.path),
405
+ revert: actionable.filter((entry) => entry.versioned).map((entry) => entry.path),
282
406
  },
283
407
  handlers: Object.assign({}, handlers, {
284
408
  toggleUnversioned: () => applyFilter({ ...filter, unversioned: !filter.unversioned }),
285
409
  toggleIgnored: () => applyFilter({ ...filter, ignored: !filter.ignored }),
410
+ onNameFilter: (name) => applyFilter({ ...filter, name }),
411
+ onExportCsv: () => onExport('csv'),
412
+ onExportHtml: () => onExport('html'),
286
413
  add: (paths) => runBatch('add', paths),
287
414
  revert: (paths) => runBatch('revert', paths),
288
415
  }),
289
416
  })
290
417
  const rootName = ctx?.svn?.wcRoot ? String(ctx.svn.wcRoot).split(/[\\/]/).pop() || ctx.svn.wcRoot : ''
291
418
 
419
+ // W2-3:远端更新提示条(null = 未检查;failed = 降级文案;否则展示落后数与 against 修订)
420
+ const remoteEl = remote === null
421
+ ? null
422
+ : React.createElement('div', { className: 'edrv-svn-hintbar' + (remote.failed ? ' edrv-svn-hintbar-warn' : '') },
423
+ React.createElement('span', { style: { flex: 1 } },
424
+ remote.failed
425
+ ? '远端检查失败(网络不可达或超时;可用 ⟳ 或「检查远端」重试)'
426
+ : (remote.outdated.length
427
+ ? '远端有 ' + remote.outdated.length + ' 个更新(against r' + (remote.againstRev ?? '?') + '),其中本地已改 ' + remote.outdated.filter((item) => item.path && entries?.some((entry) => entry.path === item.path && entry.versioned)).length + ' 个;更新前请先提交或还原'
428
+ : '远端无更新(against r' + (remote.againstRev ?? '?') + ')')),
429
+ React.createElement('button', { className: 'edrv-svn-act', title: '关闭提示条', onClick: () => setRemote(null) }, '✕'))
430
+
292
431
  return React.createElement('div', { className: 'edrv-side-panel' },
293
432
  React.createElement('div', { className: 'edrv-side-head' },
294
433
  React.createElement('span', { className: 'edrv-side-title' }, 'SVN 变更'),
295
434
  React.createElement('span', { className: 'edrv-side-root', title: ctx?.svn?.wcRoot || '' }, rootName),
296
435
  React.createElement('span', { style: { flex: 1 } }),
436
+ React.createElement('button', {
437
+ className: 'edrv-side-btn', title: '检查远端更新(svn status -u;约数秒,离线自动降级)',
438
+ onClick: checkRemote,
439
+ }, remoteBusy ? '…' : '⇅'),
297
440
  React.createElement('button', {
298
441
  className: 'edrv-side-btn', title: '刷新变更',
299
442
  onClick: () => { refreshSvnChanges(sessionId, scope); ctx?.refreshRecords?.() },
300
443
  }, refreshIconEl())),
444
+ remoteEl,
301
445
  ...toolbar,
302
446
  React.createElement('div', { className: 'edrv-svn-list' }, svnListBody(listState, handlers)))
303
447
  }
@@ -7,10 +7,12 @@
7
7
  */
8
8
  import React from 'react'
9
9
  import { IconBranchOutline16, IconFolderOpenOutline16, IconListPenOutline16, IconSearchOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
10
+ import { dapStore } from '../../dap/store.js'
10
11
  import { FileExplorer } from './FileExplorer.js'
11
12
  import { SearchPanel } from './SearchPanel.js'
12
13
  import { RulesPanel } from './RulesPanel.js'
13
14
  import { SvnPanel } from './SvnPanel.js'
15
+ import { DebugPanel } from './DebugPanel.js'
14
16
  import type { SidebarPanelDef, SidebarCtx } from '../types.js'
15
17
 
16
18
  /**
@@ -97,3 +99,20 @@ function svnVisibleCount(entries) {
97
99
  }
98
100
  return count
99
101
  }
102
+
103
+ /**
104
+ * 构造「调试」面板定义(VS Code 调试视图四段布局 + REPL)。
105
+ * 图标用文本回落(官方原语图标集未含调试类图标时由 SidebarView 文本渲染)。
106
+ * @author ddj 2026年09月29号
107
+ * @returns 面板定义(徽标 = 已暂停时显示 1,吸引注意)
108
+ */
109
+ export function createDebugPanel(): SidebarPanelDef {
110
+ return {
111
+ id: 'debug',
112
+ title: '调试',
113
+ icon: '🐞',
114
+ order: 22,
115
+ badge: () => (dapStore.getSnapshot().phase === 'paused' ? 1 : null),
116
+ render: (ctx: SidebarCtx) => React.createElement(DebugPanel, { ctx }),
117
+ }
118
+ }
@@ -41,6 +41,8 @@ export interface SidebarCtx {
41
41
  svnChangeMap?: Record<string, SvnChangeEntry>
42
42
  /** 打开 SVN 基线差异视图(变更面板行内「与基线比较」;缺省时面板降级提示)。 */
43
43
  openSvnDiff?: (path: string) => void
44
+ /** 打开双侧本地文件差异(W2-4 冲突副本 `.rN` ↔ `.mine`;缺省时面板降级提示)。 */
45
+ openSvnLocalPair?: (left: string, right: string, leftLabel: string, rightLabel: string, title: string) => void
44
46
  /** 重查 SVN 变更清单(CLI 更新/还原/加入版本控制后由菜单动作调用)。 */
45
47
  refreshSvnChanges?: () => void
46
48
  /** 打开 SVN 日志弹窗(P3;菜单/命令栏「查看日志」)。 */