dsh-vscode-mode 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +62 -11
  2. package/lib/client.js +2879 -1030
  3. package/lib/client.js.map +1 -1
  4. package/lib/index.js +2227 -1056
  5. package/lib/index.js.map +1 -1
  6. package/package.json +1 -1
  7. package/src/client/activePathStore.ts +28 -0
  8. package/src/client/commandBridge.ts +9 -41
  9. package/src/client/dap/hover.ts +2 -7
  10. package/src/client/index.ts +116 -14
  11. package/src/client/keybindings.ts +19 -73
  12. package/src/client/luaReservedWords.ts +10 -0
  13. package/src/client/monaco/lsp/keywords.ts +121 -0
  14. package/src/client/monaco/lsp/lspClient.ts +68 -6
  15. package/src/client/monaco/lsp/memberScope.ts +90 -0
  16. package/src/client/monaco/lsp/providers.ts +14 -4
  17. package/src/client/settingsDocRouter.ts +52 -0
  18. package/src/client/shortcutsOfficial.ts +451 -0
  19. package/src/client/sidebar/menuItems.ts +1 -0
  20. package/src/client/sidebar/panels/RulesPanel.ts +15 -11
  21. package/src/client/sidebar/types.ts +2 -0
  22. package/src/client/styles/editor.css +143 -37
  23. package/src/client/svnActions.ts +4 -4
  24. package/src/client/ui/ContextMenu.ts +4 -2
  25. package/src/client/ui/EditorView.ts +142 -135
  26. package/src/client/ui/McpSettings.ts +4 -5
  27. package/src/client/ui/QuickOpen.ts +10 -11
  28. package/src/client/ui/ShortcutSettings.ts +294 -0
  29. package/src/client/ui/SvnLogDialog.ts +200 -69
  30. package/src/client/ui/SvnUpdateDialog.ts +472 -0
  31. package/src/client/ui/commandCatalog.ts +36 -41
  32. package/src/client/ui/svnUpdateModel.ts +135 -0
  33. package/src/fileOpenSettings.ts +3 -0
  34. package/src/index.ts +2 -0
  35. package/src/lsp/completionCap.ts +80 -0
  36. package/src/lsp/manager.ts +42 -10
  37. package/src/lsp/rpc.ts +8 -5
  38. package/src/lsp/server.ts +18 -4
  39. package/src/rpc.ts +74 -2
  40. package/src/rules.ts +100 -44
  41. package/src/settingsDocPath.ts +30 -0
  42. package/src/shared/keybindings.ts +253 -0
  43. package/src/shared/rpc.ts +12 -2
  44. package/src/shared/rules.ts +9 -1
  45. package/src/shared/svn.ts +38 -0
  46. package/src/svn.ts +102 -7
  47. package/src/svnText.ts +22 -13
  48. package/src/svnUpdateJob.ts +588 -0
  49. package/src/client/ui/KeybindingsPanel.ts +0 -190
@@ -48,8 +48,10 @@ const MSG_H_DEFAULT = 140
48
48
  const MSG_H_MIN = 60
49
49
  /** 分段拖拽时下段保留的最小空间。 */
50
50
  const PANE_KEEP_MIN = 170
51
- /** 日志动作字母的配色分级(新增=绿、删除=红、替换=紫、修改=蓝)。 */
52
- const ACTION_TONE = { A: 'added', D: 'deleted', R: 'added', M: 'modified' }
51
+ /** 保底计算需额外扣除的两根横向分隔条合计高度(2 × 6px;拖拽 clamp 与初始 fit 共用)。 @author ddj 2026年09月24号 */
52
+ const HSPLIT_TOTAL_PX = 12
53
+ /** 日志动作字母的配色分级(新增=绿、删除=红、替换=琥珀、修改=蓝;13-log-ui-polish 语义归位)。 */
54
+ const ACTION_TONE = { A: 'added', D: 'deleted', R: 'replaced', M: 'modified' }
53
55
  /** 排序键 → 列头名(P0-12;顺序 = 列头展示顺序)。 */
