dsh-vscode-mode 0.10.0 → 0.12.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 (49) hide show
  1. package/README.md +62 -11
  2. package/lib/client.js +2879 -1030
  3. package/lib/client.js.map +1 -1
  4. package/lib/index.js +2227 -1056
  5. package/lib/index.js.map +1 -1
  6. package/package.json +1 -1
  7. package/src/client/activePathStore.ts +28 -0
  8. package/src/client/commandBridge.ts +9 -41
  9. package/src/client/dap/hover.ts +2 -7
  10. package/src/client/index.ts +116 -14
  11. package/src/client/keybindings.ts +19 -73
  12. package/src/client/luaReservedWords.ts +10 -0
  13. package/src/client/monaco/lsp/keywords.ts +121 -0
  14. package/src/client/monaco/lsp/lspClient.ts +68 -6
  15. package/src/client/monaco/lsp/memberScope.ts +90 -0
  16. package/src/client/monaco/lsp/providers.ts +14 -4
  17. package/src/client/settingsDocRouter.ts +52 -0
  18. package/src/client/shortcutsOfficial.ts +451 -0
  19. package/src/client/sidebar/menuItems.ts +1 -0
  20. package/src/client/sidebar/panels/RulesPanel.ts +15 -11
  21. package/src/client/sidebar/types.ts +2 -0
  22. package/src/client/styles/editor.css +143 -37
  23. package/src/client/svnActions.ts +4 -4
  24. package/src/client/ui/ContextMenu.ts +4 -2
  25. package/src/client/ui/EditorView.ts +142 -135
  26. package/src/client/ui/McpSettings.ts +4 -5
  27. package/src/client/ui/QuickOpen.ts +10 -11
  28. package/src/client/ui/ShortcutSettings.ts +294 -0
  29. package/src/client/ui/SvnLogDialog.ts +200 -69
  30. package/src/client/ui/SvnUpdateDialog.ts +472 -0
  31. package/src/client/ui/commandCatalog.ts +36 -41
  32. package/src/client/ui/svnUpdateModel.ts +135 -0
  33. package/src/fileOpenSettings.ts +3 -0
  34. package/src/index.ts +2 -0
  35. package/src/lsp/completionCap.ts +80 -0
  36. package/src/lsp/manager.ts +42 -10
  37. package/src/lsp/rpc.ts +8 -5
  38. package/src/lsp/server.ts +18 -4
  39. package/src/rpc.ts +74 -2
  40. package/src/rules.ts +100 -44
  41. package/src/settingsDocPath.ts +30 -0
  42. package/src/shared/keybindings.ts +253 -0
  43. package/src/shared/rpc.ts +12 -2
  44. package/src/shared/rules.ts +9 -1
  45. package/src/shared/svn.ts +38 -0
  46. package/src/svn.ts +102 -7
  47. package/src/svnText.ts +22 -13
  48. package/src/svnUpdateJob.ts +588 -0
  49. package/src/client/ui/KeybindingsPanel.ts +0 -190
