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.
- package/README.md +62 -11
- package/lib/client.js +2879 -1030
- package/lib/client.js.map +1 -1
- package/lib/index.js +2227 -1056
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/activePathStore.ts +28 -0
- package/src/client/commandBridge.ts +9 -41
- package/src/client/dap/hover.ts +2 -7
- package/src/client/index.ts +116 -14
- package/src/client/keybindings.ts +19 -73
- package/src/client/luaReservedWords.ts +10 -0
- package/src/client/monaco/lsp/keywords.ts +121 -0
- package/src/client/monaco/lsp/lspClient.ts +68 -6
- package/src/client/monaco/lsp/memberScope.ts +90 -0
- package/src/client/monaco/lsp/providers.ts +14 -4
- package/src/client/settingsDocRouter.ts +52 -0
- package/src/client/shortcutsOfficial.ts +451 -0
- package/src/client/sidebar/menuItems.ts +1 -0
- package/src/client/sidebar/panels/RulesPanel.ts +15 -11
- package/src/client/sidebar/types.ts +2 -0
- package/src/client/styles/editor.css +143 -37
- package/src/client/svnActions.ts +4 -4
- package/src/client/ui/ContextMenu.ts +4 -2
- package/src/client/ui/EditorView.ts +142 -135
- package/src/client/ui/McpSettings.ts +4 -5
- package/src/client/ui/QuickOpen.ts +10 -11
- package/src/client/ui/ShortcutSettings.ts +294 -0
- package/src/client/ui/SvnLogDialog.ts +200 -69
- package/src/client/ui/SvnUpdateDialog.ts +472 -0
- package/src/client/ui/commandCatalog.ts +36 -41
- package/src/client/ui/svnUpdateModel.ts +135 -0
- package/src/fileOpenSettings.ts +3 -0
- package/src/index.ts +2 -0
- package/src/lsp/completionCap.ts +80 -0
- package/src/lsp/manager.ts +42 -10
- package/src/lsp/rpc.ts +8 -5
- package/src/lsp/server.ts +18 -4
- package/src/rpc.ts +74 -2
- package/src/rules.ts +100 -44
- package/src/settingsDocPath.ts +30 -0
- package/src/shared/keybindings.ts +253 -0
- package/src/shared/rpc.ts +12 -2
- package/src/shared/rules.ts +9 -1
- package/src/shared/svn.ts +38 -0
- package/src/svn.ts +102 -7
- package/src/svnText.ts +22 -13
- package/src/svnUpdateJob.ts +588 -0
- 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
|
+
}
|