54
56
  const SORT_COLUMNS = [
55
57
  { key: 'revision', label: '版本' },
@@ -427,7 +429,7 @@ function LogItem(props) {
427
429
  + (merged ? ' edrv-svnlog-item-merged' : '')
428
430
  + (wc ? ' edrv-svnlog-item-wc' : ''),
429
431
  title,
430
- style: merged ? { paddingLeft: 6 + level * 18 } : undefined,
432
+ style: merged ? { paddingLeft: 16 + level * 18 } : undefined,
431
433
  onClick: (event) => onPick(event, item),
432
434
  onContextMenu: (event) => onMenu?.(event, item),
433
435
  },
@@ -460,24 +462,33 @@ function Splitter(props) {
460
462
  }
461
463
 
462
464
  /**
463
- * 吞掉拖拽结束后浏览器派发的下一次合成 click(capture 一次性)。
464
- * 拖拽常把指针移出卡片,松手后 click 落在遮罩上会误触「点遮罩关闭」。
465
- * @author ddj 2026年09月17号
465
+ * 吞掉拖拽松手后的合成 click(capture;仅遮罩目标 + 300ms 窗口)。
466
+ * 拖拽常把指针移出卡片,松手后 click 落在遮罩上会误触「点遮罩关闭」——原实现 setTimeout(0)
467
+ * 太短,CDP/合成输入的 click 跨任务到达时窗口已释放(13-log-ui-polish 勘误3 实测复现)。
468
+ * 现改为:只拦 target=遮罩 的 click(卡片内点击放行,不误吞行选中),300ms 后正常。
469
+ * @author ddj 2026年09月17号 / 2026年09月24号
466
470
  */
467
471
  function swallowNextClick() {
468
472
  const swallow = (event) => {
469
- event.stopPropagation()
470
- event.preventDefault()
473
+ const onMask = event.target && event.target.classList && event.target.classList.contains('edrv-modal-mask')
474
+ if (onMask) {
475
+ event.stopPropagation()
476
+ event.preventDefault()
477
+ }
471
478
  window.removeEventListener('click', swallow, true)
472
479
  }
473
480
  window.addEventListener('click', swallow, true)
474
- window.setTimeout(() => window.removeEventListener('click', swallow, true), 0)
481
+ window.setTimeout(() => window.removeEventListener('click', swallow, true), 300)
475
482
  }
476
483
 
484
+ /** 八向手柄 → 提示文案(title 供无障碍与实机寻址;13-log-ui-polish 增量3)。 */
485
+ const GRIP_LABEL = { n: '上缘', s: '下缘', w: '左缘', e: '右缘', nw: '左上角', ne: '右上角', sw: '左下角', se: '右下角' }
486
+
477
487
  /**
478
- * 外框尺寸手柄(P0-14):mode 'r'=右缘调宽 / 'b'=底缘调高 / 'c'=右下角双向;双击复位。
479
- * @author ddj 2026年09月17号
480
- * @param props.mode 手柄形态
488
+ * 外框尺寸手柄(13-log-ui-polish 增量3 八向):mode ∈ n/s/w/e/nw/ne/sw/se(四边+四角);双击复位。
489
+ * 拖拽语义:拖哪边哪边动、对边固定(由弹窗侧 startResize 以 rect 起点 + absolute 锚定实现)。
490
+ * @author ddj 2026年09月17号 / 2026年09月24号
491
+ * @param props.mode 手柄形态(八向标准向名)
481
492
  * @param props.onDragStart pointerdown 回调(弹窗侧启动 window 级跟踪)
482
493
  * @param props.onReset 双击复位
483
494
  * @returns 手柄元素
@@ -486,11 +497,25 @@ function Grip(props) {
486
497
  const { mode, onDragStart, onReset } = props
487
498
  return React.createElement('div', {
488
499
  className: 'edrv-svnlog-grip edrv-svnlog-grip-' + mode,
500
+ title: '调整弹窗尺寸(' + (GRIP_LABEL[mode] ?? mode) + ')· 双击复位',
489
501
  onPointerDown: (event) => onDragStart(event, mode),
490
502
  onDoubleClick: (event) => { event.stopPropagation(); onReset() },
491
503
  })
492
504
  }
493
505
 
506
+ /**
507
+ * 路径两段拆分(目录段弱化 + 文件名加重;无斜杠时整段为文件名)。
508
+ * @author ddj 2026年09月24号
509
+ * @param text 显示用路径文本
510
+ * @returns { dir, base } 目录前缀(含尾斜杠,可为空串)与文件名
511
+ */
512
+ function pathSegsOf(text) {
513
+ const raw = String(text ?? '')
514
+ const at = raw.lastIndexOf('/')
515
+ if (at < 0) return { dir: '', base: raw }
516
+ return { dir: raw.slice(0, at + 1), base: raw.slice(at + 1) }
517
+ }
518
+
494
519
  /**
495
520
  * 下窗格变更路径单行(双击看差异;目录打开;Copy from 成列 P0-7;非受影响灰显 P0-5)。
496
521
  * relPath 三态:null = 仓库外(显示仓库原路径);'' = 工作副本根自身变更(如合并属性,
@@ -512,6 +537,7 @@ function PathRow(props) {
512
537
  const open = () => { if (actionable) onOpenFile?.(item.relPath) }
513
538
  const diff = () => { if (actionable && item.kind !== 'dir') onOpenDiff?.(item.relPath, revision) }
514
539
  const relText = outside ? item.path + '(仓库外)' : (isRoot ? '(工作副本根)' : item.relPath)
540
+ const segs = pathSegsOf(relText)
515
541
  const buttons = actionable ? React.createElement('span', { className: 'edrv-svnlog-acts' },
516
542
  React.createElement('button', {
517
543
  className: 'edrv-svn-act', title: '在工作区打开该文件',
@@ -533,7 +559,9 @@ function PathRow(props) {
533
559
  onContextMenu: (event) => onMenu?.(event, item, revision),
534
560
  },
535
561
  actionLetterEl(item.action),
536
- React.createElement('span', { className: 'edrv-svnlog-pathtext' }, relText),
562
+ React.createElement('span', { className: 'edrv-svnlog-pathtext' },
563
+ segs.dir ? React.createElement('span', { className: 'edrv-svnlog-pdir' }, segs.dir) : null,
564
+ React.createElement('span', { className: 'edrv-svnlog-pbase' }, segs.base)),
537
565
  (item.kind === 'dir' ? React.createElement('span', { className: 'edrv-svnlog-kind' }, '目录') : null),
538
566
  (item.copyFrom ? React.createElement('span', { className: 'edrv-svnlog-kind' }, '复制') : null),
539
567
  React.createElement('span', { className: 'edrv-svnlog-copyfrom', title: item.copyFrom ? item.copyFrom + '@' + (item.copyFromRev ?? '?') : '' },
@@ -708,12 +736,13 @@ function LogFilterBar(props) {
708
736
  React.createElement('label', { className: 'edrv-svnlog-check', title: '把每个词元当正则表达式(语法错误回落子串匹配)' },
709
737
  React.createElement('input', { type: 'checkbox', checked: regexOn, onChange: (event) => onRegex(event.target.checked) }),
710
738
  '正则'),
711
- React.createElement('label', { className: 'edrv-svnlog-check' },
712
- '从',
713
- React.createElement('input', { type: 'date', className: 'edrv-svnlog-date', value: from, onChange: (event) => onFrom(event.target.value) })),
714
- React.createElement('label', { className: 'edrv-svnlog-check' },
715
- '到',
716
- React.createElement('input', { type: 'date', className: 'edrv-svnlog-date', value: to, onChange: (event) => onTo(event.target.value) })))
739
+ React.createElement('span', { className: 'edrv-svnlog-dates' },
740
+ React.createElement('label', { className: 'edrv-svnlog-check' },
741
+ '从',
742
+ React.createElement('input', { type: 'date', className: 'edrv-svnlog-date', value: from, onChange: (event) => onFrom(event.target.value) })),
743
+ React.createElement('label', { className: 'edrv-svnlog-check' },
744
+ '到',
745
+ React.createElement('input', { type: 'date', className: 'edrv-svnlog-date', value: to, onChange: (event) => onTo(event.target.value) }))))
717
746
  }
718
747
 
719
748
  /**
@@ -776,41 +805,44 @@ function LogFoot(props) {
776
805
  React.createElement('span', { style: { flex: 1 } }),
777
806
  status),
778
807
  React.createElement('div', { className: 'edrv-svnlog-btns' },
779
- React.createElement('button', {
780
- className: 'edrv-svn-act', disabled: busy || !truncated || rangeActive,
781
- title: rangeActive ? '区间模式下不可加载更多(回到最新后可用)' : (truncated ? '再取 100 条' : '已到最早提交:该目标的全部历史已显示(svn 日志只含该路径发生过变更的版本)'),
782
- onClick: onLoadMore,
783
- }, 'Next 100'),
784
- React.createElement('button', {
785
- className: 'edrv-svn-act', disabled: busy || rangeActive,
786
- title: rangeActive
787
- ? '区间模式下不可用(回到最新后可用)'
788
- : '取到本插件上限 ' + SVN_LOG_SHOW_ALL_LIMIT + ' 条(超大仓库可能较慢;再往前的历史可用 Show Range… 指定区间)',
789
- onClick: onShowAll,
790
- }, 'Show All'),
791
- React.createElement('button', {
792
- className: 'edrv-svn-act', disabled: busy, title: '按版本区间拉取(P1-6;起始=较早、结束=较晚)',
793
- onClick: onOpenRange,
794
- }, 'Show Range…'),
795
- (rangeActive
796
- ? React.createElement('button', { className: 'edrv-svn-act', disabled: busy, title: '清除区间,回到默认 HEAD 往前的窗口', onClick: onRangeReset }, '回到最新')
797
- : null),
798
- React.createElement('button', {
799
- className: 'edrv-svn-act', disabled: busy || !filtered || !filtered.length,
800
- title: '导出当前过滤后的修订列表为 CSV(带 BOM,Excel 直开;修订/作者/日期/信息/路径)',
801
- onClick: onExportCsv,
802
- }, '导出CSV'),
803
- React.createElement('button', {
804
- className: 'edrv-svn-act', disabled: busy || !filtered || !filtered.length,
805
- title: '导出当前过滤后的修订列表为 HTML 报表(仅本地查看,不外发)',
806
- onClick: onExportHtml,
807
- }, '导出HTML'),
808
+ React.createElement('span', { className: 'edrv-svnlog-btngroup' },
809
+ React.createElement('button', {
810
+ className: 'edrv-svn-act', disabled: busy || !truncated || rangeActive,
811
+ title: rangeActive ? '区间模式下不可加载更多(回到最新后可用)' : (truncated ? '再取 100 条' : '已到最早提交:该目标的全部历史已显示(svn 日志只含该路径发生过变更的版本)'),
812
+ onClick: onLoadMore,
813
+ }, 'Next 100'),
814
+ React.createElement('button', {
815
+ className: 'edrv-svn-act', disabled: busy || rangeActive,
816
+ title: rangeActive
817
+ ? '区间模式下不可用(回到最新后可用)'
818
+ : '取到本插件上限 ' + SVN_LOG_SHOW_ALL_LIMIT + ' 条(超大仓库可能较慢;再往前的历史可用 Show Range… 指定区间)',
819
+ onClick: onShowAll,
820
+ }, 'Show All'),
821
+ React.createElement('button', {
822
+ className: 'edrv-svn-act', disabled: busy, title: '按版本区间拉取(P1-6;起始=较早、结束=较晚)',
823
+ onClick: onOpenRange,
824
+ }, 'Show Range…'),
825
+ (rangeActive
826
+ ? React.createElement('button', { className: 'edrv-svn-act', disabled: busy, title: '清除区间,回到默认 HEAD 往前的窗口', onClick: onRangeReset }, '回到最新')
827
+ : null)),
828
+ React.createElement('span', { className: 'edrv-svnlog-btngroup' },
829
+ React.createElement('button', {
830
+ className: 'edrv-svn-act', disabled: busy || !filtered || !filtered.length,
831
+ title: '导出当前过滤后的修订列表为 CSV(带 BOM,Excel 直开;修订/作者/日期/信息/路径)',
832
+ onClick: onExportCsv,
833
+ }, '导出CSV'),
834
+ React.createElement('button', {
835
+ className: 'edrv-svn-act', disabled: busy || !filtered || !filtered.length,
836
+ title: '导出当前过滤后的修订列表为 HTML 报表(仅本地查看,不外发)',
837
+ onClick: onExportHtml,
838
+ }, '导出HTML')),
808
839
  React.createElement('span', { style: { flex: 1 } }),
809
- React.createElement('button', { className: 'edrv-svn-act', disabled: busy, title: '重新读取日志(F5)', onClick: onRefresh }, 'Refresh'),
810
- React.createElement('button', {
811
- className: 'edrv-svn-act', disabled: !list || !list.length,
812
- title: '统计当前显示的修订(期间 / 按作者 / 按日期,P1-10)', onClick: onStats,
813
- }, 'Statistics')))
840
+ React.createElement('span', { className: 'edrv-svnlog-btngroup' },
841
+ React.createElement('button', { className: 'edrv-svn-act', disabled: busy, title: '重新读取日志(F5)', onClick: onRefresh }, 'Refresh'),
842
+ React.createElement('button', {
843
+ className: 'edrv-svn-act', disabled: !list || !list.length,
844
+ title: '统计当前显示的修订(期间 / 按作者 / 按日期,P1-10)', onClick: onStats,
845
+ }, 'Statistics'))))
814
846
  }
815
847
 
816
848
  /**
@@ -856,6 +888,7 @@ export function SvnLogDialog(props) {
856
888
  const [menu, setMenu] = React.useState(null)
857
889
  const [dlgW, setDlgW] = React.useState(() => Math.min(DIALOG_W_DEFAULT, (typeof window !== 'undefined' ? window.innerWidth : 1280) - 24)) // P0-14:外框宽(拖拽受控)
858
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号
859
892
  const [listH, setListH] = React.useState(LIST_H_DEFAULT) // P0-14:上段修订列表高(拖拽受控)
860
893
  const [msgH, setMsgH] = React.useState(MSG_H_DEFAULT) // P0-14:中段提交信息高(拖拽受控)
861
894
  const [statsOpen, setStatsOpen] = React.useState(false) // P1-10:统计窗开关
@@ -863,6 +896,8 @@ export function SvnLogDialog(props) {
863
896
  const [rangeFrom, setRangeFrom] = React.useState('') // P1-6:起始修订(较早)
864
897
  const [rangeTo, setRangeTo] = React.useState('') // P1-6:结束修订(较晚)
865
898
  const bodyRef = React.useRef(null)
899
+ const paneRef = React.useRef(null) // 下段 pathspane 实高测量点(fitHeights 导出非本段占用;13-log-ui-polish 勘误) @author ddj 2026年09月24号
900
+ const fitOnDataRef = React.useRef(false) // 初次数据到达只补 fit 一次(挂载时 chrome 量到的是数据前布局,终态差 ~4px) @author ddj 2026年09月24号
866
901
 
867
902
  const list = Array.isArray(entries) ? entries : null
868
903
  // 选中项:默认首条;数据刷新/过滤后仅保留仍存在的选中(G10:按 revision 维护),全部消失回落首条;
@@ -963,17 +998,45 @@ export function SvnLogDialog(props) {
963
998
  window.addEventListener('pointerup', up)
964
999
  }
965
1000
 
966
- // P0-14 外框调尺寸:mode 'r'=右缘调宽 / 'b'=底缘调高 / 'c'=角手柄双向;clamp 视口内
1001
+ // 八向(13-log-ui-polish 增量3):pointerdown 取卡片视口 rect 起点并立即 absolute 锚定,
1002
+ // 实现「拖哪边哪边动、对边固定」(flex 居中模型无法表达该语义);clamp = 尺寸下限 680/480
1003
+ // 与视口 12px 边距;每把手柄都以实际 rect 重锚 → CSS 截断/取整漂移自校正。
967
1004
  const startResize = (event, mode) => {
968
1005
  event.preventDefault()
969
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 }
970
1011
  const startX = event.clientX
971
1012
  const startY = event.clientY
972
- const startW = dlgW
973
- const startH = dlgH
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))
974
1024
  const move = (e) => {
975
- if (mode !== 'b') setDlgW(Math.min(window.innerWidth - 24, Math.max(DIALOG_W_MIN, startW + (e.clientX - startX))))
976
- if (mode !== 'r') setDlgH(Math.min(window.innerHeight - 24, Math.max(DIALOG_H_MIN, startH + (e.clientY - startY))))
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))
977
1040
  }
978
1041
  const up = () => {
979
1042
  window.removeEventListener('pointermove', move)
@@ -984,20 +1047,77 @@ export function SvnLogDialog(props) {
984
1047
  window.addEventListener('pointerup', up)
985
1048
  }
986
1049
 
987
- /** 上段修订列表高度拖拽(clamp:下限 200,上限 = body 高 - 信息区/路径区保留空间)。 */
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
+ /** 上段修订列表高度拖拽(clamp:下限 200,上限 = body 高 - 信息区/路径区/分隔条保留空间)。 */
988
1061
  const onListHDrag = (event) => startDrag(event, 'y', listH, (next) => {
989
1062
  const total = bodyRef.current?.clientHeight ?? 0
990
- const max = Math.max(LIST_H_MIN + 80, total - msgH - PANE_KEEP_MIN)
1063
+ const max = Math.max(LIST_H_MIN + 80, total - msgH - PANE_KEEP_MIN - HSPLIT_TOTAL_PX)
991
1064
  setListH(Math.min(max, Math.max(LIST_H_MIN, next)))
992
1065
  })
993
1066
 
994
- /** 中段提交信息高度拖拽(clamp:下限 60,上限 = body 高 - 列表/路径区保留空间)。 */
1067
+ /** 中段提交信息高度拖拽(clamp:下限 60,上限 = body 高 - 列表区/路径区/分隔条保留空间)。 */
995
1068
  const onMsgHDrag = (event) => startDrag(event, 'y', msgH, (next) => {
996
1069
  const total = bodyRef.current?.clientHeight ?? 0
997
- const max = Math.max(MSG_H_MIN + 80, total - listH - PANE_KEEP_MIN)
1070
+ const max = Math.max(MSG_H_MIN + 80, total - listH - PANE_KEEP_MIN - HSPLIT_TOTAL_PX)
998
1071
  setMsgH(Math.min(max, Math.max(MSG_H_MIN, next)))
999
1072
  })
1000
1073
 
1074
+ /**
1075
+ * 上/中段高度收敛进 body(初始挂载、dlgH 变化、双击复位共用;shrink-only 只缩不涨)。
1076
+ * 非本段占用 chrome 由 DOM 实测导出(= bodyH - 上中段 state - 下段实占;含 gap4×6 + 分隔条
1077
+ * 2×10 + 上段 padding/border6 + 中段 padding/border18 = 68px),免手算且 CSS 微调自动跟随;
1078
+ * 保下段 ≥ PANE_KEEP_MIN。极端小视口(预算 < 两段地板和)给各自下限不倒挂;
1079
+ * 地板救济:抬 list 至地板后超出从 msg 扣回(预算 ≥ 地板和保证不破对方地板)。
1080
+ * 只缩不涨 + 地板兜底,与拖拽 clamp 同向,天然不会与拖拽互相拉扯成循环。
1081
+ * @author ddj 2026年09月24号
1082
+ * @param wantList 目标上段(修订列表)高度
1083
+ * @param wantMsg 目标中段(提交信息)高度
1084
+ */
1085
+ const fitHeights = (wantList, wantMsg) => {
1086
+ const total = bodyRef.current?.clientHeight ?? 0
1087
+ if (!total) { setListH(wantList); setMsgH(wantMsg); return }
1088
+ const paneOcc = paneRef.current?.offsetHeight ?? -1
1089
+ // 实测导出非本段占用(勘误 2026-09-24:手算公式漏 gap/margin/padding 曾致保底仅114);测不到回落分隔条
1090
+ const chrome = paneOcc >= 0 ? Math.max(0, total - listH - msgH - paneOcc) : HSPLIT_TOTAL_PX
1091
+ const budget = total - PANE_KEEP_MIN - chrome
1092
+ if (budget < LIST_H_MIN + MSG_H_MIN) {
1093
+ setListH(Math.min(wantList, LIST_H_MIN))
1094
+ setMsgH(Math.min(wantMsg, MSG_H_MIN))
1095
+ return
1096
+ }
1097
+ if (wantList + wantMsg <= budget) { setListH(wantList); setMsgH(wantMsg); return }
1098
+ const scale = budget / (wantList + wantMsg)
1099
+ let nextList = Math.floor(wantList * scale)
1100
+ let nextMsg = Math.floor(wantMsg * scale)
1101
+ if (nextList < LIST_H_MIN) { nextList = LIST_H_MIN; nextMsg = Math.min(nextMsg, budget - nextList) }
1102
+ else if (nextMsg < MSG_H_MIN) { nextMsg = MSG_H_MIN; nextList = Math.min(nextList, budget - nextMsg) }
1103
+ setListH(nextList)
1104
+ setMsgH(nextMsg)
1105
+ }
1106
+
1107
+ // 初始/弹窗尺寸变化 fit(2026-09-24 用户反馈:固定高中段把下段变更文件区挤到 0):
1108
+ // 挂载与 dlgH 变化时收敛一次;拖拽期间不介入(拖拽自带 clamp),双击复位经 fitHeights 显式收敛。
1109
+ React.useEffect(() => {
1110
+ fitHeights(listH, msgH)
1111
+ }, [dlgH])
1112
+
1113
+ // 勘误2(2026-09-24):挂载 fit 量到的是数据到达前布局(终态差 ~4px → 保底 166)。
1114
+ // 初次数据到达以终态布局补 fit 一次收敛至 ≥170;单次门闩,之后刷新/加载更多/拖拽不介入。
1115
+ React.useEffect(() => {
1116
+ if (!list || fitOnDataRef.current) return
1117
+ fitOnDataRef.current = true
1118
+ fitHeights(listH, msgH)
1119
+ }, [list])
1120
+
1001
1121
  // 键盘:F5 刷新(防浏览器整页刷新);↑↓ 在显示列表内移动选中(输入框内不劫持)
1002
1122
  React.useEffect(() => {
1003
1123
  const onKey = (event) => {
@@ -1094,20 +1214,25 @@ export function SvnLogDialog(props) {
1094
1214
  height: listH, wcRev,
1095
1215
  }),
1096
1216
  React.createElement(Splitter, {
1097
- title: '调整修订列表高度', onDragStart: onListHDrag, onReset: () => setListH(LIST_H_DEFAULT),
1217
+ title: '调整修订列表高度', onDragStart: onListHDrag, onReset: () => fitHeights(LIST_H_DEFAULT, msgH),
1098
1218
  }),
1099
1219
  React.createElement('div', { className: 'edrv-svnlog-pane edrv-svnlog-msgpane', style: { height: msgH } },
1100
1220
  React.createElement(LogMsgInner, { selected: visibleSelected })),
1101
1221
  React.createElement(Splitter, {
1102
- title: '调整提交信息区高度', onDragStart: onMsgHDrag, onReset: () => setMsgH(MSG_H_DEFAULT),
1222
+ title: '调整提交信息区高度', onDragStart: onMsgHDrag, onReset: () => fitHeights(listH, MSG_H_DEFAULT),
1103
1223
  }),
1104
- React.createElement('div', { className: 'edrv-svnlog-pane edrv-svnlog-pathspane' },
1224
+ React.createElement('div', { className: 'edrv-svnlog-pane edrv-svnlog-pathspane', ref: paneRef },
1105
1225
  React.createElement(LogPathsInner, { selected: visibleSelected, target, onlyAffected, onOpenFile, onOpenDiff, onPathMenu })))
1106
1226
 
1107
1227
  return React.createElement(ModalShell, {
1108
1228
  dialogClass: 'edrv-svnlog-dialog',
1109
1229
  width: Math.round(dlgW) + 'px',
1110
- cardStyle: { height: Math.round(dlgH) + 'px' },
1230
+ // 八向锚定(增量3):anchor 非空时 absolute 定位于视口坐标(脱离 flex 居中,视觉零跳变);
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
+ ),
1111
1236
  onClose,
1112
1237
  },
1113
1238
  React.createElement(LogHead, { target, onClose }),
@@ -1124,9 +1249,15 @@ export function SvnLogDialog(props) {
1124
1249
  onExportCsv: () => onExport('csv'), onExportHtml: () => onExport('html'),
1125
1250
  rangeActive: rangeActive === true, onOpenRange: () => setRangeOpen(true), onRangeReset: () => onRangeReset?.(),
1126
1251
  }),
1127
- React.createElement(Grip, { mode: 'r', onDragStart: startResize, onReset: () => { setDlgW(DIALOG_W_DEFAULT); setDlgH(DIALOG_H_DEFAULT) } }),
1128
- React.createElement(Grip, { mode: 'b', onDragStart: startResize, onReset: () => { setDlgW(DIALOG_W_DEFAULT); setDlgH(DIALOG_H_DEFAULT) } }),
1129
- React.createElement(Grip, { mode: 'c', onDragStart: startResize, onReset: () => { setDlgW(DIALOG_W_DEFAULT); setDlgH(DIALOG_H_DEFAULT) } }),
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 }),
1130
1261
  (menu ? React.createElement(ContextMenu, { x: menu.x, y: menu.y, entries: menu.entries, onClose: () => setMenu(null) }) : null),
1131
1262
  (statsOpen ? React.createElement(SvnLogStats, { key: 'edrv-svnlog-stats', entries: sorted, onClose: () => setStatsOpen(false) }) : null),
1132
1263
  (rangeOpen ? React.createElement(ModalShell, {