@@ -0,0 +1,472 @@
1
+ /**
2
+ * dsh-vscode-mode client — live SVN update dialog with bounded virtual rows.
3
+ * @author ddj 2026年09月24号
4
+ */
5
+ import React from 'react'
6
+ import type { SvnUpdateRow, SvnUpdateState } from '../../shared/svn.js'
7
+ import { copyText } from '../copyText.js'
8
+ import { rpc } from '../rpc.js'
9
+ import { absoluteOf } from '../tabActions.js'
10
+ import { ContextMenu } from './ContextMenu.js'
11
+ import type { ContextMenuEntry } from './ContextMenu.js'
12
+ import { ModalShell } from './ModalShell.js'
13
+ import { updateActs, updateCloseMode, updateDone, updateLocked, updateParent, updateRange, updateReady, updateType } from './svnUpdateModel.js'
14
+
15
+ interface UpdateProps {
16
+ sessionId?: string
17
+ cwd?: string | null
18
+ path: string
19
+ resumeJobId?: string
20
+ childOverlay?: boolean
21
+ onClose: () => void
22
+ onDone: (state: SvnUpdateState) => void
23
+ onNote: (message: string) => void
24
+ onShowLog: (path: string, jobId: string) => void
25
+ onCompare: (path: string, revision: number, jobId: string) => void
26
+ onOpenFile: (path: string) => void
27
+ onOpenWith: (path: string) => void
28
+ onOpenParent: (path: string) => void
29
+ }
30
+ interface UpdateMenu { x: number; y: number; row: SvnUpdateRow; kind: string }
31
+
32
+ /**
33
+ * 仅恢复明确匹配的原任务;失败时不重新触发一次 SVN 更新。
34
+ * @author ddj 2026年09月24号
35
+ * @param props 会话与更新目标
36
+ * @returns 已启动或已恢复的任务
37
+ */
38
+ async function connectUpdate(props: UpdateProps): Promise<SvnUpdateState> {
39
+ const result = props.resumeJobId
40
+ ? await rpc('svn.updateActive', { sessionId: props.sessionId })
41
+ : await rpc('svn.updateStart', { sessionId: props.sessionId, path: props.path })
42
+ if (!result.ok) throw new Error(result.error)
43
+ const job = 'job' in result ? result.job : result
44
+ if (!job || (props.resumeJobId && job.jobId !== props.resumeJobId)) throw new Error('上次 SVN 更新结果已过期')
45
+ return job
46
+ }
47
+
48
+ /**
49
+ * 按后端有效序号剔除过期行;无新行时保留数组身份,避免多余渲染。
50
+ * @author ddj 2026年09月24号
51
+ * @param previous 当前可视记录
52
+ * @param result 本次增量响应
53
+ * @returns 新记录集
54
+ */
55
+ function mergeUpdateRows(previous: SvnUpdateRow[], result: { offset: number; rows: SvnUpdateRow[] }): SvnUpdateRow[] {
56
+ if (!result.rows.length && (previous[0]?.seq ?? result.offset) >= result.offset) return previous
57
+ return previous.filter((row) => row.seq >= result.offset).concat(result.rows).slice(-20_000)
58
+ }
59
+
60
+ /**
61
+ * 轮询状态:保留已收到的行,后端页大小与 UI 可见行数独立。
62
+ * @author ddj 2026年09月24号
63
+ * @param props 窗口目标与回调
64
+ * @returns 状态、行、连接错误
65
+ */
66
+ function useUpdateJob(props: UpdateProps) {
67
+ const [state, setState] = React.useState<SvnUpdateState | null>(null)
68
+ const [rows, setRows] = React.useState<SvnUpdateRow[]>([])
69
+ const [error, setError] = React.useState('')
70
+ const [pendingRows, setPendingRows] = React.useState(false)
71
+ const doneRef = React.useRef('')
72
+ const onDoneRef = React.useRef(props.onDone)
73
+ onDoneRef.current = props.onDone
74
+
75
+ /**
76
+ * 仅在目标变化时连接任务,销毁时停止客户端轮询但不隐式取消更新。
77
+ * @author ddj 2026年09月24号
78
+ * @returns 轮询释放回调
79
+ */
80
+ const watchJob = () => {
81
+ let alive = true
82
+ let busy = false
83
+ let jobId = ''
84
+ let since = 0
85
+ /**
86
+ * 每轮至多请求一页,等待下一轮继续追赶后台任务。
87
+ * @author ddj 2026年09月24号
88
+ */
89
+ const poll = async () => {
90
+ if (!alive || busy || !jobId) return
91
+ busy = true
92
+ try {
93
+ const result = await rpc('svn.updatePoll', { sessionId: props.sessionId, jobId, since })
94
+ if (!alive) return
95
+ if (!result.ok) { setError(result.error); return }
96
+ setError('')
97
+ since = result.nextSeq
98
+ const backlog = result.nextSeq < result.totalSeq
99
+ setPendingRows(backlog)
100
+ setState(result)
101
+ setRows((previous) => mergeUpdateRows(previous, result))
102
+ if (updateDone(result.phase) && !backlog) window.clearInterval(timer)
103
+ if (updateDone(result.phase) && !backlog && doneRef.current !== jobId) {
104
+ doneRef.current = jobId
105
+ onDoneRef.current(result)
106
+ }
107
+ if (backlog && result.rows.length) window.setTimeout(() => { void poll() }, 0)
108
+ } catch (cause) { if (alive) setError('连接更新任务失败:' + String(cause)) }
109
+ finally { busy = false }
110
+ }
111
+ /**
112
+ * 恢复原任务或首次启动;UI 错误可见但绝不自动重试 SVN update。
113
+ * @author ddj 2026年09月24号
114
+ */
115
+ const connect = async () => {
116
+ try {
117
+ const job = await connectUpdate(props)
118
+ if (!alive) return
119
+ jobId = job.jobId
120
+ setState(job)
121
+ setPendingRows(updateDone(job.phase))
122
+ void poll()
123
+ } catch (cause) { if (alive) setError('连接更新任务失败:' + String(cause)) }
124
+ }
125
+ setRows([])
126
+ setState(null)
127
+ setError('')
128
+ setPendingRows(false)
129
+ doneRef.current = ''
130
+ void connect()
131
+ const timer = window.setInterval(() => { void poll() }, 350)
132
+ return () => { alive = false; window.clearInterval(timer) }
133
+ }
134
+ React.useEffect(watchJob, [props.sessionId, props.path, props.resumeJobId])
135
+
136
+ return { state, rows, error, pendingRows, setState }
137
+ }
138
+
139
+ /**
140
+ * 中文计数只报告已观察到的条目,截断输出时 UI 会单独告警。
141
+ * @author ddj 2026年09月24号
142
+ * @param state 任务快照
143
+ * @returns 可读的状态行
144
+ */
145
+ function countText(state: SvnUpdateState | null): string {
146
+ if (!state) return '等待 SVN 任务…'
147
+ const c = state.counts
148
+ return '合并 ' + c.G + ' · 新增 ' + c.A + ' · 删除 ' + c.D
149
+ + ' · 更新 ' + c.U + ' · 冲突 ' + c.C + (c.R || c.E ? ' · 其他 ' + (c.R + c.E) : '')
150
+ }
151
+
152
+ /**
153
+ * 右键命令:可用性先经会话内路径守卫,再依据真实文件判型。
154
+ * @author ddj 2026年09月24号
155
+ * @param props 窗口回调
156
+ * @param state 任务状态
157
+ * @param menu 右键目标及文件判型
158
+ * @param rows 当前结果列表
159
+ * @param selected 已选序号
160
+ * @returns 可点击动作列表
161
+ */
162
+ function menuEntries(props: UpdateProps, state: SvnUpdateState, menu: UpdateMenu, rows: SvnUpdateRow[], selected: number[]): ContextMenuEntry[] {
163
+ const { row, kind } = menu
164
+ const actions = updateActs(row, state.phase, kind, state.revision)
165
+ const rel = row.relPath ?? ''
166
+ const picked = new Set(selected)
167
+ const names = rows.filter((item) => picked.has(item.seq)).map((item) => item.relPath
168
+ ? absoluteOf(item.relPath, props.cwd) : item.path)
169
+ return [
170
+ { id: 'compare', label: '与工作副本比较', disabled: !actions.compare, onClick: () => props.onCompare(rel, state.revision!, state.jobId) },
171
+ { id: 'log', label: '查看日志', disabled: !actions.log, onClick: () => props.onShowLog(rel, state.jobId) },
172
+ { id: 'open', label: '打开', disabled: !actions.open, separator: true, onClick: () => props.onOpenFile(rel) },
173
+ { id: 'open-with', label: '打开方式…', disabled: !actions.openWith, onClick: () => props.onOpenWith(rel) },
174
+ { id: 'parent', label: '打开父目录', disabled: !actions.parent, onClick: () => props.onOpenParent(updateParent(rel)) },
175
+ { id: 'copy', label: selected.length > 1 ? '复制选中路径' : '复制路径', disabled: !actions.copy, separator: true,
176
+ onClick: () => { void copyText(names.join('\n'), '已复制路径', props.onNote) } },
177
+ ]
178
+ }
179
+
180
+ /**
181
+ * 将一条增量行映射为高密度的三列;单元格显示工作副本内相对路径,完整绝对路径留在 title 中。
182
+ * @author ddj 2026年09月24号
183
+ * @param row 更新行
184
+ * @param cwd 工作区目录
185
+ * @param picked 是否选中
186
+ * @param onPick 选择回调
187
+ * @param onMenu 右键回调
188
+ * @returns 行元素
189
+ */
190
+ function rowElement(row: SvnUpdateRow, cwd: string | null | undefined, picked: boolean,
191
+ onPick: (event: React.MouseEvent<HTMLDivElement>) => void,
192
+ onMenu: (event: React.MouseEvent<HTMLDivElement>) => void): React.ReactElement {
193
+ // 显示用相对路径(窄窗省略尾部时先保住文件名),悬浮与复制仍用完整绝对路径。
194
+ const full = row.relPath ? absoluteOf(row.relPath, cwd) : (row.path || row.label)
195
+ const path = row.relPath || row.path || row.label
196
+ const tone = row.action === 'C' ? 'conflict' : row.action === 'D' ? 'deleted'
197
+ : row.action === 'A' ? 'added' : row.action === 'G' ? 'merged' : 'updated'
198
+ return React.createElement('div', {
199
+ role: 'row', 'aria-selected': picked, className: 'edrv-update-row' + (picked ? ' is-selected' : ''),
200
+ onClick: onPick, onContextMenu: onMenu, title: row.raw,
201
+ },
202
+ React.createElement('span', { role: 'cell', className: 'edrv-update-action tone-' + tone }, row.action ? row.label : '信息'),
203
+ React.createElement('span', { role: 'cell', className: 'edrv-update-path', title: full }, path),
204
+ React.createElement('span', { role: 'cell', className: 'edrv-update-type' }, row.action ? updateType(row.path) : ''))
205
+ }
206
+
207
+ /**
208
+ * 观察列表尺寸并只跟随用户尚未滚离底部的更新结果。
209
+ * @author ddj 2026年09月24号
210
+ * @param count 当前记录数
211
+ * @returns 虚拟列表视口、行高和滚动回调
212
+ */
213
+ function useUpdateView(count: number) {
214
+ const [scrollTop, setScrollTop] = React.useState(0)
215
+ const [height, setHeight] = React.useState(350)
216
+ const [compact, setCompact] = React.useState(() => window.innerWidth < 620)
217
+ const listRef = React.useRef<HTMLDivElement | null>(null)
218
+ const followRef = React.useRef(true)
219
+ const rowHeight = compact ? 44 : 28
220
+ React.useEffect(() => {
221
+ const list = listRef.current
222
+ if (!list) return
223
+ const observer = new ResizeObserver(() => setHeight(list.clientHeight))
224
+ observer.observe(list)
225
+ setHeight(list.clientHeight)
226
+ const resize = () => setCompact(window.innerWidth < 620)
227
+ window.addEventListener('resize', resize)
228
+ return () => { observer.disconnect(); window.removeEventListener('resize', resize) }
229
+ }, [])
230
+ React.useLayoutEffect(() => {
231
+ const list = listRef.current
232
+ if (list && followRef.current) { list.scrollTop = list.scrollHeight; setScrollTop(list.scrollTop) }
233
+ }, [count, rowHeight])
234
+ /**
235
+ * 用户离开底部后暂停自动滚动,保持当前文件可读。
236
+ * @author ddj 2026年09月24号
237
+ * @param event 列表滚动事件
238
+ */
239
+ const onScroll = (event: React.UIEvent<HTMLDivElement>) => {
240
+ const list = event.currentTarget
241
+ followRef.current = list.scrollHeight - list.scrollTop - list.clientHeight < 40
242
+ setScrollTop(list.scrollTop)
243
+ }
244
+ return { listRef, rowHeight, range: updateRange(count, scrollTop, height, rowHeight), onScroll }
245
+ }
246
+
247
+ /**
248
+ * 行选择、右键文件判型与菜单状态只在可见行交互时运行。
249
+ * @author ddj 2026年09月24号
250
+ * @param props 窗口回调
251
+ * @param rows 增量结果
252
+ * @param state 任务状态
253
+ * @returns 选择集和交互回调
254
+ */
255
+ function useUpdateMenu(props: UpdateProps, rows: SvnUpdateRow[], state: SvnUpdateState | null) {
256
+ const [selected, setSelected] = React.useState<number[]>([])
257
+ const [menu, setMenu] = React.useState<UpdateMenu | null>(null)
258
+ const anchorRef = React.useRef(0)
259
+ const picked = React.useMemo(() => new Set(selected), [selected])
260
+ /**
261
+ * 支持普通、Ctrl 和 Shift 范围选择。
262
+ * @author ddj 2026年09月24号
263
+ * @param event 行点击事件
264
+ * @param row 目标行
265
+ */
266
+ const pick = (event: React.MouseEvent<HTMLDivElement>, row: SvnUpdateRow) => {
267
+ const index = rows.findIndex((item) => item.seq === row.seq)
268
+ if (event.shiftKey) {
269
+ const from = Math.min(index, anchorRef.current)
270
+ const to = Math.max(index, anchorRef.current)
271
+ setSelected(rows.slice(from, to + 1).map((item) => item.seq))
272
+ } else if (event.ctrlKey || event.metaKey) {
273
+ setSelected((previous) => previous.includes(row.seq)
274
+ ? previous.filter((seq) => seq !== row.seq) : previous.concat(row.seq))
275
+ anchorRef.current = index
276
+ } else { setSelected([row.seq]); anchorRef.current = index }
277
+ }
278
+ /**
279
+ * 先开放父目录/复制,再按真实磁盘类型启用文件级操作。
280
+ * @author ddj 2026年09月24号
281
+ * @param event 右键位置
282
+ * @param row 目标行
283
+ */
284
+ const openMenu = (event: React.MouseEvent<HTMLDivElement>, row: SvnUpdateRow) => {
285
+ event.preventDefault()
286
+ event.stopPropagation()
287
+ if (!state) return
288
+ if (!picked.has(row.seq)) setSelected([row.seq])
289
+ setMenu({ x: event.clientX, y: event.clientY, row, kind: 'loading' })
290
+ if (!row.relPath || !updateDone(state.phase) || row.action === 'D') return
291
+ void rpc('edrv.externalStat', { path: absoluteOf(row.relPath, props.cwd) }).then((outcome) => {
292
+ setMenu((current) => current?.row.seq === row.seq
293
+ ? { ...current, kind: outcome.ok ? outcome.kind : 'missing' } : current)
294
+ }).catch(() => {})
295
+ }
296
+ return { selected, picked, menu, setMenu, pick, openMenu }
297
+ }
298
+
299
+ /**
300
+ * 窗口标题和执行阶段(无活动任务时仍可展示目标)。
301
+ * @author ddj 2026年09月24号
302
+ * @param props 用户目标与关闭动作
303
+ * @param state 任务状态
304
+ * @param phase 当前阶段文案
305
+ * @param closable 能否显式关闭
306
+ * @param dismiss 确认后的关闭动作
307
+ * @returns 标题元素
308
+ */
309
+ function updateHead(props: UpdateProps, state: SvnUpdateState | null, phase: string, closable: boolean, dismiss: () => void): React.ReactElement {
310
+ return React.createElement('header', { className: 'edrv-update-head' },
311
+ React.createElement('div', { className: 'edrv-update-heading' },
312
+ React.createElement('h3', null, 'SVN 更新'),
313
+ React.createElement('span', { className: 'edrv-update-target', title: state?.target ?? props.path }, state?.target || '工作区根')),
314
+ React.createElement('span', { className: 'edrv-update-phase', role: 'status' }, phase),
315
+ React.createElement('button', {
316
+ type: 'button', className: 'edrv-update-close', disabled: !closable,
317
+ title: closable ? '关闭更新窗口' : '更新运行中;请先取消', 'aria-label': '关闭更新窗口', onClick: dismiss,
318
+ }, '✕'))
319
+ }
320
+
321
+ /**
322
+ * 固定列头与虚拟行区域;只渲染当前视口附近的条目。
323
+ * @author ddj 2026年09月24号
324
+ * @param props 工作区路径
325
+ * @param rows 已收到的更新行
326
+ * @param view 虚拟滚动视口
327
+ * @param selection 行选择/菜单
328
+ * @param finished 是否终态
329
+ * @returns 表格元素
330
+ */
331
+ function updateTable(props: UpdateProps, rows: SvnUpdateRow[], view: ReturnType<typeof useUpdateView>,
332
+ selection: ReturnType<typeof useUpdateMenu>, finished: boolean): React.ReactElement {
333
+ const visible = rows.slice(view.range.start, view.range.end)
334
+ return React.createElement('div', { className: 'edrv-update-table', role: 'table', 'aria-label': 'SVN 更新文件明细', 'aria-rowcount': rows.length },
335
+ React.createElement('div', { role: 'row', className: 'edrv-update-cols' },
336
+ React.createElement('span', { role: 'columnheader' }, '操作'),
337
+ React.createElement('span', { role: 'columnheader' }, '路径'),
338
+ React.createElement('span', { role: 'columnheader', title: '仅对已知扩展名推断,非 SVN 属性' }, '类型(推断)')),
339
+ React.createElement('div', { className: 'edrv-update-list', ref: view.listRef, role: 'rowgroup', onScroll: view.onScroll },
340
+ React.createElement('div', { className: 'edrv-update-space', style: { height: rows.length * view.rowHeight } },
341
+ visible.map((row, index) => React.createElement('div', {
342
+ key: row.seq, className: 'edrv-update-pos',
343
+ style: { top: (view.range.start + index) * view.rowHeight, height: view.rowHeight },
344
+ 'aria-rowindex': view.range.start + index + 1,
345
+ }, rowElement(row, props.cwd, selection.picked.has(row.seq),
346
+ (event) => selection.pick(event, row), (event) => selection.openMenu(event, row))))),
347
+ !rows.length ? React.createElement('div', { className: 'edrv-update-empty' },
348
+ finished ? '没有逐文件变更' : '等待 SVN 输出…') : null))
349
+ }
350
+
351
+ /**
352
+ * 真实修订号、耗时和错误回显;输出不完整时醒目标记;
353
+ * 被中断的操作留下 SVN 锁时明确引导先清理,避免用户反复重试。
354
+ * @author ddj 2026年09月24号
355
+ * @param state 任务快照
356
+ * @param error 连接错误
357
+ * @param cleaned 本次窗口内是否已执行过清理
358
+ * @returns 状态元素
359
+ */
360
+ function updateStatus(state: SvnUpdateState | null, error: string, cleaned: boolean): React.ReactElement {
361
+ const duration = state ? (state.elapsedMs / 1000).toFixed(1) + ' 秒' : ''
362
+ const locked = updateLocked(state)
363
+ return React.createElement('div', { className: 'edrv-update-telemetry', role: 'status' },
364
+ error || state?.error ? React.createElement('span', { className: 'edrv-update-error' }, error || state?.error) : null,
365
+ locked ? React.createElement('span', { className: 'edrv-update-locked' },
366
+ cleaned ? '已执行清理,可重新点击「SVN 更新」' : '工作副本被上次中断的操作锁定:请先执行清理再重试') : null,
367
+ React.createElement('span', null, (state?.revision ? 'r' + state.revision + ' · ' : '') + '已耗时 ' + duration),
368
+ state?.truncated ? React.createElement('span', { className: 'edrv-update-warning' }, '输出已截断,列表与计数可能不完整') : null)
369
+ }
370
+
371
+ /**
372
+ * 运行时仅可取消,结束后才可查看日志或关闭;
373
+ * 失败于工作副本锁定时额外提供一次清理入口(不自动重试更新)。
374
+ * @author ddj 2026年09月24号
375
+ * @param props 窗口动作
376
+ * @param state 任务快照
377
+ * @param closable 能否关闭
378
+ * @param finished 是否终态
379
+ * @param cancel 终止请求
380
+ * @param dismiss 明确关闭或暂时退出窗口
381
+ * @param confirmClose 是否要提示后台可能继续运行
382
+ * @param cleaned 本次窗口内是否已执行过清理
383
+ * @param cleanup 请求清理工作副本
384
+ * @returns 操作区
385
+ */
386
+ function updateFoot(props: UpdateProps, state: SvnUpdateState | null, closable: boolean,
387
+ finished: boolean, cancel: () => void, dismiss: () => void, confirmClose: boolean,
388
+ cleaned: boolean, cleanup: () => void): React.ReactElement {
389
+ const locked = updateLocked(state)
390
+ return React.createElement('footer', { className: 'edrv-update-foot' },
391
+ React.createElement('span', { className: 'edrv-update-counts' }, countText(state)),
392
+ locked ? React.createElement('button', {
393
+ type: 'button', className: 'edrv-svn-act edrv-update-cleanup', disabled: !finished || cleaned,
394
+ title: locked && !cleaned ? '执行 svn cleanup,清除中断残留的锁(不撤销已完成的更新)' : '已执行过清理',
395
+ onClick: cleanup,
396
+ }, cleaned ? '已清理' : '执行清理') : null,
397
+ React.createElement('button', { type: 'button', className: 'edrv-svn-act', disabled: !finished || !state,
398
+ onClick: () => state && props.onShowLog(state.target, state.jobId) }, '查看日志'),
399
+ React.createElement('button', { type: 'button', className: 'edrv-svn-act edrv-update-primary', disabled: !closable, onClick: dismiss }, confirmClose ? '暂时关闭' : '确定'),
400
+ React.createElement('button', { type: 'button', className: 'edrv-svn-act',
401
+ disabled: !state || updateDone(state.phase) || state.phase === 'cancelling', onClick: cancel }, '取消更新'))
402
+ }
403
+
404
+ /**
405
+ * 原生 SVN 更新窗口;任务只在用户打开后启动,运行中不静默关闭。
406
+ * @author ddj 2026年09月24号
407
+ * @param props 目标与 EditorView 的复用动作
408
+ * @returns 更新窗口
409
+ */
410
+ export function SvnUpdateDialog(props: UpdateProps): React.ReactElement {
411
+ const { state, rows, error, pendingRows, setState } = useUpdateJob(props)
412
+ const [cleaned, setCleaned] = React.useState(false)
413
+ const view = useUpdateView(rows.length)
414
+ const selection = useUpdateMenu(props, rows, state)
415
+ const processFinished = updateDone(state?.phase)
416
+ const finished = updateReady(state?.phase, pendingRows)
417
+ const closeMode = updateCloseMode(finished, error, Boolean(state), Boolean(selection.menu || props.childOverlay))
418
+ const closable = closeMode.explicit
419
+ /**
420
+ * 确认后请求终止任务;已变更文件不会自动回滚。
421
+ * @author ddj 2026年09月24号
422
+ */
423
+ const cancel = () => {
424
+ if (!state || processFinished || state.phase === 'cancelling') return
425
+ if (!window.confirm('停止 SVN 更新?已更新的文件不会回滚,工作副本可能仅完成一部分。')) return
426
+ void rpc('svn.updateCancel', { sessionId: props.sessionId, jobId: state.jobId }).then((outcome) => {
427
+ if (outcome.ok) setState(outcome)
428
+ else props.onNote(outcome.error)
429
+ }).catch((cause) => props.onNote('取消更新失败:' + String(cause)))
430
+ }
431
+ /**
432
+ * 清理只修复工作副本锁,绝不自动重跑更新;结果经提示条回报。
433
+ * @author ddj 2026年09月28号
434
+ */
435
+ const cleanup = () => {
436
+ if (!updateLocked(state) || cleaned) return
437
+ if (!window.confirm('对工作副本执行 svn cleanup?这会清除上次中断残留的锁,不会撤销已完成的更新。')) return
438
+ void rpc('svn.cleanup', { sessionId: props.sessionId }).then((outcome) => {
439
+ if (outcome.ok) { setCleaned(true); props.onNote('清理完成,可重新执行 SVN 更新') }
440
+ else props.onNote(outcome.error)
441
+ }).catch((cause) => props.onNote('清理失败:' + String(cause)))
442
+ }
443
+ /**
444
+ * 连接中断时只暂时关闭窗口;从不声称 SVN 更新已经停止。
445
+ * @author ddj 2026年09月24号
446
+ */
447
+ const dismiss = () => {
448
+ if (!closeMode.explicit) return
449
+ if (closeMode.confirm && !window.confirm('更新任务连接中断。关闭窗口不会停止可能仍在运行的 SVN 更新,确定暂时关闭?')) return
450
+ props.onClose()
451
+ }
452
+ const phase = pendingRows && processFinished ? '正在加载更新明细…'
453
+ : state?.phase === 'completed' ? '更新完成' : state?.phase === 'failed' ? '更新失败'
454
+ : state?.phase === 'cancelled' ? '更新已停止' : state?.phase === 'cancelling' ? '正在停止…' : '正在更新…'
455
+ return React.createElement(ModalShell, {
456
+ dialogClass: 'edrv-update-dialog', width: 'min(1320px, calc(100vw - 48px))',
457
+ cardStyle: { height: 'min(680px, calc(100vh - 48px))', boxSizing: 'border-box', maxHeight: 'none' },
458
+ closeOnEsc: closeMode.passive, closeOnMask: closeMode.passive, onClose: dismiss,
459
+ },
460
+ updateHead(props, state, phase, closable, dismiss),
461
+ updateTable(props, rows, view, selection, finished),
462
+ updateStatus(state, error, cleaned),
463
+ updateFoot(props, state, closable, finished, cancel, dismiss, closeMode.confirm, cleaned, cleanup),
464
+ state?.rawTail && (state.phase === 'failed' || state.truncated)
465
+ ? React.createElement('details', { className: 'edrv-update-raw' },
466
+ React.createElement('summary', null, '查看原始输出'), React.createElement('pre', null, state.rawTail)) : null,
467
+ selection.menu && state ? React.createElement(ContextMenu, {
468
+ x: selection.menu.x, y: selection.menu.y,
469
+ entries: menuEntries(props, state, selection.menu, rows, selection.selected), zIndex: 193,
470
+ onClose: () => selection.setMenu(null),
471
+ }) : null)
472
+ }