dsh-vscode-mode 0.15.3 → 0.15.5
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 +8 -3
- package/lib/client.js +535 -388
- package/lib/client.js.map +1 -1
- package/lib/index.js +2 -1
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/compatSummary.ts +77 -0
- package/src/client/externalOpen.ts +21 -3
- package/src/client/index.ts +14 -10
- package/src/client/monaco/lsp/lspClient.ts +26 -5
- package/src/client/monaco/lsp/providers.ts +3 -1
- package/src/client/state/floatingWindow.ts +50 -0
- package/src/client/styles/editor.css +14 -1
- package/src/client/ui/EditorView.ts +26 -30
- package/src/client/ui/FloatingEditor.ts +16 -70
- package/src/client/ui/FloatingWindow.ts +168 -0
- package/src/client/ui/McpSettings.ts +1 -1
- package/src/client/ui/SvnAiPlanDialog.ts +4 -4
- package/src/client/ui/SvnLogDialog.ts +94 -125
- package/src/client/ui/SvnLogStats.ts +4 -5
- package/src/client/ui/SvnPatchDialog.ts +4 -3
- package/src/client/ui/SvnSumDialog.ts +4 -3
- package/src/client/ui/SvnUpdateDialog.ts +4 -5
- package/src/svn.ts +8 -1
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/** Shared in-page window chrome, without a mask or focus trap. @author ddj 2026年10月14号 */
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import { createPortal } from 'react-dom'
|
|
4
|
+
import { clampFloat } from '../state/editorLayout.js'
|
|
5
|
+
import type { FloatRect } from '../state/editorLayout.js'
|
|
6
|
+
import { dropFloat, floatLayer, floatVersion, initialFloat, moveFloat, raiseFloat, watchFloats } from '../state/floatingWindow.js'
|
|
7
|
+
|
|
8
|
+
export interface WindowProps {
|
|
9
|
+
title: string
|
|
10
|
+
dialogClass?: string
|
|
11
|
+
width?: number
|
|
12
|
+
height?: number
|
|
13
|
+
rect?: FloatRect
|
|
14
|
+
owner?: string
|
|
15
|
+
group?: string
|
|
16
|
+
closeOnEsc?: boolean
|
|
17
|
+
onClose?: () => void
|
|
18
|
+
onBounds?: (rect: FloatRect) => void
|
|
19
|
+
onFocus?: () => void
|
|
20
|
+
onKeyDown?: (event: React.KeyboardEvent) => void
|
|
21
|
+
actions?: React.ReactNode
|
|
22
|
+
children?: React.ReactNode
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** @private @author ddj 2026年10月14号 @param start Originating press. @param rect Initial bounds. @param sizing Resize mode. @param draw Paint live bounds without a render. @param apply Commit bounds once. @param done Gesture cleanup notification. @returns Gesture cancellation. */
|
|
26
|
+
function beginGesture(start: React.PointerEvent, rect: FloatRect, sizing: boolean, draw: (rect: FloatRect) => void, apply: (rect: FloatRect) => void, done: () => void): () => void {
|
|
27
|
+
let frame: number | null = null
|
|
28
|
+
let next = rect
|
|
29
|
+
let moved = false
|
|
30
|
+
/** @private @author ddj 2026年10月14号 Paint no more than once per frame. */
|
|
31
|
+
function paint(): void { frame = null; draw(next) }
|
|
32
|
+
/** @private @author ddj 2026年10月14号 @param event Pointer motion. Ignore unrelated pointers. */
|
|
33
|
+
function move(event: PointerEvent): void {
|
|
34
|
+
if (event.pointerId !== start.pointerId) return
|
|
35
|
+
event.preventDefault()
|
|
36
|
+
moved = true
|
|
37
|
+
next = moveFloat(rect, event.clientX - start.clientX, event.clientY - start.clientY, sizing, window.innerWidth, window.innerHeight)
|
|
38
|
+
if (frame === null) frame = requestAnimationFrame(paint)
|
|
39
|
+
}
|
|
40
|
+
/** @private @author ddj 2026年10月14号 Remove listeners, then commit the settled geometry exactly once. */
|
|
41
|
+
function stop(): void {
|
|
42
|
+
if (frame !== null) { cancelAnimationFrame(frame); frame = null }
|
|
43
|
+
window.removeEventListener('pointermove', move, true)
|
|
44
|
+
window.removeEventListener('pointerup', end, true)
|
|
45
|
+
window.removeEventListener('pointercancel', end, true)
|
|
46
|
+
window.removeEventListener('blur', stop)
|
|
47
|
+
if (moved) { draw(next); apply(next) }
|
|
48
|
+
done()
|
|
49
|
+
}
|
|
50
|
+
/** @private @author ddj 2026年10月14号 @param event Pointer release. Only end originating gesture. */
|
|
51
|
+
function end(event: PointerEvent): void { if (event.pointerId === start.pointerId) stop() }
|
|
52
|
+
window.addEventListener('pointermove', move, true)
|
|
53
|
+
window.addEventListener('pointerup', end, true)
|
|
54
|
+
window.addEventListener('pointercancel', end, true)
|
|
55
|
+
window.addEventListener('blur', stop)
|
|
56
|
+
return stop
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const resizeSinks = new Set<() => void>()
|
|
60
|
+
let resizeFrame: number | null = null
|
|
61
|
+
|
|
62
|
+
/** @private @author ddj 2026年10月14号 Re-clamp every window at most once per frame. */
|
|
63
|
+
function onViewportResize(): void {
|
|
64
|
+
if (resizeFrame !== null) return
|
|
65
|
+
resizeFrame = requestAnimationFrame(() => {
|
|
66
|
+
resizeFrame = null
|
|
67
|
+
for (const sink of [...resizeSinks]) sink()
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** @private @author ddj 2026年10月14号 @param sink Per-window re-clamp callback. @returns Unsubscribe. */
|
|
72
|
+
function subscribeResize(sink: () => void): () => void {
|
|
73
|
+
if (resizeSinks.size === 0) window.addEventListener('resize', onViewportResize)
|
|
74
|
+
resizeSinks.add(sink)
|
|
75
|
+
return () => {
|
|
76
|
+
resizeSinks.delete(sink)
|
|
77
|
+
if (resizeSinks.size === 0) {
|
|
78
|
+
window.removeEventListener('resize', onViewportResize)
|
|
79
|
+
if (resizeFrame !== null) { cancelAnimationFrame(resizeFrame); resizeFrame = null }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** @public @author ddj 2026年10月14号 @param props Window contents and lifecycle callbacks. @returns Movable, resizable nonmodal portal. */
|
|
85
|
+
export function FloatingWindow(props: WindowProps): React.ReactPortal {
|
|
86
|
+
const id = React.useId()
|
|
87
|
+
const root = React.useRef<HTMLDivElement>(null)
|
|
88
|
+
const latest = React.useRef(props)
|
|
89
|
+
latest.current = props
|
|
90
|
+
const [bounds, setBounds] = React.useState(() => initialFloat(props.width ?? 760, props.height ?? 600, window.innerWidth, window.innerHeight))
|
|
91
|
+
const current = React.useRef(props.rect ?? bounds)
|
|
92
|
+
current.current = props.rect ?? bounds
|
|
93
|
+
const cleanup = React.useRef<(() => void) | null>(null)
|
|
94
|
+
const gesture = React.useRef<FloatRect | null>(null)
|
|
95
|
+
React.useSyncExternalStore(watchFloats, floatVersion, floatVersion)
|
|
96
|
+
/** @private @author ddj 2026年10月14号 @param rect Next geometry. Notify controlled owner or retain local geometry. */
|
|
97
|
+
function apply(rect: FloatRect): void {
|
|
98
|
+
if (latest.current.rect) latest.current.onBounds?.(rect)
|
|
99
|
+
else setBounds(rect)
|
|
100
|
+
}
|
|
101
|
+
/** @private @author ddj 2026年10月14号 @param rect Live gesture geometry. Write the DOM and hold it until the commit render lands. */
|
|
102
|
+
function draw(rect: FloatRect): void {
|
|
103
|
+
gesture.current = rect
|
|
104
|
+
const node = root.current
|
|
105
|
+
if (!node) return
|
|
106
|
+
node.style.left = rect.left + 'px'
|
|
107
|
+
node.style.top = rect.top + 'px'
|
|
108
|
+
node.style.width = rect.width + 'px'
|
|
109
|
+
node.style.height = rect.height + 'px'
|
|
110
|
+
}
|
|
111
|
+
/** @private @author ddj 2026年10月14号 End any active gesture. */
|
|
112
|
+
function cancel(): void { cleanup.current?.(); cleanup.current = null }
|
|
113
|
+
/** @private @author ddj 2026年10月14号 Re-clamp after viewport resize, preferring the gesture that just settled. */
|
|
114
|
+
function resize(): void { cancel(); apply(clampFloat(gesture.current ?? current.current, window.innerWidth, window.innerHeight)) }
|
|
115
|
+
React.useEffect(() => {
|
|
116
|
+
raiseFloat(id)
|
|
117
|
+
const off = subscribeResize(resize)
|
|
118
|
+
resize()
|
|
119
|
+
return () => { off(); cancel(); dropFloat(id) }
|
|
120
|
+
}, [id])
|
|
121
|
+
/** @private @author ddj 2026年10月14号 Activate without stealing text input focus. */
|
|
122
|
+
function focus(): void { raiseFloat(id); latest.current.onFocus?.() }
|
|
123
|
+
/** @private @author ddj 2026年10月14号 @param event Window press. Give noninteractive content a keyboard owner. */
|
|
124
|
+
function activate(event: React.PointerEvent): void {
|
|
125
|
+
focus()
|
|
126
|
+
if (!(event.target as Element).closest('button, input, select, textarea, a, [contenteditable], .monaco-editor')) root.current?.focus({ preventScroll: true })
|
|
127
|
+
}
|
|
128
|
+
/** @private @author ddj 2026年10月14号 @param event Handle press. @param sizing Resize instead of move. */
|
|
129
|
+
function start(event: React.PointerEvent, sizing: boolean): void {
|
|
130
|
+
if (event.button !== 0 || !event.isPrimary) return
|
|
131
|
+
cancel()
|
|
132
|
+
event.preventDefault()
|
|
133
|
+
cleanup.current = beginGesture(event, gesture.current ?? current.current, sizing, draw, apply, clear)
|
|
134
|
+
}
|
|
135
|
+
// 手势几何的持有范围只到「提交渲染落地」为止:apply() 走的是 pointerup 等离散事件,
|
|
136
|
+
// React 18 会在事件末尾同步 flush,下一次渲染 base 已是新几何,故此处可同步释放。
|
|
137
|
+
// 勘误 2026-10-14(测试约定):原先额外挂一帧再释放,会在手势结束后残留一个待执行帧
|
|
138
|
+
// (tests/floatingWindow.test.ts 断言 frames.size === 0),且无必要。
|
|
139
|
+
/** @private @author ddj 2026年10月14号 Release the settled geometry once React has committed it. */
|
|
140
|
+
function clear(): void {
|
|
141
|
+
cleanup.current = null
|
|
142
|
+
gesture.current = null
|
|
143
|
+
}
|
|
144
|
+
/** @private @author ddj 2026年10月14号 @param event Title press. Preserve title action buttons. */
|
|
145
|
+
function moveStart(event: React.PointerEvent): void { if (!(event.target as Element).closest('button')) start(event, false) }
|
|
146
|
+
/** @private @author ddj 2026年10月14号 @param event Resize press. */
|
|
147
|
+
function sizeStart(event: React.PointerEvent): void { start(event, true) }
|
|
148
|
+
/** @private @author ddj 2026年10月14号 @param event Local key. Exclude React-bubbling child portals. */
|
|
149
|
+
function keyDown(event: React.KeyboardEvent): void {
|
|
150
|
+
if ((event.target as Element).closest('[data-edrv-window]') !== root.current || event.defaultPrevented) return
|
|
151
|
+
if (event.key === 'Escape' && props.closeOnEsc !== false && props.onClose) {
|
|
152
|
+
event.preventDefault(); event.stopPropagation(); props.onClose(); return
|
|
153
|
+
}
|
|
154
|
+
props.onKeyDown?.(event)
|
|
155
|
+
}
|
|
156
|
+
const base = clampFloat(props.rect ?? bounds, window.innerWidth, window.innerHeight)
|
|
157
|
+
const rect = gesture.current ?? base
|
|
158
|
+
return createPortal(React.createElement('div', {
|
|
159
|
+
ref: root, tabIndex: -1, role: 'dialog', 'aria-label': props.title, 'aria-modal': false,
|
|
160
|
+
className: 'edrv-floating', 'data-edrv-view': '1', 'data-edrv-window': id,
|
|
161
|
+
'data-edrv-float': props.group, 'data-edrv-owner': props.owner,
|
|
162
|
+
style: { ...rect, zIndex: floatLayer(id) }, onPointerDownCapture: activate, onFocusCapture: focus, onKeyDown: keyDown,
|
|
163
|
+
},
|
|
164
|
+
React.createElement('div', { className: 'edrv-floating-title', onPointerDown: moveStart },
|
|
165
|
+
React.createElement('span', null, props.title), props.actions),
|
|
166
|
+
React.createElement('div', { className: 'edrv-floating-content' + (props.dialogClass ? ' ' + props.dialogClass : '') }, props.children),
|
|
167
|
+
React.createElement('div', { className: 'edrv-floating-grip', title: '拖动调整窗口大小', onPointerDown: sizeStart })), document.body)
|
|
168
|
+
}
|
|
@@ -414,7 +414,7 @@ function CompatSection({ getSummary }) {
|
|
|
414
414
|
devForm?.enabled && group('开发形态', React.createElement('div', { className: 'vsm-compat-item ok' }, React.createElement('span', { className: 'vsm-compat-name' }, '开发形态开启(工作区链接安装)'), React.createElement('span', { className: 'vsm-compat-note' }, devForm.path))),
|
|
415
415
|
group('版本适配', renderItems(report?.adapters)),
|
|
416
416
|
group('外部插件与服务', renderItems(report?.external)),
|
|
417
|
-
group('客户端适配', renderItems(getSummary?.() ?? [])),
|
|
417
|
+
group('客户端适配', renderItems(getSummary?.(report?.dshVersion ?? '') ?? [])),
|
|
418
418
|
group('护栏', renderItems(report?.guards)),
|
|
419
419
|
group('警告', warnings.length ? warnings.map((w, index) => React.createElement('div', { key: index, className: 'vsm-mcp-error vsm-mcp-banner' }, w)) : React.createElement('div', { className: 'vsm-compat-item ok' }, React.createElement('span', { className: 'vsm-compat-name' }, '未检测到兼容性问题'), React.createElement('span', { className: 'vsm-compat-note' }, ''))),
|
|
420
420
|
)
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* 作者 ddj 2026年09月23号
|
|
15
15
|
*/
|
|
16
16
|
import React from 'react'
|
|
17
|
-
import {
|
|
17
|
+
import { FloatingWindow } from './FloatingWindow.js'
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* 预览勾选初值:分组默认全勾(changelist 可逆),还原/忽略默认不勾(有副作用)。
|
|
@@ -282,11 +282,11 @@ export function SvnAiPlanDialog(props) {
|
|
|
282
282
|
}, '取消剩余'))))
|
|
283
283
|
}
|
|
284
284
|
|
|
285
|
-
return React.createElement(
|
|
285
|
+
return React.createElement(FloatingWindow, {
|
|
286
|
+
title: 'SVN 智能整理', height: 600,
|
|
286
287
|
key: 'edrv-svn-plan',
|
|
287
288
|
dialogClass: 'edrv-svnplan-dialog',
|
|
288
|
-
width:
|
|
289
|
-
closeOnMask: !running,
|
|
289
|
+
width: 680,
|
|
290
290
|
closeOnEsc: !running,
|
|
291
291
|
onClose,
|
|
292
292
|
}, header, metaEl, body, footer)
|
|
@@ -26,6 +26,8 @@ import { SVN_LOG_ACTION_LABEL } from '../../shared/svn.js'
|
|
|
26
26
|
import { orderLogEntries } from '../../svnLog.js'
|
|
27
27
|
import { SVN_LOG_SHOW_ALL_LIMIT } from '../svnLog.js'
|
|
28
28
|
import { ModalShell } from './ModalShell.js'
|
|
29
|
+
import { FloatingWindow } from './FloatingWindow.js'
|
|
30
|
+
import { inputOwnsKey } from '../state/floatingWindow.js'
|
|
29
31
|
import { ContextMenu } from './ContextMenu.js'
|
|
30
32
|
import type { ContextMenuEntry } from './ContextMenu.js'
|
|
31
33
|
import { SvnLogStats } from './SvnLogStats.js'
|
|
@@ -569,11 +571,42 @@ function PathRow(props) {
|
|
|
569
571
|
buttons)
|
|
570
572
|
}
|
|
571
573
|
|
|
574
|
+
/**
|
|
575
|
+
* 单个数据行:浅比较包装(回调身份由父级 ref 转发保持稳定,故 memo 才可能命中)。
|
|
576
|
+
* @author ddj 2026年10月14号
|
|
577
|
+
*/
|
|
578
|
+
const LogItemRow = React.memo(LogItem)
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* 数据行容器(P0-14 上段)。独立 memo:只在条目列表/选中集/工作副本版号变化时重建行元素;
|
|
582
|
+
* 上段高度拖拽与弹窗缩放只改 LogList 的 height,不再逐帧重建整表(原先每帧 logRowsOf + N 个元素)。
|
|
583
|
+
* @author ddj 2026年10月14号
|
|
584
|
+
* @param props.list 过滤排序后的条目(非空)
|
|
585
|
+
* @param props.selectedRevs 可见选中的 revision 集合
|
|
586
|
+
* @param props.wcRev 工作副本版号(null = 不加粗)
|
|
587
|
+
* @param props.onPick 选中回调(event, item)
|
|
588
|
+
* @param props.onMenu 右键回调(event, item)
|
|
589
|
+
* @returns 行元素
|
|
590
|
+
*/
|
|
591
|
+
const LogRows = React.memo(function LogRows(props) {
|
|
592
|
+
const { list, selectedRevs, wcRev, onPick, onMenu } = props
|
|
593
|
+
return React.createElement(React.Fragment, null,
|
|
594
|
+
logRowsOf(list).map((row) => React.createElement(LogItemRow, {
|
|
595
|
+
key: row.item.revision + ':' + row.level,
|
|
596
|
+
item: row.item,
|
|
597
|
+
on: selectedRevs.has(row.item.revision),
|
|
598
|
+
wc: wcRev != null && row.item.revision === wcRev,
|
|
599
|
+
level: row.level,
|
|
600
|
+
onPick,
|
|
601
|
+
onMenu,
|
|
602
|
+
})))
|
|
603
|
+
})
|
|
604
|
+
|
|
572
605
|
/**
|
|
573
606
|
* 上窗格列表(列头行 + 数据行 + 「加载更多」;P0-13 宽度受拖拽控制)。
|
|
574
607
|
* @author ddj 2026年09月16号 / 2026年09月17号
|
|
575
608
|
* @param props.list 过滤排序后的条目(null = 未加载)
|
|
576
|
-
* @param props.
|
|
609
|
+
* @param props.selectedRevs 可见选中的 revision 集合(P1-3 多选;由父级按 revision 维护)
|
|
577
610
|
* @param props.keyword 过滤词(决定空态文案)
|
|
578
611
|
* @param props.busy 加载中
|
|
579
612
|
* @param props.truncated 是否可能还有更多
|
|
@@ -586,22 +619,14 @@ function PathRow(props) {
|
|
|
586
619
|
* @returns 列表元素
|
|
587
620
|
*/
|
|
588
621
|
function LogList(props) {
|
|
589
|
-
const { list,
|
|
622
|
+
const { list, selectedRevs, keyword, busy, truncated, sortKey, sortDir, onSort, onSortReset, onPick, onLoadMore, onItemMenu, height, wcRev } = props
|
|
590
623
|
let inner
|
|
591
624
|
if (list === null) {
|
|
592
625
|
inner = React.createElement('div', { className: 'edrv-tree-loading' }, busy ? '读取日志中…' : '尚未加载')
|
|
593
626
|
} else if (!list.length) {
|
|
594
627
|
inner = React.createElement('div', { className: 'edrv-tree-loading' }, keyword ? '无匹配条目' : '无提交历史')
|
|
595
628
|
} else {
|
|
596
|
-
inner =
|
|
597
|
-
key: row.item.revision + ':' + row.level,
|
|
598
|
-
item: row.item,
|
|
599
|
-
on: selected.some((s) => s.revision === row.item.revision),
|
|
600
|
-
wc: wcRev != null && row.item.revision === wcRev,
|
|
601
|
-
level: row.level,
|
|
602
|
-
onPick,
|
|
603
|
-
onMenu: onItemMenu,
|
|
604
|
-
}))
|
|
629
|
+
inner = React.createElement(LogRows, { list, selectedRevs, wcRev, onPick, onMenu: onItemMenu })
|
|
605
630
|
}
|
|
606
631
|
const more = truncated
|
|
607
632
|
? React.createElement('div', { className: 'edrv-svnlog-more' },
|
|
@@ -886,9 +911,6 @@ export function SvnLogDialog(props) {
|
|
|
886
911
|
const [sortKey, setSortKey] = React.useState(null)
|
|
887
912
|
const [sortDir, setSortDir] = React.useState('desc')
|
|
888
913
|
const [menu, setMenu] = React.useState(null)
|
|
889
|
-
const [dlgW, setDlgW] = React.useState(() => Math.min(DIALOG_W_DEFAULT, (typeof window !== 'undefined' ? window.innerWidth : 1280) - 24)) // P0-14:外框宽(拖拽受控)
|
|
890
|
-
const [dlgH, setDlgH] = React.useState(() => Math.min(DIALOG_H_DEFAULT, (typeof window !== 'undefined' ? window.innerHeight : 800) - 24)) // P0-14:外框高(拖拽受控)
|
|
891
|
-
const [anchor, setAnchor] = React.useState(null) // 八向拖拽锚点(null=flex 居中;非 null=absolute 定位于该视口坐标) @author ddj 2026年09月24号
|
|
892
914
|
const [listH, setListH] = React.useState(LIST_H_DEFAULT) // P0-14:上段修订列表高(拖拽受控)
|
|
893
915
|
const [msgH, setMsgH] = React.useState(MSG_H_DEFAULT) // P0-14:中段提交信息高(拖拽受控)
|
|
894
916
|
const [statsOpen, setStatsOpen] = React.useState(false) // P1-10:统计窗开关
|
|
@@ -922,10 +944,14 @@ export function SvnLogDialog(props) {
|
|
|
922
944
|
() => (filtered === null ? null : orderLogEntries(filtered, sortKey ?? 'revision', sortKey ? sortDir : 'desc')),
|
|
923
945
|
[filtered, sortKey, sortDir],
|
|
924
946
|
)
|
|
925
|
-
// P1-3:可见选中 = 选中数组 ∩ 当前显示列表;current =
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
947
|
+
// P1-3:可见选中 = 选中数组 ∩ 当前显示列表;current = 可见选中首条(详情/菜单主语),空则回落首条。
|
|
948
|
+
// 交集改用 revision 集合:原实现对每条选中再扫一遍显示列表(O(n×m))且每次渲染重算(缩放弹窗时每帧都会走到)。
|
|
949
|
+
const sortedRevs = React.useMemo(() => (sorted ? new Set(sorted.map((x) => x.revision)) : null), [sorted])
|
|
950
|
+
const visibleSelected = React.useMemo(
|
|
951
|
+
() => (sortedRevs ? selected.filter((item) => sortedRevs.has(item.revision)) : selected),
|
|
952
|
+
[sortedRevs, selected],
|
|
953
|
+
)
|
|
954
|
+
const selectedRevs = React.useMemo(() => new Set(visibleSelected.map((item) => item.revision)), [visibleSelected])
|
|
929
955
|
const current = visibleSelected.length
|
|
930
956
|
? visibleSelected[0]
|
|
931
957
|
: (sorted && sorted.length ? sorted[0] : null)
|
|
@@ -998,65 +1024,6 @@ export function SvnLogDialog(props) {
|
|
|
998
1024
|
window.addEventListener('pointerup', up)
|
|
999
1025
|
}
|
|
1000
1026
|
|
|
1001
|
-
// 八向(13-log-ui-polish 增量3):pointerdown 取卡片视口 rect 起点并立即 absolute 锚定,
|
|
1002
|
-
// 实现「拖哪边哪边动、对边固定」(flex 居中模型无法表达该语义);clamp = 尺寸下限 680/480
|
|
1003
|
-
// 与视口 12px 边距;每把手柄都以实际 rect 重锚 → CSS 截断/取整漂移自校正。
|
|
1004
|
-
const startResize = (event, mode) => {
|
|
1005
|
-
event.preventDefault()
|
|
1006
|
-
event.stopPropagation()
|
|
1007
|
-
const card = event.currentTarget.parentElement
|
|
1008
|
-
const rect = card ? card.getBoundingClientRect() : null
|
|
1009
|
-
if (!rect) return
|
|
1010
|
-
const r0 = { left: rect.left, top: rect.top, width: rect.width, height: rect.height }
|
|
1011
|
-
const startX = event.clientX
|
|
1012
|
-
const startY = event.clientY
|
|
1013
|
-
const moveL = mode === 'w' || mode === 'nw' || mode === 'sw'
|
|
1014
|
-
const moveR = mode === 'e' || mode === 'ne' || mode === 'se'
|
|
1015
|
-
const moveT = mode === 'n' || mode === 'nw' || mode === 'ne'
|
|
1016
|
-
const moveB = mode === 's' || mode === 'sw' || mode === 'se'
|
|
1017
|
-
const EDGE = 12 // 视口边距
|
|
1018
|
-
const right0 = r0.left + r0.width
|
|
1019
|
-
const bottom0 = r0.top + r0.height
|
|
1020
|
-
// 起手锚定:切 absolute 到当前 rect(视觉零跳变)并同步 state=实际渲染尺寸
|
|
1021
|
-
setAnchor({ left: r0.left, top: r0.top })
|
|
1022
|
-
setDlgW(Math.round(r0.width))
|
|
1023
|
-
setDlgH(Math.round(r0.height))
|
|
1024
|
-
const move = (e) => {
|
|
1025
|
-
const dx = e.clientX - startX
|
|
1026
|
-
const dy = e.clientY - startY
|
|
1027
|
-
let left = r0.left
|
|
1028
|
-
let top = r0.top
|
|
1029
|
-
let width = r0.width
|
|
1030
|
-
let height = r0.height
|
|
1031
|
-
if (moveR) width = Math.min(Math.max(DIALOG_W_MIN, r0.width + dx), window.innerWidth - EDGE - r0.left)
|
|
1032
|
-
if (moveL) { left = Math.min(Math.max(EDGE, r0.left + dx), right0 - DIALOG_W_MIN); width = right0 - left }
|
|
1033
|
-
if (moveB) height = Math.min(Math.max(DIALOG_H_MIN, r0.height + dy), window.innerHeight - EDGE - r0.top)
|
|
1034
|
-
if (moveT) { top = Math.min(Math.max(EDGE, r0.top + dy), bottom0 - DIALOG_H_MIN); height = bottom0 - top }
|
|
1035
|
-
const nextLeft = left
|
|
1036
|
-
const nextTop = top
|
|
1037
|
-
setAnchor((prev) => (prev && prev.left === nextLeft && prev.top === nextTop) ? prev : { left: nextLeft, top: nextTop })
|
|
1038
|
-
setDlgW(Math.round(width))
|
|
1039
|
-
setDlgH(Math.round(height))
|
|
1040
|
-
}
|
|
1041
|
-
const up = () => {
|
|
1042
|
-
window.removeEventListener('pointermove', move)
|
|
1043
|
-
window.removeEventListener('pointerup', up)
|
|
1044
|
-
swallowNextClick()
|
|
1045
|
-
}
|
|
1046
|
-
window.addEventListener('pointermove', move)
|
|
1047
|
-
window.addEventListener('pointerup', up)
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
/**
|
|
1051
|
-
* 双击复位:清锚回 flex 居中 + 默认尺寸(视口 clamp 同初始,防小屏溢出)。
|
|
1052
|
-
* @author ddj 2026年09月24号
|
|
1053
|
-
*/
|
|
1054
|
-
const resetDlgSize = () => {
|
|
1055
|
-
setAnchor(null)
|
|
1056
|
-
setDlgW(Math.min(DIALOG_W_DEFAULT, window.innerWidth - 24))
|
|
1057
|
-
setDlgH(Math.min(DIALOG_H_DEFAULT, window.innerHeight - 24))
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
1027
|
/** 上段修订列表高度拖拽(clamp:下限 200,上限 = body 高 - 信息区/路径区/分隔条保留空间)。 */
|
|
1061
1028
|
const onListHDrag = (event) => startDrag(event, 'y', listH, (next) => {
|
|
1062
1029
|
const total = bodyRef.current?.clientHeight ?? 0
|
|
@@ -1082,33 +1049,45 @@ export function SvnLogDialog(props) {
|
|
|
1082
1049
|
* @param wantList 目标上段(修订列表)高度
|
|
1083
1050
|
* @param wantMsg 目标中段(提交信息)高度
|
|
1084
1051
|
*/
|
|
1052
|
+
/** @private @author ddj 2026年10月14号 @param nextList 目标上段高度 @param nextMsg 目标中段高度 只在值真正变化时提交。 */
|
|
1053
|
+
const applyFit = (nextList, nextMsg) => {
|
|
1054
|
+
if (nextList !== listH) setListH(nextList)
|
|
1055
|
+
if (nextMsg !== msgH) setMsgH(nextMsg)
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1085
1058
|
const fitHeights = (wantList, wantMsg) => {
|
|
1086
1059
|
const total = bodyRef.current?.clientHeight ?? 0
|
|
1087
|
-
if (!total) {
|
|
1060
|
+
if (!total) { applyFit(wantList, wantMsg); return }
|
|
1088
1061
|
const paneOcc = paneRef.current?.offsetHeight ?? -1
|
|
1089
1062
|
// 实测导出非本段占用(勘误 2026-09-24:手算公式漏 gap/margin/padding 曾致保底仅114);测不到回落分隔条
|
|
1090
1063
|
const chrome = paneOcc >= 0 ? Math.max(0, total - listH - msgH - paneOcc) : HSPLIT_TOTAL_PX
|
|
1091
1064
|
const budget = total - PANE_KEEP_MIN - chrome
|
|
1092
1065
|
if (budget < LIST_H_MIN + MSG_H_MIN) {
|
|
1093
|
-
|
|
1094
|
-
setMsgH(Math.min(wantMsg, MSG_H_MIN))
|
|
1066
|
+
applyFit(Math.min(wantList, LIST_H_MIN), Math.min(wantMsg, MSG_H_MIN))
|
|
1095
1067
|
return
|
|
1096
1068
|
}
|
|
1097
|
-
if (wantList + wantMsg <= budget) {
|
|
1069
|
+
if (wantList + wantMsg <= budget) { applyFit(wantList, wantMsg); return }
|
|
1098
1070
|
const scale = budget / (wantList + wantMsg)
|
|
1099
1071
|
let nextList = Math.floor(wantList * scale)
|
|
1100
1072
|
let nextMsg = Math.floor(wantMsg * scale)
|
|
1101
1073
|
if (nextList < LIST_H_MIN) { nextList = LIST_H_MIN; nextMsg = Math.min(nextMsg, budget - nextList) }
|
|
1102
1074
|
else if (nextMsg < MSG_H_MIN) { nextMsg = MSG_H_MIN; nextList = Math.min(nextList, budget - nextMsg) }
|
|
1103
|
-
|
|
1104
|
-
setMsgH(nextMsg)
|
|
1075
|
+
applyFit(nextList, nextMsg)
|
|
1105
1076
|
}
|
|
1106
1077
|
|
|
1107
1078
|
// 初始/弹窗尺寸变化 fit(2026-09-24 用户反馈:固定高中段把下段变更文件区挤到 0):
|
|
1108
|
-
//
|
|
1079
|
+
// 观察的是 body(其高度由弹窗尺寸决定、不含 listH/msgH),故收敛是单步不动点、不会与自身互相激发;
|
|
1080
|
+
// 拖拽期间不介入(拖拽自带 clamp),双击复位经 fitHeights 显式收敛。
|
|
1081
|
+
// 勘误 2026-10-14:ResizeObserver 回调本身已按帧派发,无需再包 rAF —— 推迟到下一帧反而会多出
|
|
1082
|
+
// 一帧「尺寸已变小、height 还是旧值」的中间态,正是要避免的下段被挤扁。改由 applyFit 抑制同值提交。
|
|
1083
|
+
const fitRef = React.useRef(null)
|
|
1084
|
+
fitRef.current = () => fitHeights(listH, msgH)
|
|
1109
1085
|
React.useEffect(() => {
|
|
1110
|
-
|
|
1111
|
-
|
|
1086
|
+
const observer = new ResizeObserver(() => fitRef.current?.())
|
|
1087
|
+
if (bodyRef.current) observer.observe(bodyRef.current)
|
|
1088
|
+
fitRef.current?.()
|
|
1089
|
+
return () => observer.disconnect()
|
|
1090
|
+
}, [])
|
|
1112
1091
|
|
|
1113
1092
|
// 勘误2(2026-09-24):挂载 fit 量到的是数据到达前布局(终态差 ~4px → 保底 166)。
|
|
1114
1093
|
// 初次数据到达以终态布局补 fit 一次收敛至 ≥170;单次门闩,之后刷新/加载更多/拖拽不介入。
|
|
@@ -1118,24 +1097,20 @@ export function SvnLogDialog(props) {
|
|
|
1118
1097
|
fitHeights(listH, msgH)
|
|
1119
1098
|
}, [list])
|
|
1120
1099
|
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
}
|
|
1136
|
-
window.addEventListener('keydown', onKey, true)
|
|
1137
|
-
return () => window.removeEventListener('keydown', onKey, true)
|
|
1138
|
-
}, [sorted, onRefresh, selected])
|
|
1100
|
+
/** @private @author ddj 2026年10月14号 @param event 日志浮窗内按键。其他窗口及输入控件保留自己的键盘行为。 */
|
|
1101
|
+
function onLogKey(event) {
|
|
1102
|
+
if (rangeOpen || menu || inputOwnsKey(event.target)) return
|
|
1103
|
+
if (event.key === 'F5') { event.preventDefault(); event.stopPropagation(); onRefresh(); return }
|
|
1104
|
+
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return
|
|
1105
|
+
if (!sorted || !sorted.length) return
|
|
1106
|
+
event.preventDefault()
|
|
1107
|
+
event.stopPropagation()
|
|
1108
|
+
const at = sorted.findIndex((item) => item.revision === selected[0]?.revision)
|
|
1109
|
+
const next = at < 0 ? 0 : Math.min(sorted.length - 1, Math.max(0, at + (event.key === 'ArrowDown' ? 1 : -1)))
|
|
1110
|
+
const hit = sorted[next]
|
|
1111
|
+
setSelected([hit])
|
|
1112
|
+
setAnchorRev(hit.revision)
|
|
1113
|
+
}
|
|
1139
1114
|
|
|
1140
1115
|
/** 复制当前选中修订(P0-9;剪贴板不可用静默忽略)。 */
|
|
1141
1116
|
const copyRevision = (entry) => {
|
|
@@ -1186,6 +1161,15 @@ export function SvnLogDialog(props) {
|
|
|
1186
1161
|
})
|
|
1187
1162
|
}
|
|
1188
1163
|
|
|
1164
|
+
// React.memo 生效的前提是行回调身份稳定:pickWithMods/onItemMenu 的闭包依赖 anchorRev/sorted/visibleSelected,
|
|
1165
|
+
// 每次渲染都是新函数,会把 LogRows/LogItemRow 的 memo 全部打穿。用 ref 存最新闭包、useCallback 空依赖转发。
|
|
1166
|
+
/** @author ddj 2026年10月14号 */
|
|
1167
|
+
const rowCbs = React.useRef<{ pick: ((event: unknown, item: unknown) => void) | null; menu: ((event: unknown, item: unknown) => void) | null }>({ pick: null, menu: null })
|
|
1168
|
+
rowCbs.current.pick = pickWithMods
|
|
1169
|
+
rowCbs.current.menu = onItemMenu
|
|
1170
|
+
const pickStable = React.useCallback((event: unknown, item: unknown) => rowCbs.current.pick?.(event, item), [])
|
|
1171
|
+
const menuStable = React.useCallback((event: unknown, item: unknown) => rowCbs.current.menu?.(event, item), [])
|
|
1172
|
+
|
|
1189
1173
|
/** 下窗格路径行右键菜单(P0-8;G8 目录不可开差异,G9 仓库外不可打开)。 */
|
|
1190
1174
|
const onPathMenu = (event, item, revision) => {
|
|
1191
1175
|
event.preventDefault()
|
|
@@ -1208,9 +1192,9 @@ export function SvnLogDialog(props) {
|
|
|
1208
1192
|
// P0-14 上中下三段全宽:上=修订列表(高可拖)/ 中=提交信息(高可拖)/ 下=变更路径(占余)
|
|
1209
1193
|
const body = React.createElement('div', { className: 'edrv-svnlog-body', ref: bodyRef },
|
|
1210
1194
|
React.createElement(LogList, {
|
|
1211
|
-
list: sorted,
|
|
1195
|
+
list: sorted, selectedRevs, keyword: query.trim(), busy, truncated,
|
|
1212
1196
|
sortKey, sortDir, onSort, onSortReset,
|
|
1213
|
-
onPick:
|
|
1197
|
+
onPick: pickStable, onLoadMore, onItemMenu: menuStable,
|
|
1214
1198
|
height: listH, wcRev,
|
|
1215
1199
|
}),
|
|
1216
1200
|
React.createElement(Splitter, {
|
|
@@ -1224,16 +1208,10 @@ export function SvnLogDialog(props) {
|
|
|
1224
1208
|
React.createElement('div', { className: 'edrv-svnlog-pane edrv-svnlog-pathspane', ref: paneRef },
|
|
1225
1209
|
React.createElement(LogPathsInner, { selected: visibleSelected, target, onlyAffected, onOpenFile, onOpenDiff, onPathMenu })))
|
|
1226
1210
|
|
|
1227
|
-
return React.createElement(
|
|
1228
|
-
dialogClass: 'edrv-svnlog-dialog',
|
|
1229
|
-
width:
|
|
1230
|
-
|
|
1231
|
-
// border-box 令 rect==state(clamp/拖拽零换算);maxHeight:none 消除 88vh 造成 state≠视觉的漂移。
|
|
1232
|
-
cardStyle: Object.assign(
|
|
1233
|
-
{ height: Math.round(dlgH) + 'px', boxSizing: 'border-box', maxHeight: 'none' },
|
|
1234
|
-
anchor ? { position: 'absolute', left: Math.round(anchor.left) + 'px', top: Math.round(anchor.top) + 'px' } : null,
|
|
1235
|
-
),
|
|
1236
|
-
onClose,
|
|
1211
|
+
return React.createElement(FloatingWindow, {
|
|
1212
|
+
title: 'SVN 日志', dialogClass: 'edrv-svnlog-dialog',
|
|
1213
|
+
width: DIALOG_W_DEFAULT, height: DIALOG_H_DEFAULT,
|
|
1214
|
+
onKeyDown: onLogKey, onClose,
|
|
1237
1215
|
},
|
|
1238
1216
|
React.createElement(LogHead, { target, onClose }),
|
|
1239
1217
|
React.createElement(LogFilterBar, {
|
|
@@ -1249,15 +1227,6 @@ export function SvnLogDialog(props) {
|
|
|
1249
1227
|
onExportCsv: () => onExport('csv'), onExportHtml: () => onExport('html'),
|
|
1250
1228
|
rangeActive: rangeActive === true, onOpenRange: () => setRangeOpen(true), onRangeReset: () => onRangeReset?.(),
|
|
1251
1229
|
}),
|
|
1252
|
-
// 八向手柄(增量3):四边先渲染、四角后渲染(角盖过相邻边 2px overlap);双击复位统一走 resetDlgSize
|
|
1253
|
-
React.createElement(Grip, { mode: 'n', onDragStart: startResize, onReset: resetDlgSize }),
|
|
1254
|
-
React.createElement(Grip, { mode: 's', onDragStart: startResize, onReset: resetDlgSize }),
|
|
1255
|
-
React.createElement(Grip, { mode: 'w', onDragStart: startResize, onReset: resetDlgSize }),
|
|
1256
|
-
React.createElement(Grip, { mode: 'e', onDragStart: startResize, onReset: resetDlgSize }),
|
|
1257
|
-
React.createElement(Grip, { mode: 'nw', onDragStart: startResize, onReset: resetDlgSize }),
|
|
1258
|
-
React.createElement(Grip, { mode: 'ne', onDragStart: startResize, onReset: resetDlgSize }),
|
|
1259
|
-
React.createElement(Grip, { mode: 'sw', onDragStart: startResize, onReset: resetDlgSize }),
|
|
1260
|
-
React.createElement(Grip, { mode: 'se', onDragStart: startResize, onReset: resetDlgSize }),
|
|
1261
1230
|
(menu ? React.createElement(ContextMenu, { x: menu.x, y: menu.y, entries: menu.entries, onClose: () => setMenu(null) }) : null),
|
|
1262
1231
|
(statsOpen ? React.createElement(SvnLogStats, { key: 'edrv-svnlog-stats', entries: sorted, onClose: () => setStatsOpen(false) }) : null),
|
|
1263
1232
|
(rangeOpen ? React.createElement(ModalShell, {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* 作者 ddj 2026年09月17号
|
|
11
11
|
*/
|
|
12
12
|
import React from 'react'
|
|
13
|
-
import {
|
|
13
|
+
import { FloatingWindow } from './FloatingWindow.js'
|
|
14
14
|
|
|
15
15
|
/** stacked 时间线展开的作者上限(其余并入「其他」)。 */
|
|
16
16
|
const STACK_TOP = 5
|
|
@@ -205,11 +205,10 @@ export function SvnLogStats(props) {
|
|
|
205
205
|
const [othersPct, setOthersPct] = React.useState(0)
|
|
206
206
|
const [stacked, setStacked] = React.useState(false)
|
|
207
207
|
const stats = React.useMemo(() => svnLogStatsOf(entries, { caseInsensitive: ci }), [entries, ci])
|
|
208
|
-
return React.createElement(
|
|
208
|
+
return React.createElement(FloatingWindow, {
|
|
209
|
+
title: 'SVN 日志统计', height: 600,
|
|
209
210
|
dialogClass: 'edrv-svnlog-stats',
|
|
210
|
-
width:
|
|
211
|
-
zIndex: 192,
|
|
212
|
-
closeOnEsc: false,
|
|
211
|
+
width: 680,
|
|
213
212
|
onClose,
|
|
214
213
|
},
|
|
215
214
|
React.createElement('div', { className: 'edrv-svnlog-stats-head' },
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* 作者 ddj 2026年09月20号
|
|
10
10
|
*/
|
|
11
11
|
import React from 'react'
|
|
12
|
-
import {
|
|
12
|
+
import { FloatingWindow } from './FloatingWindow.js'
|
|
13
13
|
import { downloadText } from './svnExport.js'
|
|
14
14
|
|
|
15
15
|
/**
|
|
@@ -40,10 +40,11 @@ export function SvnPatchDialog(props) {
|
|
|
40
40
|
onNote?.(done ? '已保存 ' + fileName : '保存失败:浏览器下载能力不可用')
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
return React.createElement(
|
|
43
|
+
return React.createElement(FloatingWindow, {
|
|
44
|
+
title: 'SVN 补丁预览', height: 560,
|
|
44
45
|
key: 'edrv-svn-patch',
|
|
45
46
|
dialogClass: 'edrv-svn-patch-dialog',
|
|
46
|
-
width:
|
|
47
|
+
width: 760,
|
|
47
48
|
onClose,
|
|
48
49
|
},
|
|
49
50
|
React.createElement('div', { className: 'edrv-svn-patch-head' },
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import React from 'react'
|
|
11
11
|
import { SVN_SUM_LETTER } from '../../shared/svn.js'
|
|
12
|
-
import {
|
|
12
|
+
import { FloatingWindow } from './FloatingWindow.js'
|
|
13
13
|
import { svnDiffSum } from '../svnStatus.js'
|
|
14
14
|
|
|
15
15
|
/**
|
|
@@ -53,10 +53,11 @@ export function SvnSumDialog(props) {
|
|
|
53
53
|
(entry.kind === 'dir' ? React.createElement('span', { className: 'edrv-svnlog-path-kind' }, '目录') : null)))
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
return React.createElement(
|
|
56
|
+
return React.createElement(FloatingWindow, {
|
|
57
|
+
title: 'SVN 目录对比', height: 560,
|
|
57
58
|
key: 'edrv-svn-sum',
|
|
58
59
|
dialogClass: 'edrv-svn-sum-dialog',
|
|
59
|
-
width:
|
|
60
|
+
width: 680,
|
|
60
61
|
onClose,
|
|
61
62
|
},
|
|
62
63
|
React.createElement('div', { className: 'edrv-svn-patch-head' },
|
|
@@ -9,7 +9,7 @@ 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
|
-
import {
|
|
12
|
+
import { FloatingWindow } from './FloatingWindow.js'
|
|
13
13
|
import { nextConflict, nextUpdateSort, rowConflict, sortUpdateRows, updateActs, updateCloseMode, updateDone, updateLocked, updateParent, updateRange, updateReady, updateType } from './svnUpdateModel.js'
|
|
14
14
|
import type { UpdateColumn, UpdateSort } from './svnUpdateModel.js'
|
|
15
15
|
|
|
@@ -564,10 +564,9 @@ export function SvnUpdateDialog(props: UpdateProps): React.ReactElement {
|
|
|
564
564
|
const phase = pendingRows && processFinished ? '正在加载更新明细…'
|
|
565
565
|
: state?.phase === 'completed' ? '更新完成' : state?.phase === 'failed' ? '更新失败'
|
|
566
566
|
: state?.phase === 'cancelled' ? '更新已停止' : state?.phase === 'cancelling' ? '正在停止…' : '正在更新…'
|
|
567
|
-
return React.createElement(
|
|
568
|
-
dialogClass: 'edrv-update-dialog', width:
|
|
569
|
-
|
|
570
|
-
closeOnEsc: closeMode.passive, closeOnMask: closeMode.passive, onClose: dismiss,
|
|
567
|
+
return React.createElement(FloatingWindow, {
|
|
568
|
+
title: 'SVN 更新', dialogClass: 'edrv-update-dialog', width: 1320, height: 680,
|
|
569
|
+
closeOnEsc: closeMode.passive, onClose: dismiss,
|
|
571
570
|
},
|
|
572
571
|
updateHead(props, state, phase, closable, dismiss),
|
|
573
572
|
updateTable(props, displayRows, view, selection, finished, sort, onSort, evidence.states),
|
package/src/svn.ts
CHANGED
|
@@ -1119,8 +1119,15 @@ export function createSvnRpc(deps: SvnRpcDeps): { handlers: Record<string, unkno
|
|
|
1119
1119
|
const paths = [...new Set(args.paths.map(safeRel))]
|
|
1120
1120
|
if (paths.some((path) => !path)) return { ok: false as const, error: '路径不合法' }
|
|
1121
1121
|
try {
|
|
1122
|
+
// 分批并发(批 4,计划批准值):冲突面板一次请求上限 64 条,旧串行版在 64 冲突时要排队 64 次 svn 进程
|
|
1123
|
+
// (每次冷启动约 10~40ms,最坏累计 1~2s 阻塞面板)。readConflict 只跑 `svn status --xml`(只读,无写入竞争)。
|
|
1124
|
+
// 与 svn.fileSizes 的分批并发同型(见下方 BATCH = 16);分批 push 保持结果顺序不变。
|
|
1125
|
+
// @author ddj 2026年10月14号
|
|
1122
1126
|
const states = []
|
|
1123
|
-
|
|
1127
|
+
const BATCH = 4
|
|
1128
|
+
for (let start = 0; start < paths.length; start += BATCH) {
|
|
1129
|
+
states.push(...await Promise.all(paths.slice(start, start + BATCH).map((path) => readConflict(scope.cwd, scope.root, path!))))
|
|
1130
|
+
}
|
|
1124
1131
|
return { ok: true as const, states }
|
|
1125
1132
|
} catch (error) { return { ok: false as const, error: String(error) } }
|
|
1126
1133
|
},
|