dsh-vscode-mode 0.15.0 → 0.15.3
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 +7 -3
- package/lib/client.js +335 -30
- package/lib/client.js.map +1 -1
- package/lib/index.js +221 -8
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/sidebar/panels/SearchPanel.ts +58 -1
- package/src/client/styles/editor.css +4 -0
- package/src/client/svnNotice.ts +1 -1
- package/src/client/ui/SvnUpdateDialog.ts +143 -29
- package/src/client/ui/svnUpdateModel.ts +45 -1
- package/src/rpc.ts +1 -0
- package/src/search/content.ts +6 -2
- package/src/shared/rpc.ts +6 -2
- package/src/shared/svn.ts +10 -0
- package/src/svn.ts +70 -5
- package/src/svnResolve.ts +33 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vscode-mode",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.3",
|
|
4
4
|
"description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 DSH 0.1.5+ 官方右侧 Sidebar 与对话同屏)+ 命令栏(Ctrl+Shift+P)与可扩展指令系统 + VS Code 兼容代码片段(.code-snippets 全局/项目,IntelliSense 展开)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚)+ SVN 集成(侧栏变更面板/日志弹窗/并排差异)+ 语言服务器(LSP)智能(跳转定义/引用/hover/大纲 + VSIX 扩展市场安装),状态持久化到工作区旁车,界面状态(页签/光标/侧栏/编辑 Tab/导航历史)镜像到 DSH home 跨重启恢复",
|
|
5
5
|
"icon": "assets/icon.svg",
|
|
6
6
|
"keywords": [
|
|
@@ -15,6 +15,8 @@ import { workspaceScopeOf } from '../../state/scopeStore.js'
|
|
|
15
15
|
import { takeSearchSeed, takeSearchScope } from '../../searchSeed.js'
|
|
16
16
|
|
|
17
17
|
const DEBOUNCE_MS = 250
|
|
18
|
+
/** 文件变更后的重搜去抖:合并一次写盘的 refresh+file-changed 双事件与批量保存。 */
|
|
19
|
+
const REFRESH_DEBOUNCE_MS = 300
|
|
18
20
|
const INCLUDE_PLACEHOLDER = '例如 *.ts, src/**/include'
|
|
19
21
|
|
|
20
22
|
/**
|
|
@@ -56,6 +58,30 @@ export function savedSearchOf(key: string) {
|
|
|
56
58
|
}
|
|
57
59
|
}
|
|
58
60
|
|
|
61
|
+
/**
|
|
62
|
+
* 判断文件变化通知是否属于本面板作用域。
|
|
63
|
+
* @author ddj 2026年10月14号
|
|
64
|
+
* @param detail 事件 detail(scope 可缺省)
|
|
65
|
+
* @param scope 本面板作用域
|
|
66
|
+
* @returns 是否触发重搜
|
|
67
|
+
*/
|
|
68
|
+
export function noticeInScope(detail: any, scope: string): boolean {
|
|
69
|
+
const changed = detail?.scope
|
|
70
|
+
return !changed || changed === scope
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 判断文件变化通知是否可能改变内容搜索命中。
|
|
75
|
+
* 文件增删改名(structure)会增删命中,一并重搜;SVN 状态刷新(svn)不代表内容变化,跳过以免无谓全库扫描。
|
|
76
|
+
* @author ddj 2026年10月14号
|
|
77
|
+
* @param detail 事件 detail(kind 可缺省)
|
|
78
|
+
* @returns 是否触发重搜
|
|
79
|
+
*/
|
|
80
|
+
export function noticeTouchesContent(detail: any): boolean {
|
|
81
|
+
const kind = detail?.kind
|
|
82
|
+
return !kind || kind === 'text' || kind === 'structure'
|
|
83
|
+
}
|
|
84
|
+
|
|
59
85
|
/**
|
|
60
86
|
* 命中行文本三段切分(before/命中/after,列为 1-based UTF-16)。
|
|
61
87
|
* @param text 行文本
|
|
@@ -103,6 +129,8 @@ export function SearchPanel(props) {
|
|
|
103
129
|
queryRef.current = query
|
|
104
130
|
const activePathRef = React.useRef(activePath)
|
|
105
131
|
activePathRef.current = activePath
|
|
132
|
+
// 文件变化后的重搜定时器(与输入防抖分开:两者互不取消对方)
|
|
133
|
+
const refreshTimerRef = React.useRef(null)
|
|
106
134
|
// 请求参数 ref:防抖/立即重搜闭包统一读最新值,避免陈旧选项
|
|
107
135
|
const requestRef = React.useRef({ matchCase, wholeWord, regex, include: [], exclude: [] })
|
|
108
136
|
requestRef.current = {
|
|
@@ -152,9 +180,11 @@ export function SearchPanel(props) {
|
|
|
152
180
|
|
|
153
181
|
/**
|
|
154
182
|
* 执行内容搜索(请求参数从 ref 读最新值)。
|
|
183
|
+
* @author ddj 2026年10月14号
|
|
155
184
|
* @param text 搜索词
|
|
185
|
+
* @param force 是否强制绕过 host 短期缓存(文件变更后的重搜必须读最新磁盘内容)
|
|
156
186
|
*/
|
|
157
|
-
const runSearch = (text) => {
|
|
187
|
+
const runSearch = (text, force) => {
|
|
158
188
|
const s = ++seqRef.current
|
|
159
189
|
const clean = String(text ?? '').trim()
|
|
160
190
|
if (clean.length < 2) { setMatches(null); setStatus('idle'); return }
|
|
@@ -174,6 +204,8 @@ export function SearchPanel(props) {
|
|
|
174
204
|
sessionId, query: clean,
|
|
175
205
|
matchCase: req.matchCase, wholeWord: req.wholeWord, regex: req.regex,
|
|
176
206
|
include: req.include, exclude: req.exclude,
|
|
207
|
+
// 仅重搜路径带上 force:普通搜索的请求载荷与修复前保持完全一致。
|
|
208
|
+
...(force === true ? { force: true } : {}),
|
|
177
209
|
}).then((res) => {
|
|
178
210
|
if (s !== seqRef.current) return
|
|
179
211
|
if (res && res.ok) {
|
|
@@ -194,6 +226,10 @@ export function SearchPanel(props) {
|
|
|
194
226
|
})
|
|
195
227
|
}
|
|
196
228
|
|
|
229
|
+
// 事件监听闭包经 ref 取最新搜索实现与最新查询词(事件注册只做一次)
|
|
230
|
+
const runSearchRef = React.useRef(runSearch)
|
|
231
|
+
runSearchRef.current = runSearch
|
|
232
|
+
|
|
197
233
|
/**
|
|
198
234
|
* 输入变化:防抖触发搜索。
|
|
199
235
|
* @param value 新输入
|
|
@@ -303,6 +339,27 @@ export function SearchPanel(props) {
|
|
|
303
339
|
return () => window.removeEventListener('edrv:search-focus', onFocus)
|
|
304
340
|
}, [])
|
|
305
341
|
|
|
342
|
+
// 文件内容变化(保存/外部写盘/文件增删改名)→ 去抖强制重搜,避免面板残留已失效的旧命中。
|
|
343
|
+
// 两种事件都监听:编辑器内保存只发 edrv:refresh,外部写盘只发 edrv:file-changed。
|
|
344
|
+
React.useEffect(() => {
|
|
345
|
+
/** @private @author ddj 2026年10月14号 @description 合并同一次写盘的多条通知后强制重搜 */
|
|
346
|
+
const onChanged = (event) => {
|
|
347
|
+
const detail = event?.detail ?? {}
|
|
348
|
+
if (!noticeInScope(detail, scope)) return
|
|
349
|
+
if (!noticeTouchesContent(detail)) return
|
|
350
|
+
if (refreshTimerRef.current) clearTimeout(refreshTimerRef.current)
|
|
351
|
+
refreshTimerRef.current = setTimeout(() => runSearchRef.current(queryRef.current, true), REFRESH_DEBOUNCE_MS)
|
|
352
|
+
}
|
|
353
|
+
window.addEventListener('edrv:file-changed', onChanged)
|
|
354
|
+
window.addEventListener('edrv:refresh', onChanged)
|
|
355
|
+
return () => {
|
|
356
|
+
window.removeEventListener('edrv:file-changed', onChanged)
|
|
357
|
+
window.removeEventListener('edrv:refresh', onChanged)
|
|
358
|
+
if (refreshTimerRef.current) { clearTimeout(refreshTimerRef.current); refreshTimerRef.current = null }
|
|
359
|
+
}
|
|
360
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
361
|
+
}, [])
|
|
362
|
+
|
|
306
363
|
// 一次意图对应一次提交后选区;无需定时回调,卸载后不会抢焦点。
|
|
307
364
|
React.useLayoutEffect(() => {
|
|
308
365
|
if (!focusTick) return
|
|
@@ -640,6 +640,10 @@
|
|
|
640
640
|
[data-edrv-view] .edrv-update-close:not(:disabled):hover { background: var(--dsw-alias-interactive-bg-hover, #e7edf0); }
|
|
641
641
|
[data-edrv-view] .edrv-update-table { display: flex; flex-direction: column; flex: 1; min-height: 0; margin: 12px 14px 0; border: 1px solid var(--dsw-alias-border-l1, #d9e0e4); border-radius: 5px; overflow: hidden; background: var(--dsw-alias-bg-layer-1, #fff); }
|
|
642
642
|
[data-edrv-view] .edrv-update-cols, [data-edrv-view] .edrv-update-row { display: grid; grid-template-columns: 110px minmax(0, 1fr) 160px; align-items: center; }
|
|
643
|
+
[data-edrv-view] .edrv-update-sort { width: 100%; padding: 0; border: 0; background: transparent; color: inherit; font: inherit; text-align: left; cursor: pointer; }
|
|
644
|
+
[data-edrv-view] .edrv-update-sort:hover { color: var(--dsw-alias-brand-primary, #2869b4); }
|
|
645
|
+
[data-edrv-view] .edrv-update-sort:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary, #2869b4); outline-offset: 2px; }
|
|
646
|
+
[data-edrv-view] .edrv-update-jump { color: var(--dsw-alias-state-error-primary, #b93b43); }
|
|
643
647
|
[data-edrv-view] .edrv-update-cols { flex: none; height: 30px; background: var(--dsw-alias-bg-layer-2, #f1f4f5); border-bottom: 1px solid var(--dsw-alias-border-l1, #d9e0e4); color: var(--dsw-alias-label-secondary, #485762); font-size: 11px; font-weight: 600; }
|
|
644
648
|
[data-edrv-view] .edrv-update-cols > span, [data-edrv-view] .edrv-update-row > span { min-width: 0; padding: 0 10px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
645
649
|
[data-edrv-view] .edrv-update-cols > span + span, [data-edrv-view] .edrv-update-row > span + span { border-left: 1px solid var(--dsw-alias-border-l1, #d9e0e4); }
|
package/src/client/svnNotice.ts
CHANGED
|
@@ -8,7 +8,7 @@ export function bindSvnScope(sessionId: string | undefined, scope: string): void
|
|
|
8
8
|
scopes.delete(sessionId); scopes.set(sessionId, scope)
|
|
9
9
|
if (scopes.size > 512) scopes.delete(scopes.keys().next().value!)
|
|
10
10
|
}
|
|
11
|
-
const WRITES = new Set<RpcMethod>(['svn.add', 'svn.revert', 'svn.ignore', 'svn.cleanup', 'svn.update', 'svn.tortoise'])
|
|
11
|
+
const WRITES = new Set<RpcMethod>(['svn.add', 'svn.revert', 'svn.resolve', 'svn.ignore', 'svn.cleanup', 'svn.update', 'svn.tortoise'])
|
|
12
12
|
/** @public @author ddj 2026年10月10号 @param method RPC operation. @returns Whether local evidence may change (launching a writable native dialog counts, not its completion). */
|
|
13
13
|
export function isSvnWrite(method: RpcMethod): boolean { return WRITES.has(method) }
|
|
14
14
|
/** @public @author ddj 2026年10月10号 @param method Operation. @param args Typed request. @param ok Completed successfully. Notify even failed/partially applied operations; no repository access. */
|
|
@@ -3,14 +3,15 @@
|
|
|
3
3
|
* @author ddj 2026年09月24号
|
|
4
4
|
*/
|
|
5
5
|
import React from 'react'
|
|
6
|
-
import type { SvnUpdateRow, SvnUpdateState } from '../../shared/svn.js'
|
|
6
|
+
import type { SvnConflictState, SvnResolveAccept, SvnUpdateRow, SvnUpdateState } from '../../shared/svn.js'
|
|
7
7
|
import { copyText } from '../copyText.js'
|
|
8
8
|
import { rpc } from '../rpc.js'
|
|
9
9
|
import { absoluteOf } from '../tabActions.js'
|
|
10
10
|
import { ContextMenu } from './ContextMenu.js'
|
|
11
11
|
import type { ContextMenuEntry } from './ContextMenu.js'
|
|
12
12
|
import { ModalShell } from './ModalShell.js'
|
|
13
|
-
import { updateActs, updateCloseMode, updateDone, updateLocked, updateParent, updateRange, updateReady, updateType } from './svnUpdateModel.js'
|
|
13
|
+
import { nextConflict, nextUpdateSort, rowConflict, sortUpdateRows, updateActs, updateCloseMode, updateDone, updateLocked, updateParent, updateRange, updateReady, updateType } from './svnUpdateModel.js'
|
|
14
|
+
import type { UpdateColumn, UpdateSort } from './svnUpdateModel.js'
|
|
14
15
|
|
|
15
16
|
interface UpdateProps {
|
|
16
17
|
sessionId?: string
|
|
@@ -28,6 +29,68 @@ interface UpdateProps {
|
|
|
28
29
|
onOpenParent: (path: string) => void
|
|
29
30
|
}
|
|
30
31
|
interface UpdateMenu { x: number; y: number; row: SvnUpdateRow; kind: string }
|
|
32
|
+
type ConflictChoice = SvnResolveAccept | 'revert'
|
|
33
|
+
const conflictWarnings: Record<ConflictChoice, string> = {
|
|
34
|
+
'theirs-full': '使用对方完整版本,丢弃该目标本地冲突修改?',
|
|
35
|
+
'mine-full': '使用更新前自身完整版本,丢弃对方修改以及更新后编辑?这不是当前 working 内容。',
|
|
36
|
+
working: '仅标记为已解决,保留当前内容。SVN 不会自动清除文件内冲突标记;树冲突必须先手动处理。确定?',
|
|
37
|
+
revert: '还原此目标到当前 BASE,丢弃本地修改和冲突状态?这不是回到更新前版本;目录不递归。',
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** @private @author ddj 2026年10月14号 @param props Owner. @param paths Unique exact targets. @returns Evidence and bounded refresh action. */
|
|
41
|
+
function useConflictState(props: UpdateProps, paths: string[], ready: boolean) {
|
|
42
|
+
const [states, setStates] = React.useState<Map<string, SvnConflictState>>(new Map())
|
|
43
|
+
const [error, setError] = React.useState('')
|
|
44
|
+
const [loading, setLoading] = React.useState(false)
|
|
45
|
+
const generation = React.useRef(0)
|
|
46
|
+
/** @private @author ddj 2026年10月14号 @returns Fresh evidence; failed targets remain unknown rather than clean. */
|
|
47
|
+
const refresh = React.useCallback(async () => {
|
|
48
|
+
const token = ++generation.current
|
|
49
|
+
setLoading(true)
|
|
50
|
+
const evidence = new Map<string, SvnConflictState>()
|
|
51
|
+
const errors: string[] = []
|
|
52
|
+
for (let start = 0; start < paths.length; start += 100) {
|
|
53
|
+
try {
|
|
54
|
+
const result = await rpc('svn.conflictStatus', { sessionId: props.sessionId, paths: paths.slice(start, start + 100) })
|
|
55
|
+
if (result.ok) result.states.forEach((state) => evidence.set(state.path, state))
|
|
56
|
+
else errors.push(result.error)
|
|
57
|
+
} catch (cause) { errors.push(String(cause)) }
|
|
58
|
+
}
|
|
59
|
+
if (token !== generation.current) return
|
|
60
|
+
setStates(evidence); setError(errors.join('\n')); setLoading(false)
|
|
61
|
+
}, [props.sessionId, paths])
|
|
62
|
+
React.useEffect(() => {
|
|
63
|
+
setStates(new Map()); setError('')
|
|
64
|
+
if (ready) void refresh()
|
|
65
|
+
return () => { generation.current++ }
|
|
66
|
+
}, [ready, refresh])
|
|
67
|
+
return { states, error, refresh, setStates, loading }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** @private @author ddj 2026年10月14号 @param props Owner. @param ready Complete output. @param evidence Conflict reader. @returns Single-target confirmed mutation state and command. */
|
|
71
|
+
function useConflictWrite(props: UpdateProps, ready: boolean, evidence: ReturnType<typeof useConflictState>) {
|
|
72
|
+
const [busy, setBusy] = React.useState(false)
|
|
73
|
+
const lock = React.useRef(false)
|
|
74
|
+
/** @private @author ddj 2026年10月14号 @param row Right-click target only. @param choice Explicit SVN operation. Execute once and always re-read status. */
|
|
75
|
+
const execute = async (row: SvnUpdateRow, choice: ConflictChoice) => {
|
|
76
|
+
if (lock.current || !ready || evidence.loading || !row.relPath) return
|
|
77
|
+
const state = evidence.states.get(row.relPath)
|
|
78
|
+
if (!state || !(state.text || state.props || state.tree)) return
|
|
79
|
+
if (state.tree && (choice === 'mine-full' || choice === 'theirs-full')) return
|
|
80
|
+
lock.current = true
|
|
81
|
+
try {
|
|
82
|
+
if (!window.confirm(conflictWarnings[choice] + '\n\n仅处理:' + row.relPath)) { lock.current = false; return }
|
|
83
|
+
setBusy(true)
|
|
84
|
+
evidence.setStates((previous) => { const next = new Map(previous); next.delete(row.relPath!); return next })
|
|
85
|
+
const result = choice === 'revert'
|
|
86
|
+
? await rpc('svn.revert', { sessionId: props.sessionId, paths: [row.relPath], depth: 'empty' })
|
|
87
|
+
: await rpc('svn.resolve', { sessionId: props.sessionId, path: row.relPath, accept: choice })
|
|
88
|
+
props.onNote(result.ok ? result.summary : result.error)
|
|
89
|
+
} catch (cause) { props.onNote('冲突操作失败:' + String(cause)) }
|
|
90
|
+
finally { if (lock.current) { await evidence.refresh(); lock.current = false; setBusy(false) } }
|
|
91
|
+
}
|
|
92
|
+
return { busy, execute }
|
|
93
|
+
}
|
|
31
94
|
|
|
32
95
|
/**
|
|
33
96
|
* 仅恢复明确匹配的原任务;失败时不重新触发一次 SVN 更新。
|
|
@@ -159,10 +222,22 @@ function countText(state: SvnUpdateState | null): string {
|
|
|
159
222
|
* @param selected 已选序号
|
|
160
223
|
* @returns 可点击动作列表
|
|
161
224
|
*/
|
|
162
|
-
function menuEntries(props: UpdateProps, state: SvnUpdateState, menu: UpdateMenu, rows: SvnUpdateRow[], selected: number[]
|
|
225
|
+
function menuEntries(props: UpdateProps, state: SvnUpdateState, menu: UpdateMenu, rows: SvnUpdateRow[], selected: number[],
|
|
226
|
+
ready: boolean, evidence: ReturnType<typeof useConflictState>, write: ReturnType<typeof useConflictWrite>): ContextMenuEntry[] {
|
|
163
227
|
const { row, kind } = menu
|
|
164
228
|
const actions = updateActs(row, state.phase, kind, state.revision)
|
|
165
229
|
const rel = row.relPath ?? ''
|
|
230
|
+
const conflict = rel ? evidence.states.get(rel) : undefined
|
|
231
|
+
const enabled = ready && !write.busy && !evidence.loading && Boolean(conflict && (conflict.text || conflict.props || conflict.tree))
|
|
232
|
+
const full = enabled && !conflict?.tree
|
|
233
|
+
const conflictEntries: ContextMenuEntry[] = row.action === 'C' ? [
|
|
234
|
+
{ id: 'theirs-full', label: '使用对方完整版本解决', separator: true, disabled: !full, onClick: () => { void write.execute(row, 'theirs-full') } },
|
|
235
|
+
{ id: 'mine-full', label: '使用自身完整版本解决', disabled: !full, onClick: () => { void write.execute(row, 'mine-full') } },
|
|
236
|
+
{ id: 'working', label: '标记为已解决(保留当前内容)', disabled: !enabled, onClick: () => { void write.execute(row, 'working') } },
|
|
237
|
+
{ id: 'revert', label: '还原此项…', disabled: !enabled, onClick: () => { void write.execute(row, 'revert') } },
|
|
238
|
+
...(conflict?.tree ? [{ id: 'tree-hint', label: '树冲突:请先针对性处理,再标记解决', disabled: true, onClick: () => {} }] : []),
|
|
239
|
+
...(!conflict && ready ? [{ id: 'unknown-hint', label: '状态未确认:请刷新冲突状态', disabled: true, onClick: () => {} }] : []),
|
|
240
|
+
] : []
|
|
166
241
|
const picked = new Set(selected)
|
|
167
242
|
const names = rows.filter((item) => picked.has(item.seq)).map((item) => item.relPath
|
|
168
243
|
? absoluteOf(item.relPath, props.cwd) : item.path)
|
|
@@ -172,6 +247,7 @@ function menuEntries(props: UpdateProps, state: SvnUpdateState, menu: UpdateMenu
|
|
|
172
247
|
{ id: 'open', label: '打开', disabled: !actions.open, separator: true, onClick: () => props.onOpenFile(rel) },
|
|
173
248
|
{ id: 'open-with', label: '打开方式…', disabled: !actions.openWith, onClick: () => props.onOpenWith(rel) },
|
|
174
249
|
{ id: 'parent', label: '打开父目录', disabled: !actions.parent, onClick: () => props.onOpenParent(updateParent(rel)) },
|
|
250
|
+
...conflictEntries,
|
|
175
251
|
{ id: 'copy', label: selected.length > 1 ? '复制选中路径' : '复制路径', disabled: !actions.copy, separator: true,
|
|
176
252
|
onClick: () => { void copyText(names.join('\n'), '已复制路径', props.onNote) } },
|
|
177
253
|
]
|
|
@@ -189,7 +265,7 @@ function menuEntries(props: UpdateProps, state: SvnUpdateState, menu: UpdateMenu
|
|
|
189
265
|
*/
|
|
190
266
|
function rowElement(row: SvnUpdateRow, cwd: string | null | undefined, picked: boolean,
|
|
191
267
|
onPick: (event: React.MouseEvent<HTMLDivElement>) => void,
|
|
192
|
-
onMenu: (event: React.MouseEvent<HTMLDivElement>) => void): React.ReactElement {
|
|
268
|
+
onMenu: (event: React.MouseEvent<HTMLDivElement>) => void, resolved: boolean): React.ReactElement {
|
|
193
269
|
// 显示用相对路径(窄窗省略尾部时先保住文件名),悬浮与复制仍用完整绝对路径。
|
|
194
270
|
const full = row.relPath ? absoluteOf(row.relPath, cwd) : (row.path || row.label)
|
|
195
271
|
const path = row.relPath || row.path || row.label
|
|
@@ -199,7 +275,7 @@ function rowElement(row: SvnUpdateRow, cwd: string | null | undefined, picked: b
|
|
|
199
275
|
role: 'row', 'aria-selected': picked, className: 'edrv-update-row' + (picked ? ' is-selected' : ''),
|
|
200
276
|
onClick: onPick, onContextMenu: onMenu, title: row.raw,
|
|
201
277
|
},
|
|
202
|
-
React.createElement('span', { role: 'cell', className: 'edrv-update-action tone-' + tone }, row.action ? row.label : '信息'),
|
|
278
|
+
React.createElement('span', { role: 'cell', className: 'edrv-update-action tone-' + tone }, resolved ? row.label + '(已解决)' : row.action ? row.label : '信息'),
|
|
203
279
|
React.createElement('span', { role: 'cell', className: 'edrv-update-path', title: full }, path),
|
|
204
280
|
React.createElement('span', { role: 'cell', className: 'edrv-update-type' }, row.action ? updateType(row.path) : ''))
|
|
205
281
|
}
|
|
@@ -210,7 +286,7 @@ function rowElement(row: SvnUpdateRow, cwd: string | null | undefined, picked: b
|
|
|
210
286
|
* @param count 当前记录数
|
|
211
287
|
* @returns 虚拟列表视口、行高和滚动回调
|
|
212
288
|
*/
|
|
213
|
-
function useUpdateView(count: number) {
|
|
289
|
+
function useUpdateView(count: number, sorted: boolean) {
|
|
214
290
|
const [scrollTop, setScrollTop] = React.useState(0)
|
|
215
291
|
const [height, setHeight] = React.useState(350)
|
|
216
292
|
const [compact, setCompact] = React.useState(() => window.innerWidth < 620)
|
|
@@ -229,8 +305,8 @@ function useUpdateView(count: number) {
|
|
|
229
305
|
}, [])
|
|
230
306
|
React.useLayoutEffect(() => {
|
|
231
307
|
const list = listRef.current
|
|
232
|
-
if (list && followRef.current) { list.scrollTop = list.scrollHeight; setScrollTop(list.scrollTop) }
|
|
233
|
-
}, [count, rowHeight])
|
|
308
|
+
if (list && followRef.current && !sorted) { list.scrollTop = list.scrollHeight; setScrollTop(list.scrollTop) }
|
|
309
|
+
}, [count, rowHeight, sorted])
|
|
234
310
|
/**
|
|
235
311
|
* 用户离开底部后暂停自动滚动,保持当前文件可读。
|
|
236
312
|
* @author ddj 2026年09月24号
|
|
@@ -241,7 +317,15 @@ function useUpdateView(count: number) {
|
|
|
241
317
|
followRef.current = list.scrollHeight - list.scrollTop - list.clientHeight < 40
|
|
242
318
|
setScrollTop(list.scrollTop)
|
|
243
319
|
}
|
|
244
|
-
|
|
320
|
+
/** @private @author ddj 2026年10月14号 @param index Display index. Scroll without requiring a mounted virtual row. */
|
|
321
|
+
const locate = (index: number) => {
|
|
322
|
+
const list = listRef.current
|
|
323
|
+
if (!list || index < 0) return
|
|
324
|
+
followRef.current = false
|
|
325
|
+
list.scrollTop = index * rowHeight
|
|
326
|
+
setScrollTop(list.scrollTop)
|
|
327
|
+
}
|
|
328
|
+
return { listRef, rowHeight, range: updateRange(count, scrollTop, height, rowHeight), onScroll, locate }
|
|
245
329
|
}
|
|
246
330
|
|
|
247
331
|
/**
|
|
@@ -266,14 +350,15 @@ function useUpdateMenu(props: UpdateProps, rows: SvnUpdateRow[], state: SvnUpdat
|
|
|
266
350
|
const pick = (event: React.MouseEvent<HTMLDivElement>, row: SvnUpdateRow) => {
|
|
267
351
|
const index = rows.findIndex((item) => item.seq === row.seq)
|
|
268
352
|
if (event.shiftKey) {
|
|
269
|
-
const
|
|
270
|
-
const
|
|
353
|
+
const anchor = rows.findIndex((item) => item.seq === anchorRef.current)
|
|
354
|
+
const from = Math.min(index, anchor < 0 ? index : anchor)
|
|
355
|
+
const to = Math.max(index, anchor < 0 ? index : anchor)
|
|
271
356
|
setSelected(rows.slice(from, to + 1).map((item) => item.seq))
|
|
272
357
|
} else if (event.ctrlKey || event.metaKey) {
|
|
273
358
|
setSelected((previous) => previous.includes(row.seq)
|
|
274
359
|
? previous.filter((seq) => seq !== row.seq) : previous.concat(row.seq))
|
|
275
|
-
anchorRef.current =
|
|
276
|
-
} else { setSelected([row.seq]); anchorRef.current =
|
|
360
|
+
anchorRef.current = row.seq
|
|
361
|
+
} else { setSelected([row.seq]); anchorRef.current = row.seq }
|
|
277
362
|
}
|
|
278
363
|
/**
|
|
279
364
|
* 先开放父目录/复制,再按真实磁盘类型启用文件级操作。
|
|
@@ -293,7 +378,9 @@ function useUpdateMenu(props: UpdateProps, rows: SvnUpdateRow[], state: SvnUpdat
|
|
|
293
378
|
? { ...current, kind: outcome.ok ? outcome.kind : 'missing' } : current)
|
|
294
379
|
}).catch(() => {})
|
|
295
380
|
}
|
|
296
|
-
|
|
381
|
+
/** @private @author ddj 2026年10月14号 @param row Navigation target. Select one stable row identity. */
|
|
382
|
+
const selectRow = (row: SvnUpdateRow) => { setSelected([row.seq]); anchorRef.current = row.seq }
|
|
383
|
+
return { selected, picked, menu, setMenu, pick, openMenu, selectRow }
|
|
297
384
|
}
|
|
298
385
|
|
|
299
386
|
/**
|
|
@@ -329,13 +416,18 @@ function updateHead(props: UpdateProps, state: SvnUpdateState | null, phase: str
|
|
|
329
416
|
* @returns 表格元素
|
|
330
417
|
*/
|
|
331
418
|
function updateTable(props: UpdateProps, rows: SvnUpdateRow[], view: ReturnType<typeof useUpdateView>,
|
|
332
|
-
selection: ReturnType<typeof useUpdateMenu>, finished: boolean
|
|
419
|
+
selection: ReturnType<typeof useUpdateMenu>, finished: boolean, sort: UpdateSort | null,
|
|
420
|
+
onSort: (column: UpdateColumn) => void, states: ReadonlyMap<string, SvnConflictState>): React.ReactElement {
|
|
333
421
|
const visible = rows.slice(view.range.start, view.range.end)
|
|
334
422
|
return React.createElement('div', { className: 'edrv-update-table', role: 'table', 'aria-label': 'SVN 更新文件明细', 'aria-rowcount': rows.length },
|
|
335
423
|
React.createElement('div', { role: 'row', className: 'edrv-update-cols' },
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
424
|
+
(['action', 'path', 'type'] as const).map((column) => {
|
|
425
|
+
const direction = sort?.column === column ? sort.direction : 0
|
|
426
|
+
const label = column === 'action' ? '操作' : column === 'path' ? '路径' : '类型(推断)'
|
|
427
|
+
return React.createElement('span', { key: column, role: 'columnheader', 'aria-sort': direction === 1 ? 'ascending' : direction === -1 ? 'descending' : 'none' },
|
|
428
|
+
React.createElement('button', { type: 'button', className: 'edrv-update-sort', onClick: () => onSort(column),
|
|
429
|
+
title: '点击切换升序、降序、原始顺序' + (column === 'type' ? ';仅推断类型,非 SVN 属性' : '') }, label + (direction === 1 ? ' ▲' : direction === -1 ? ' ▼' : ' ↕')))
|
|
430
|
+
})),
|
|
339
431
|
React.createElement('div', { className: 'edrv-update-list', ref: view.listRef, role: 'rowgroup', onScroll: view.onScroll },
|
|
340
432
|
React.createElement('div', { className: 'edrv-update-space', style: { height: rows.length * view.rowHeight } },
|
|
341
433
|
visible.map((row, index) => React.createElement('div', {
|
|
@@ -343,7 +435,8 @@ function updateTable(props: UpdateProps, rows: SvnUpdateRow[], view: ReturnType<
|
|
|
343
435
|
style: { top: (view.range.start + index) * view.rowHeight, height: view.rowHeight },
|
|
344
436
|
'aria-rowindex': view.range.start + index + 1,
|
|
345
437
|
}, rowElement(row, props.cwd, selection.picked.has(row.seq),
|
|
346
|
-
(event) => selection.pick(event, row), (event) => selection.openMenu(event, row)
|
|
438
|
+
(event) => selection.pick(event, row), (event) => selection.openMenu(event, row),
|
|
439
|
+
row.action === 'C' && !rowConflict(row, states))))),
|
|
347
440
|
!rows.length ? React.createElement('div', { className: 'edrv-update-empty' },
|
|
348
441
|
finished ? '没有逐文件变更' : '等待 SVN 输出…') : null))
|
|
349
442
|
}
|
|
@@ -385,10 +478,12 @@ function updateStatus(state: SvnUpdateState | null, error: string, cleaned: bool
|
|
|
385
478
|
*/
|
|
386
479
|
function updateFoot(props: UpdateProps, state: SvnUpdateState | null, closable: boolean,
|
|
387
480
|
finished: boolean, cancel: () => void, dismiss: () => void, confirmClose: boolean,
|
|
388
|
-
cleaned: boolean, cleanup: () => void): React.ReactElement {
|
|
481
|
+
cleaned: boolean, cleanup: () => void, remaining: number, jump: () => void, refresh: () => void): React.ReactElement {
|
|
389
482
|
const locked = updateLocked(state)
|
|
390
483
|
return React.createElement('footer', { className: 'edrv-update-foot' },
|
|
391
484
|
React.createElement('span', { className: 'edrv-update-counts' }, countText(state)),
|
|
485
|
+
remaining > 0 ? React.createElement('button', { type: 'button', className: 'edrv-svn-act edrv-update-jump', onClick: jump }, '跳转冲突(' + remaining + ')') : null,
|
|
486
|
+
state?.counts.C ? React.createElement('button', { type: 'button', className: 'edrv-svn-act', disabled: !finished, onClick: refresh }, '刷新冲突状态') : null,
|
|
392
487
|
locked ? React.createElement('button', {
|
|
393
488
|
type: 'button', className: 'edrv-svn-act edrv-update-cleanup', disabled: !finished || cleaned,
|
|
394
489
|
title: locked && !cleaned ? '执行 svn cleanup,清除中断残留的锁(不撤销已完成的更新)' : '已执行过清理',
|
|
@@ -410,12 +505,29 @@ function updateFoot(props: UpdateProps, state: SvnUpdateState | null, closable:
|
|
|
410
505
|
export function SvnUpdateDialog(props: UpdateProps): React.ReactElement {
|
|
411
506
|
const { state, rows, error, pendingRows, setState } = useUpdateJob(props)
|
|
412
507
|
const [cleaned, setCleaned] = React.useState(false)
|
|
413
|
-
const
|
|
414
|
-
const
|
|
508
|
+
const [sort, setSort] = React.useState<UpdateSort | null>(null)
|
|
509
|
+
const displayRows = React.useMemo(() => sortUpdateRows(rows, sort), [rows, sort])
|
|
510
|
+
const view = useUpdateView(displayRows.length, Boolean(sort))
|
|
511
|
+
const selection = useUpdateMenu(props, displayRows, state)
|
|
415
512
|
const processFinished = updateDone(state?.phase)
|
|
416
513
|
const finished = updateReady(state?.phase, pendingRows)
|
|
417
|
-
const
|
|
418
|
-
const
|
|
514
|
+
const conflictPaths = React.useMemo(() => [...new Set(rows.filter((row) => row.action === 'C' && row.relPath).map((row) => row.relPath!))], [rows])
|
|
515
|
+
const evidence = useConflictState(props, conflictPaths, finished)
|
|
516
|
+
const write = useConflictWrite(props, finished, evidence)
|
|
517
|
+
const remaining = new Set(rows.filter((row) => rowConflict(row, evidence.states)).map((row) => row.relPath || row.path)).size
|
|
518
|
+
const outside = rows.some((row) => row.action === 'C' && !row.relPath)
|
|
519
|
+
const closeMode = updateCloseMode(finished, error, Boolean(state), Boolean(selection.menu || props.childOverlay || write.busy))
|
|
520
|
+
const closable = closeMode.explicit && !write.busy
|
|
521
|
+
/** @private @author ddj 2026年10月14号 @param column Header identity. Cycle cached display ordering. */
|
|
522
|
+
const onSort = (column: UpdateColumn) => setSort((previous) => nextUpdateSort(previous, column))
|
|
523
|
+
/** @private @author ddj 2026年10月14号 Select and scroll to the next unresolved row in display order. */
|
|
524
|
+
const jump = () => {
|
|
525
|
+
const index = nextConflict(displayRows, evidence.states, selection.selected.at(-1))
|
|
526
|
+
if (index < 0) return
|
|
527
|
+
selection.selectRow(displayRows[index]); view.locate(index)
|
|
528
|
+
}
|
|
529
|
+
/** @private @author ddj 2026年10月14号 Refresh exact targets after an external resolution. */
|
|
530
|
+
const refresh = () => { if (!write.busy && finished) void evidence.refresh() }
|
|
419
531
|
/**
|
|
420
532
|
* 确认后请求终止任务;已变更文件不会自动回滚。
|
|
421
533
|
* @author ddj 2026年09月24号
|
|
@@ -445,7 +557,7 @@ export function SvnUpdateDialog(props: UpdateProps): React.ReactElement {
|
|
|
445
557
|
* @author ddj 2026年09月24号
|
|
446
558
|
*/
|
|
447
559
|
const dismiss = () => {
|
|
448
|
-
if (!
|
|
560
|
+
if (!closable) return
|
|
449
561
|
if (closeMode.confirm && !window.confirm('更新任务连接中断。关闭窗口不会停止可能仍在运行的 SVN 更新,确定暂时关闭?')) return
|
|
450
562
|
props.onClose()
|
|
451
563
|
}
|
|
@@ -458,15 +570,17 @@ export function SvnUpdateDialog(props: UpdateProps): React.ReactElement {
|
|
|
458
570
|
closeOnEsc: closeMode.passive, closeOnMask: closeMode.passive, onClose: dismiss,
|
|
459
571
|
},
|
|
460
572
|
updateHead(props, state, phase, closable, dismiss),
|
|
461
|
-
updateTable(props,
|
|
462
|
-
updateStatus(state, error, cleaned),
|
|
463
|
-
|
|
573
|
+
updateTable(props, displayRows, view, selection, finished, sort, onSort, evidence.states),
|
|
574
|
+
updateStatus(state, error || evidence.error, cleaned),
|
|
575
|
+
outside ? React.createElement('div', { className: 'edrv-update-warning' }, '存在工作区外冲突;可定位但不能在此执行解决操作') : null,
|
|
576
|
+
write.busy ? React.createElement('div', { role: 'status' }, '正在处理冲突并核对 SVN 状态…') : null,
|
|
577
|
+
updateFoot(props, state, closable, finished && !write.busy, cancel, dismiss, closeMode.confirm, cleaned, cleanup, remaining, jump, refresh),
|
|
464
578
|
state?.rawTail && (state.phase === 'failed' || state.truncated)
|
|
465
579
|
? React.createElement('details', { className: 'edrv-update-raw' },
|
|
466
580
|
React.createElement('summary', null, '查看原始输出'), React.createElement('pre', null, state.rawTail)) : null,
|
|
467
581
|
selection.menu && state ? React.createElement(ContextMenu, {
|
|
468
582
|
x: selection.menu.x, y: selection.menu.y,
|
|
469
|
-
entries: menuEntries(props, state, selection.menu,
|
|
583
|
+
entries: menuEntries(props, state, selection.menu, displayRows, selection.selected, finished, evidence, write), zIndex: 193,
|
|
470
584
|
onClose: () => selection.setMenu(null),
|
|
471
585
|
}) : null)
|
|
472
586
|
}
|
|
@@ -2,7 +2,51 @@
|
|
|
2
2
|
* dsh-vscode-mode client — SVN 更新窗口的纯视图逻辑。
|
|
3
3
|
* @author ddj 2026年09月24号
|
|
4
4
|
*/
|
|
5
|
-
import type { SvnUpdatePhase, SvnUpdateRow } from '../../shared/svn.js'
|
|
5
|
+
import type { SvnConflictState, SvnUpdatePhase, SvnUpdateRow } from '../../shared/svn.js'
|
|
6
|
+
|
|
7
|
+
export type UpdateColumn = 'action' | 'path' | 'type'
|
|
8
|
+
export interface UpdateSort { column: UpdateColumn; direction: 1 | -1 }
|
|
9
|
+
const collator = new Intl.Collator('zh-CN', { numeric: true, sensitivity: 'base' })
|
|
10
|
+
|
|
11
|
+
/** @public @author ddj 2026年10月14号 @param sort Current sort. @param column Clicked header. @returns Ascending, descending, then original output order. */
|
|
12
|
+
export function nextUpdateSort(sort: UpdateSort | null, column: UpdateColumn): UpdateSort | null {
|
|
13
|
+
if (sort?.column !== column) return { column, direction: 1 }
|
|
14
|
+
return sort.direction === 1 ? { column, direction: -1 } : null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** @public @author ddj 2026年10月14号 @param row Output row. @param column Display column. @returns Display sort key. */
|
|
18
|
+
function updateKey(row: SvnUpdateRow, column: UpdateColumn): string {
|
|
19
|
+
if (column === 'action') return row.action ? row.label : '信息'
|
|
20
|
+
if (column === 'type') return row.action ? updateType(row.path) : ''
|
|
21
|
+
return row.relPath || row.path || row.label
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** @public @author ddj 2026年10月14号 @param rows Immutable original rows. @param sort Optional sort. @returns Stable derived display order, empty keys last. */
|
|
25
|
+
export function sortUpdateRows(rows: SvnUpdateRow[], sort: UpdateSort | null): SvnUpdateRow[] {
|
|
26
|
+
if (!sort) return rows
|
|
27
|
+
return rows.slice().sort((a, b) => {
|
|
28
|
+
const left = updateKey(a, sort.column), right = updateKey(b, sort.column)
|
|
29
|
+
if (!left || !right) return left ? -1 : right ? 1 : a.seq - b.seq
|
|
30
|
+
return collator.compare(left, right) * sort.direction || a.seq - b.seq
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** @public @author ddj 2026年10月14号 @param row Output row. @param states Confirmed evidence by path. @returns Conservative unresolved status, including unknown/outside paths. */
|
|
35
|
+
export function rowConflict(row: SvnUpdateRow, states: ReadonlyMap<string, SvnConflictState>): boolean {
|
|
36
|
+
if (row.action !== 'C') return false
|
|
37
|
+
const state = row.relPath ? states.get(row.relPath) : undefined
|
|
38
|
+
return state ? state.text || state.props || state.tree : true
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** @public @author ddj 2026年10月14号 @param rows Display order. @param states Confirmed evidence. @param seq Current selection. @returns Next conflict index, wrapping around. */
|
|
42
|
+
export function nextConflict(rows: SvnUpdateRow[], states: ReadonlyMap<string, SvnConflictState>, seq?: number): number {
|
|
43
|
+
const start = rows.findIndex((row) => row.seq === seq)
|
|
44
|
+
for (let step = 1; step <= rows.length; step++) {
|
|
45
|
+
const index = (start + step) % rows.length
|
|
46
|
+
if (rowConflict(rows[index], states)) return index
|
|
47
|
+
}
|
|
48
|
+
return -1
|
|
49
|
+
}
|
|
6
50
|
|
|
7
51
|
/**
|
|
8
52
|
* 只有真实终态允许直接关闭窗口。
|
package/src/rpc.ts
CHANGED
package/src/search/content.ts
CHANGED
|
@@ -45,6 +45,8 @@ export interface ContentSearchRequest {
|
|
|
45
45
|
include?: string[]
|
|
46
46
|
/** 排除 glob(这些文件不参与搜索)。 */
|
|
47
47
|
exclude?: string[]
|
|
48
|
+
/** 强制绕过短期缓存:文件变更后的重搜必须看到最新磁盘内容,每次都跑真实 rg。 */
|
|
49
|
+
force?: boolean
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
type ContentSearchResponse = { matches: ContentMatch[]; truncated: boolean; warning?: string }
|
|
@@ -257,7 +259,8 @@ export class ContentSearcher {
|
|
|
257
259
|
const includeKey = (request.include ?? []).join(',')
|
|
258
260
|
const excludeKey = (request.exclude ?? []).join(',')
|
|
259
261
|
const key = [rootKey, query, request.matchCase ? 'mc' : '', request.wholeWord ? 'ww' : '', request.regex ? 'rx' : '', includeKey, excludeKey, PROVIDER_VERSION].join('|')
|
|
260
|
-
|
|
262
|
+
// force(文件变更后的重搜)必须看到最新磁盘内容:跳过缓存读,见下方跳过缓存写。
|
|
263
|
+
const cached = request.force ? undefined : this.cache.get(key)
|
|
261
264
|
if (cached) return { matches: cached.matches, truncated: cached.truncated, warning: cached.warning }
|
|
262
265
|
this.inflight.get(root)?.abort()
|
|
263
266
|
const controller = new AbortController()
|
|
@@ -278,7 +281,8 @@ export class ContentSearcher {
|
|
|
278
281
|
root,
|
|
279
282
|
})
|
|
280
283
|
if (controller.signal.aborted) return { matches: [], truncated: false }
|
|
281
|
-
|
|
284
|
+
// force 结果不回填缓存:本次是「读最新」,不代表该键的稳定内容。
|
|
285
|
+
if (!request.force) this.cache.set(key, result)
|
|
282
286
|
return { matches: result.matches, truncated: result.truncated, warning: result.warning }
|
|
283
287
|
} catch (error) {
|
|
284
288
|
if (controller.signal.aborted) return { matches: [], truncated: false }
|
package/src/shared/rpc.ts
CHANGED
|
@@ -377,7 +377,7 @@ export interface RpcRequestMap {
|
|
|
377
377
|
'edrv.dlog.clear': { sessionId?: string; file?: string }
|
|
378
378
|
'edrv.dlog.reveal': { sessionId?: string }
|
|
379
379
|
'edrv.searchFiles': { sessionId?: string; query: string }
|
|
380
|
-
'edrv.searchContent': { sessionId?: string; query: string; matchCase?: boolean; wholeWord?: boolean; regex?: boolean; maxResults?: number; include?: string[]; exclude?: string[] }
|
|
380
|
+
'edrv.searchContent': { sessionId?: string; query: string; matchCase?: boolean; wholeWord?: boolean; regex?: boolean; maxResults?: number; include?: string[]; exclude?: string[]; force?: boolean }
|
|
381
381
|
'edrv.listDir': { sessionId?: string; path: string; force?: boolean }
|
|
382
382
|
'edrv.revealInExplorer': { sessionId?: string; path: string }
|
|
383
383
|
'edrv.fsCreateFile': { sessionId?: string; path: string }
|
|
@@ -468,7 +468,9 @@ export interface RpcRequestMap {
|
|
|
468
468
|
'svn.explorerStatus': { sessionId?: string; force?: boolean }
|
|
469
469
|
'svn.explorerView': SvnViewRequest
|
|
470
470
|
'svn.diffBase': { sessionId?: string; path: string }
|
|
471
|
-
'svn.revert': { sessionId?: string; paths: string[] }
|
|
471
|
+
'svn.revert': { sessionId?: string; paths: string[]; depth?: 'empty' }
|
|
472
|
+
'svn.conflictStatus': { sessionId?: string; paths: string[] }
|
|
473
|
+
'svn.resolve': { sessionId?: string; path: string; accept: import('./svn.js').SvnResolveAccept }
|
|
472
474
|
'svn.add': { sessionId?: string; paths: string[] }
|
|
473
475
|
'svn.changelist': { sessionId?: string; paths: string[]; name?: string; remove?: boolean }
|
|
474
476
|
'svn.log': { sessionId?: string; path?: string; limit?: number; stopOnCopy?: boolean; startRev?: number; endRev?: number; showMerged?: boolean }
|
|
@@ -631,6 +633,8 @@ export interface RpcOkMap {
|
|
|
631
633
|
'svn.explorerView': SvnViewPayload
|
|
632
634
|
'svn.diffBase': { base: string | null; working: string; reason?: string; error?: string; binary?: boolean; encodingHint?: boolean }
|
|
633
635
|
'svn.revert': { count: number; summary: string; output: string }
|
|
636
|
+
'svn.conflictStatus': { states: import('./svn.js').SvnConflictState[] }
|
|
637
|
+
'svn.resolve': { summary: string; output: string }
|
|
634
638
|
'svn.add': { count: number; summary: string; output: string }
|
|
635
639
|
'svn.changelist': { count: number; summary: string; output: string }
|
|
636
640
|
'svn.log': { entries: SvnLogEntry[]; truncated: boolean; target: string; limit?: number }
|
package/src/shared/svn.ts
CHANGED
|
@@ -137,6 +137,16 @@ export interface SvnUpdatePoll extends SvnUpdateState {
|
|
|
137
137
|
totalSeq: number
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
/** Exact target conflict evidence; null/error is never a clean status. */
|
|
141
|
+
export interface SvnConflictState {
|
|
142
|
+
path: string
|
|
143
|
+
text: boolean
|
|
144
|
+
props: boolean
|
|
145
|
+
tree: boolean
|
|
146
|
+
versioned: boolean
|
|
147
|
+
}
|
|
148
|
+
export type SvnResolveAccept = 'mine-full' | 'theirs-full' | 'working'
|
|
149
|
+
|
|
140
150
|
/** revert/add 等批量动作结果(计数 + 摘要 + 原文)。 */
|
|
141
151
|
export interface SvnActionResult {
|
|
142
152
|
/** 处理条目数(宽松计数)。 */
|