dsh-vscode-mode 0.4.6 → 0.5.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 (44) hide show
  1. package/README.md +32 -2
  2. package/lib/client.js +8854 -4023
  3. package/lib/client.js.map +1 -1
  4. package/lib/index.js +1842 -65
  5. package/lib/index.js.map +1 -1
  6. package/package.json +2 -2
  7. package/src/client/index.ts +3 -0
  8. package/src/client/monaco/lsp/providers.ts +28 -6
  9. package/src/client/navHighlight.ts +96 -0
  10. package/src/client/paths.ts +2 -0
  11. package/src/client/rpc.ts +32 -12
  12. package/src/client/sidebar/SidebarView.ts +6 -2
  13. package/src/client/sidebar/menuItems.ts +93 -2
  14. package/src/client/sidebar/panels/FileExplorer.ts +18 -1
  15. package/src/client/sidebar/panels/SvnPanel.ts +303 -0
  16. package/src/client/sidebar/panels/index.ts +44 -3
  17. package/src/client/sidebar/types.ts +20 -0
  18. package/src/client/styles/editor.css +177 -0
  19. package/src/client/svnActions.ts +153 -0
  20. package/src/client/svnLog.ts +356 -0
  21. package/src/client/svnStatus.ts +363 -0
  22. package/src/client/svnStore.ts +150 -0
  23. package/src/client/tabMenu.ts +51 -0
  24. package/src/client/ui/EditorView.ts +583 -24
  25. package/src/client/ui/LogDialog.ts +333 -0
  26. package/src/client/ui/McpSettings.ts +98 -0
  27. package/src/client/ui/ModalShell.ts +79 -0
  28. package/src/client/ui/SideBySideDiff.tsx +139 -0
  29. package/src/client/ui/SnippetsPicker.ts +70 -58
  30. package/src/client/ui/SvnDiffPanel.ts +50 -0
  31. package/src/client/ui/SvnLogDialog.ts +1141 -0
  32. package/src/client/ui/SvnLogStats.ts +243 -0
  33. package/src/client/ui/commandCatalog.ts +72 -0
  34. package/src/debugLog.ts +81 -12
  35. package/src/fileOpenSettings.ts +40 -3
  36. package/src/index.ts +5 -1
  37. package/src/rpc.ts +41 -4
  38. package/src/shared/rpc.ts +53 -1
  39. package/src/shared/svn.ts +377 -0
  40. package/src/shared/svnActions.ts +296 -0
  41. package/src/svn.ts +870 -0
  42. package/src/svnLog.ts +315 -0
  43. package/src/svnText.ts +175 -0
  44. package/src/svnXml.ts +110 -0
@@ -24,6 +24,7 @@ import {
24
24
  } from '../watchDecision.js'
25
25
  import { useFileWatch } from './useFileWatch.js'
26
26
  import { createPdfPanel } from '../pdf/pdfPanel.js'
27
+ import { SvnDiffPanel } from './SvnDiffPanel.js'
27
28
  import { applyOfficial, registerThemes, themeNameOf } from '../monaco/theme.js'
28
29
  import { createDiffRenderer } from '../monaco/diffRender.js'
29
30
  import { ST, callIdAttr, noopHunk, summarize } from '../state/records.js'
@@ -41,6 +42,7 @@ import { setSidePending, SIDEBAR_INSTALL_CMD } from '../sidebarBridge.js'
41
42
  import { upsertViewState, viewStatesLoad, viewStatesSave } from '../state/viewStateCache.js'
42
43
  import { migrateScopedKeys, workspaceScopeOf } from '../state/scopeStore.js'
43
44
  import { modelsForScope, rememberModel } from '../state/modelCache.js'
45
+ import { navFlashRangeOf } from '../navHighlight.js'
44
46
  import { bindingsOf, chordOf, matchEvent, useKeybindingsVersion } from '../keybindings.js'
45
47
  import { getSidebarMinWidth } from '../sidebarMin.js'
46
48
  import { navHistoryFor } from '../navHistory.js'
@@ -57,8 +59,34 @@ import {
57
59
  insertTab, isTreeRevealable, normalizeTabs, pickActive, relativeOf, togglePin,
58
60
  } from '../tabActions.js'
59
61
  import { buildTabMenu } from '../tabMenu.js'
62
+ import { ensureSvnChanges, ensureSvnStatus, getSvnChanges, getSvnStatus, refreshSvnChanges, svnAdd, svnChangeMapOf, svnDiffBase, svnEditorActions, svnRevert, svnTortoise, svnUpdate } from '../svnStatus.js'
63
+ import { ensureSvnLog, getSvnLog, refreshSvnLog, svnDiffPair, svnDiffRev, svnDiffWorking, svnLogKeyOf, svnLogLoadedLimit, svnLogTruncated, svnWcRev, SVN_LOG_SHOW_ALL_LIMIT } from '../svnLog.js'
64
+ import { runSvnAction } from '../svnActions.js'
65
+ import { dshTrace } from '../svnStore.js'
66
+ import { SVN_ACTION_BY_ID, svnActionsFor } from '../../shared/svnActions.js'
67
+ import { isSvnDiffable } from '../../shared/svn.js'
68
+ import type { SvnAction } from '../../shared/svn.js'
60
69
  import { createSaveTimer } from '../saveDebounce.js'
61
70
  import { ContextMenu } from './ContextMenu.js'
71
+ import { SvnLogDialog } from './SvnLogDialog.js'
72
+ import { LogDialog } from './LogDialog.js'
73
+
74
+ /** 日志弹窗每次加载条数(「加载更多」按此步长递增;上限由 host 的 SVN_LOG_SHOW_ALL_CAP 约束)。 */
75
+ const SVN_LOG_PAGE = 100
76
+
77
+ /**
78
+ * 日志弹窗状态 → 取数选项(P1-4/P1-5/P1-6:soc/mrg/range 与状态层键保持同源)。
79
+ * @author ddj 2026年09月17号 / 2026年09月18号
80
+ * @param svnLog 日志弹窗状态(null = 关闭)
81
+ * @returns 状态层取数选项
82
+ */
83
+ function svnLogOptsOf(svnLog) {
84
+ if (!svnLog) return {}
85
+ return { stopOnCopy: svnLog.soc === true, showMerged: svnLog.merged === true, range: svnLog.range ?? null }
86
+ }
87
+
88
+ /** 跳转目标高亮的保留时长(LSP/搜索跳转落地后给用户的位置提示,到期自动清除)。 */
89
+ const NAV_FLASH_MS = 1200
62
90
 
63
91
  /**
64
92
  * 从 Monaco 语言目录取可选语言 id 列表(代码片段新建时的语言下拉来源)。
@@ -158,6 +186,16 @@ export function EditorView(props) {
158
186
  const [diffIdx, setDiffIdx] = React.useState(0) // 当前文件内差异位置(x/x 显示)
159
187
  const [fileIdx, setFileIdx] = React.useState(0) // 全局差异文件位置(x/x 文件 显示)
160
188
  const [tabMenu, setTabMenu] = React.useState(null) // Tab 右键菜单 { x,y,path }(编辑区右键走 Monaco 原生菜单,无此浮层)
189
+ const [svnStatus, setSvnStatus] = React.useState(null) // SVN 能力状态(SvnStatusPayload;null=未加载,SVN 入口隐藏)
190
+ const [svnChanges, setSvnChanges] = React.useState(null) // SVN 变更清单(null=未加载;徽标/菜单判定)
191
+ const [svnDiff, setSvnDiff] = React.useState(null) // 差异视图 { path, base, working, reason, message, leftLabel, rightLabel, scheme }(null=关闭)
192
+ const [svnLog, setSvnLog] = React.useState(null) // 日志弹窗 { target, limit, soc?, range? }(null=关闭;P1-4/P1-6 增选项维度)
193
+ const [svnLogData, setSvnLogData] = React.useState(null) // 日志条目(null=未加载)
194
+ const [svnLogBusy, setSvnLogBusy] = React.useState(false) // 日志在途
195
+ const [svnLogError, setSvnLogError] = React.useState('') // 日志错误文案
196
+ const [svnLogWcRev, setSvnLogWcRev] = React.useState(null) // P1-9:文件目标的工作副本版号(null = 不加粗)
197
+ const [dlogOpen, setDlogOpen] = React.useState(false) // 诊断日志弹窗(自取数据,null 语义无需状态层)
198
+ const [svnUpdateResult, setSvnUpdateResult] = React.useState(null) // update 条目级结果条(null=不显示)
161
199
  const [sidebarOn, setSidebarOn] = React.useState(layout !== 'side') // 侧边栏显隐(侧栏形态默认收起)
162
200
  const [sidebarW, setSidebarW] = React.useState(() => getSidebarMinWidth()) // 侧边栏宽度(初值 = 最小宽度,默认 300)
163
201
  const [activePanel, setActivePanel] = React.useState('explorer') // 激活面板 id
@@ -180,7 +218,9 @@ export function EditorView(props) {
180
218
  const loadSeqRef = React.useRef(0)
181
219
  const programmaticRef = React.useRef(false)
182
220
  const restoredScopeRef = React.useRef(null) // 已恢复状态的作用域(cwd 晚到 sid→ws 时允许重恢复)
183
- const pendingFocusRef = React.useRef(null) // { path, region } 内容加载后跳转
221
+ const pendingFocusRef = React.useRef(null) // { path, region, line, column } 内容加载后跳转
222
+ const navFlashRef = React.useRef([]) // 跳转目标高亮装饰 id(独立于 diff/下划线,随跳转替换)
223
+ const navFlashTimerRef = React.useRef(null) // 跳转目标高亮自动清除计时器
184
224
  // 导航历史(后退/前进):跨文件焦点位置;按会话隔离,会话切换重置
185
225
  const navRef = React.useRef(null)
186
226
  if (!navRef.current) navRef.current = navHistoryFor(scope)
@@ -211,6 +251,8 @@ export function EditorView(props) {
211
251
  const hoverPanelRef = React.useRef(false) // 鼠标是否已进入 Keep/Undo 浮层
212
252
  const batchBusyRef = React.useRef(false) // 批量 Keep All/Undo All 防重入
213
253
  const menuHandlersRef = React.useRef(null) // 右键菜单动作的最新闭包(Monaco addAction 空依赖回调读取)
254
+ const svnMenuDisposersRef = React.useRef([]) // Monaco 右键 SVN 组当前 disposables(同步时先注销旧组)
255
+ const svnStatusRef = React.useRef(null) // svnStatus 渲染值镜像(ensureEditor 空依赖闭包读取最新状态)
214
256
  const [snippetPicker, setSnippetPicker] = React.useState(null) // 'configure' | 'insert' | null(代码片段浮层)
215
257
  const doSaveRef = React.useRef(null) // 保存动作的最新闭包(窗口级保存监听读取)
216
258
  const onEditRef = React.useRef(null) // 编辑置脏的最新闭包(Monaco 内容变化监听经 ref 调用,防首帧 active=null 陈旧闭包)
@@ -798,8 +840,8 @@ export function EditorView(props) {
798
840
  return
799
841
  }
800
842
  if (e?.detail?.line != null) {
801
- // LSP/搜索跳转:打开并定位到行列
802
- openFileAt(p, e?.detail?.line, e?.detail?.column)
843
+ // LSP/搜索跳转:打开并定位到行列(endLine/endColumn 为目标区间,供落地高亮)
844
+ openFileAt(p, e?.detail?.line, e?.detail?.column, e?.detail?.endLine, e?.detail?.endColumn)
803
845
  return
804
846
  }
805
847
  recordNav()
@@ -1030,6 +1072,56 @@ export function EditorView(props) {
1030
1072
 
1031
1073
  // 整行上下移动的实现见下方「指令系统接线」effect(moveRow 单点定义,命令栏与键位共用)。
1032
1074
 
1075
+ // SVN 能力状态:scope 变化拉取 + `edrv:svn-status` 事件驱动刷新(菜单/页签/命令面板显隐数据源)
1076
+ React.useEffect(() => {
1077
+ ensureSvnStatus(sessionId, scope)
1078
+ const onSvnStatus = (event) => {
1079
+ if (!event?.detail?.scope || event.detail.scope === scope) setSvnStatus(getSvnStatus(scope))
1080
+ }
1081
+ window.addEventListener('edrv:svn-status', onSvnStatus)
1082
+ return () => window.removeEventListener('edrv:svn-status', onSvnStatus)
1083
+ }, [sessionId, scope])
1084
+
1085
+ // SVN 变更清单:状态就绪且受管理时才拉取(非 SVN 工作区零请求)+ `edrv:svn-changes` 驱动刷新
1086
+ // 依赖里放 managed/svnCli:状态后到时会自动补拉(首次渲染时状态尚为 null)
1087
+ React.useEffect(() => {
1088
+ const ready = Boolean(svnStatus?.managed && svnStatus?.svnCli)
1089
+ if (ready) ensureSvnChanges(sessionId, scope)
1090
+ const onSvnChanges = (event) => {
1091
+ if (!event?.detail?.scope || event.detail.scope === scope) setSvnChanges(getSvnChanges(scope))
1092
+ }
1093
+ window.addEventListener('edrv:svn-changes', onSvnChanges)
1094
+ // 状态刚到达(同轮已拉到清单)时也同步一次当前值,避免事件早于监听错过
1095
+ setSvnChanges(getSvnChanges(scope))
1096
+ return () => window.removeEventListener('edrv:svn-changes', onSvnChanges)
1097
+ }, [sessionId, scope, svnStatus?.managed, svnStatus?.svnCli])
1098
+
1099
+ // SVN 日志:弹窗打开期间订阅 `edrv:svn-log`(键经 svnLogKeyOf 统一拼装,含 P1-4/P1-5/P1-6 选项维度)
1100
+ React.useEffect(() => {
1101
+ if (!svnLog) return undefined
1102
+ const target = svnLog.target || ''
1103
+ const opts = svnLogOptsOf(svnLog)
1104
+ const key = svnLogKeyOf(scope, target, opts)
1105
+ const sync = (event) => {
1106
+ dshTrace('svnLog.syncEvent', { got: event?.detail?.key ?? null, expected: key })
1107
+ if (event?.detail?.key && event.detail.key !== key) return
1108
+ setSvnLogData(getSvnLog(scope, target, 0, opts))
1109
+ setSvnLogBusy(false)
1110
+ }
1111
+ window.addEventListener('edrv:svn-log', sync)
1112
+ // 打开时先同步一次(状态层可能已缓存)
1113
+ setSvnLogData(getSvnLog(scope, target, 0, opts))
1114
+ return () => window.removeEventListener('edrv:svn-log', sync)
1115
+ }, [svnLog, scope])
1116
+
1117
+ // P1-9:文件目标拉取工作副本版号(目录/根不拉,官方对目录需 crawl 工作副本,本插件不做)
1118
+ React.useEffect(() => {
1119
+ if (!svnLog || !svnLog.target) { setSvnLogWcRev(null); return undefined }
1120
+ let live = true
1121
+ void svnWcRev(sessionId, svnLog.target).then((rev) => { if (live) setSvnLogWcRev(rev) })
1122
+ return () => { live = false }
1123
+ }, [svnLog?.target, sessionId])
1124
+
1033
1125
  /**
1034
1126
  * 指令系统接线:命令栏/键位/第三方派发的 `edrv.command.*` 事件落到编辑器动作。
1035
1127
  * 全部动作经最新闭包执行(与窗口级键位监听同源);事件名逐条字面书写,
@@ -1094,6 +1186,26 @@ export function EditorView(props) {
1094
1186
  if (!addToConversation) { setStatus('添加到对话不可用'); return }
1095
1187
  addToConversation.appendReference(sessionId, path, range).then((o) => setStatus(statusOfAdd(o, '已添加选中内容为引用')))
1096
1188
  }],
1189
+ // SVN:命令面板动作(活动文件;无活动文件时 update/commit 用工作区根,其余提示)
1190
+ ['edrv.command.svnUpdate', () => runSvnUpdate(activeRef.current ?? '')],
1191
+ ['edrv.command.svnRefreshChanges', () => runSvnReload()],
1192
+ ['edrv.command.svnDiffBase', () => runSvnDiffBase(activeRef.current)],
1193
+ ['edrv.command.svnAdd', () => runSvnAdd(activeRef.current)],
1194
+ ['edrv.command.svnRevertCli', () => runSvnRevertCli(activeRef.current)],
1195
+ ['edrv.command.svnLog', () => openSvnLog(activeRef.current || undefined)],
1196
+ ['edrv.command.svnCleanup', () => runSvnAction(SVN_ACTION_BY_ID.cleanup, {
1197
+ sessionId,
1198
+ scope,
1199
+ notify: (message) => setStatus(message),
1200
+ refreshChanges: () => refreshSvnChanges(sessionId, scope),
1201
+ })],
1202
+ ['edrv.command.svnTortoiseCommit', () => runSvnTortoise('commit', activeRef.current ?? '')],
1203
+ ['edrv.command.svnTortoiseLog', () => runSvnTortoise('log', activeRef.current)],
1204
+ ['edrv.command.svnTortoiseDiff', () => runSvnTortoise('diff', activeRef.current)],
1205
+ ['edrv.command.svnTortoiseBlame', () => runSvnTortoise('blame', activeRef.current)],
1206
+ ['edrv.command.svnTortoiseRevert', () => runSvnTortoise('revert', activeRef.current)],
1207
+ // 诊断日志弹窗(同片段选择器:执行一条命令即关命令栏,避免残浮层遮挡)
1208
+ ['edrv.command.showLogs', () => { closeCommandPalette(); setDlogOpen(true) }],
1097
1209
  ]
1098
1210
  const byName = new Map(handlers)
1099
1211
  const onCommand = (event) => {
@@ -1213,6 +1325,64 @@ export function EditorView(props) {
1213
1325
 
1214
1326
  monacoRef.current = monaco
1215
1327
 
1328
+ /** 从编辑器实例 model URI 解析活动文件路径(工作区相对;无模型返回 null)。 */
1329
+ const modelPathOf = (edx) => {
1330
+ const uriPath = edx?.getModel?.()?.uri?.path
1331
+ return uriPath ? decodeURIComponent(String(uriPath).replace(/^\//, '')) : null
1332
+ }
1333
+
1334
+ /**
1335
+ * 同步 Monaco 右键菜单 SVN 组:先注销旧组,再按最新 svn 状态注册(空状态 = 全部移除)。
1336
+ * ensureEditor(编辑器创建)与 svn 状态变化两条通道都会调用,幂等。
1337
+ * @author ddj 2026年09月16号
1338
+ * @param ed Monaco 编辑器实例(未创建传 null)
1339
+ */
1340
+ const syncSvnMenuActs = (ed) => {
1341
+ const stale = svnMenuDisposersRef.current
1342
+ svnMenuDisposersRef.current = []
1343
+ for (const dispose of stale) {
1344
+ try { dispose?.() } catch { /* dispose 异常忽略,不阻塞重注册 */ }
1345
+ }
1346
+ if (!ed || typeof ed.addAction !== 'function') return
1347
+ const changeMap = svnChangeMapOf(scope)
1348
+ const activeEntry = activeRef.current ? changeMap[activeRef.current] : undefined
1349
+ for (const item of svnEditorActions(svnStatusRef.current, {
1350
+ versioned: activeEntry ? activeEntry.versioned : true,
1351
+ status: activeEntry?.status,
1352
+ diffable: activeRef.current ? isSvnDiffable(activeRef.current, activeEntry?.status) : true,
1353
+ })) {
1354
+ const handle = ed.addAction({
1355
+ id: item.id,
1356
+ label: item.label,
1357
+ contextMenuGroupId: '2_edrv',
1358
+ precondition: 'editorTextFocus',
1359
+ run: (edx) => {
1360
+ const relPath = modelPathOf(edx)
1361
+ if (item.kind === 'tortoise') { menuHandlersRef.current?.svnTortoise?.(item.tortoiseAction, relPath); return }
1362
+ // 自研动作经动作目录统一执行(与树/页签/命令栏同一执行器)
1363
+ const def = SVN_ACTION_BY_ID[item.actionId]
1364
+ if (!def) return
1365
+ runSvnAction(def, {
1366
+ sessionId,
1367
+ scope,
1368
+ path: def.id === 'update' ? (relPath ?? '') : relPath,
1369
+ notify: (message) => setStatus(message),
1370
+ openSvnDiff: (p) => runSvnDiffBase(p),
1371
+ openSvnLog: (p) => openSvnLog(p ?? relPath),
1372
+ refreshChanges: () => refreshSvnChanges(sessionId, scope),
1373
+ })
1374
+ },
1375
+ })
1376
+ svnMenuDisposersRef.current.push(handle?.dispose ? () => handle.dispose() : null)
1377
+ }
1378
+ }
1379
+
1380
+ // Monaco 右键 SVN 组随 svn 状态与变更清单增减;编辑器尚未创建时 no-op(创建时由 ensureEditor 补注册)
1381
+ React.useEffect(() => {
1382
+ syncSvnMenuActs(editorRef.current)
1383
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1384
+ }, [monaco, svnStatus, svnChanges])
1385
+
1216
1386
  const getModel = (path, text) => {
1217
1387
  const cache = modelsRef.current
1218
1388
  let model = cache.get(path)
@@ -1358,14 +1528,17 @@ export function EditorView(props) {
1358
1528
  onEditRef.current = onEdit
1359
1529
 
1360
1530
  // model 同步(当前内容)
1531
+ // ⚠️ 必须以 contentReady(contentPath === active)为门,不能用 `content !== null`:
1532
+ // 切页签的那一帧 content 仍是**上一个文件**的内容,据此建 model 会先塞入陈旧文本,
1533
+ // 待真实内容到达再由 getModel→setValue 覆盖,光标随之被重置(见下方跳转 effect 注释)。
1361
1534
  React.useEffect(() => {
1362
- if (!monaco || !editorRef.current || !active || content === null) return
1535
+ if (!monaco || !editorRef.current || !active || !contentReady) return
1363
1536
  const ed = editorRef.current
1364
1537
  const model = getModel(active, content)
1365
1538
  if (ed.getModel() !== model) ed.setModel(model)
1366
1539
  restoreViewState(active)
1367
1540
  setLoadStage((prev) => ({ progress: Math.max(96, prev.progress), message: '创建编辑器视图…' }))
1368
- }, [monaco, active, content])
1541
+ }, [monaco, active, content, contentReady])
1369
1542
 
1370
1543
  // PDF 面板外壳 ref 回调(div 仅在 PDF 分支渲染,refs 先于 effect 就绪)
1371
1544
  const ensurePdfHost = (node) => { pdfHostRef.current = node }
@@ -1406,6 +1579,9 @@ export function EditorView(props) {
1406
1579
  saveViewStateRef.current?.()
1407
1580
  diffRendererRef.current?.dispose?.()
1408
1581
  hideReferencesOverlay()
1582
+ // 跳转目标高亮的计时器随卸载清理(装饰随编辑器 dispose 一并消失)
1583
+ if (navFlashTimerRef.current) { clearTimeout(navFlashTimerRef.current); navFlashTimerRef.current = null }
1584
+ navFlashRef.current = []
1409
1585
  if (editorRef.current) { editorRef.current.dispose(); editorRef.current = null }
1410
1586
  // model 不在此销毁:跨挂载缓存按作用域存活(modelCache 切作用域时统一释放)
1411
1587
  for (const ctl of pdfCtlRef.current.values()) ctl.destroy()
@@ -1468,11 +1644,7 @@ export function EditorView(props) {
1468
1644
  // 有选区时额外显示「选中内容」项:context key edrvSelection 由光标选区变化驱动。
1469
1645
  // 路径/选区在点击时从 model/editor 实时读取(不依赖闭包里的过期 active)。
1470
1646
  const menuHandlers = () => menuHandlersRef.current
1471
- const pathOf = (edx) => {
1472
- const model = edx?.getModel?.()
1473
- const uriPath = model?.uri?.path
1474
- return uriPath ? decodeURIComponent(String(uriPath).replace(/^\//, '')) : null
1475
- }
1647
+ const pathOf = modelPathOf
1476
1648
  const selectionOf = (edx) => {
1477
1649
  const s = edx?.getSelection?.()
1478
1650
  if (!s) return null
@@ -1532,6 +1704,8 @@ export function EditorView(props) {
1532
1704
  id: 'edrv.showCommands', label: '显示所有命令', contextMenuGroupId: '1_edrv',
1533
1705
  run: () => window.dispatchEvent(new CustomEvent('edrv.command.showCommands')),
1534
1706
  })
1707
+ // SVN 组按创建时的最新状态注册(此后由 [monaco, svnStatus] effect 随状态增减)
1708
+ syncSvnMenuActs(ed)
1535
1709
  bindLspUnderline(ed, m)
1536
1710
  // AI 补全:编辑器实例登记(差异静默判定用)+ Alt+\ 手动触发 ghost text
1537
1711
  trackAiEditor(ed)
@@ -1573,13 +1747,48 @@ export function EditorView(props) {
1573
1747
  editorRef.current = ed
1574
1748
  }, [])
1575
1749
 
1576
- // 打开文件后的跳转(导航历史恢复/差异聚焦/搜索行列跳转,内容就绪后执行一次)
1750
+ /**
1751
+ * 跳转落地后给目标区域挂临时高亮(默认 1.2s 后自动清除)。
1752
+ *
1753
+ * 为什么自绘:原生 `_openReference` 跳转后会挂 `symbolHighlight` 装饰并在 350ms 后清除,
1754
+ * 但本插件用 registerEditorOpener 接管了打开动作(自行派发 edrv:open-editor),
1755
+ * 原生那段续作不会执行 → 跳转后没有任何目标提示。这里补上同等语义的高亮。
1756
+ * 装饰 id 独立保存,与差异渲染(diffRender)和 Ctrl+hover 下划线(underline)互不干扰;
1757
+ * 连续跳转先替换旧装饰,避免叠加残留。
1758
+ * @author ddj 2026年09月17号
1759
+ * @param ed Monaco 编辑器
1760
+ * @param target 目标位置 { line, column, endLine?, endColumn? }(均 1-based)
1761
+ */
1762
+ const flashNavTarget = (ed, target) => {
1763
+ const model = ed?.getModel?.()
1764
+ if (!model || !target) return
1765
+ const range = navFlashRangeOf(model, target)
1766
+ if (!range) return
1767
+ if (navFlashTimerRef.current) { clearTimeout(navFlashTimerRef.current); navFlashTimerRef.current = null }
1768
+ navFlashRef.current = ed.deltaDecorations(navFlashRef.current, [{
1769
+ range,
1770
+ options: { className: 'edrv-nav-target', isWholeLine: false },
1771
+ }])
1772
+ navFlashTimerRef.current = setTimeout(() => {
1773
+ navFlashTimerRef.current = null
1774
+ const current = editorRef.current
1775
+ navFlashRef.current = current ? current.deltaDecorations(navFlashRef.current, []) : []
1776
+ }, NAV_FLASH_MS)
1777
+ }
1778
+
1779
+ // 打开文件后的跳转(导航历史恢复/差异聚焦/搜索与 LSP 行列跳转,目标内容就绪后执行一次)
1780
+ //
1781
+ // ⚠️ 门控必须用 contentReady(contentPath === active),不能用 `content === null`:
1782
+ // 跨文件跳转时切页签的那一帧 content 仍是**上一个文件**的内容(非 null),据此判定
1783
+ // 「已就绪」会提前定位;随后真实内容到达触发 model 重设/内容写入,Monaco 会重置光标,
1784
+ // 落点丢失且 pendingFocus 已消费,最终停在 {1,1}。实测(v0.4.6,Ctrl+点击枚举成员):
1785
+ // opener 正确派发 line=349,落地后编辑器实际停在 1:1 且无高亮;同文件跳转不换内容故不复现。
1577
1786
  React.useEffect(() => {
1578
1787
  const ed = editorRef.current
1579
- if (!ed || content === null) return
1788
+ if (!ed || !contentReady) return
1580
1789
  // 导航历史恢复优先:viewState 快照恢复滚动/折叠,行列补定位
1581
1790
  const nav = navPendingRef.current
1582
- if (nav && nav.path === active) {
1791
+ if (nav && sameFile(nav.path, active)) {
1583
1792
  navPendingRef.current = null
1584
1793
  if (nav.viewState) {
1585
1794
  try { ed.restoreViewState(nav.viewState) } catch (e) { /* 非法快照忽略(行号越界自动兜底) */ }
@@ -1592,12 +1801,15 @@ export function EditorView(props) {
1592
1801
  return
1593
1802
  }
1594
1803
  const pf = pendingFocusRef.current
1595
- if (!pf || pf.path !== active) return
1804
+ if (!pf || !sameFile(pf.path, active)) return
1596
1805
  if (pf.line != null) {
1597
- // 搜索命中跳转:直接定位到行/列(不依赖差异区域)
1806
+ // 搜索命中 / LSP 跳转:直接定位到行/列(不依赖差异区域)并高亮目标区域
1598
1807
  pendingFocusRef.current = null
1599
- ed.revealLineInCenter(Math.max(1, pf.line))
1600
- ed.setPosition({ lineNumber: Math.max(1, pf.line), column: Math.max(1, pf.column ?? 1) })
1808
+ const line = Math.max(1, pf.line)
1809
+ const column = Math.max(1, pf.column ?? 1)
1810
+ ed.revealLineInCenter(line)
1811
+ ed.setPosition({ lineNumber: line, column })
1812
+ flashNavTarget(ed, { line, column, endLine: pf.endLine, endColumn: pf.endColumn })
1601
1813
  ed.focus()
1602
1814
  return
1603
1815
  }
@@ -1607,7 +1819,7 @@ export function EditorView(props) {
1607
1819
  ed.revealLineInCenter(Math.max(1, target.start ?? 1))
1608
1820
  ed.setPosition({ lineNumber: Math.max(1, target.start ?? 1), column: 1 })
1609
1821
  ed.focus()
1610
- }, [active, content, pendingRegions, focusRequest])
1822
+ }, [active, content, contentReady, pendingRegions, focusRequest])
1611
1823
 
1612
1824
  const jumpTo = (region) => {
1613
1825
  if (editorRef.current) {
@@ -1959,7 +2171,186 @@ export function EditorView(props) {
1959
2171
  }
1960
2172
 
1961
2173
  // 供 Monaco 原生右键菜单 addAction 读取的最新动作闭包(空依赖回调不随渲染重建)
1962
- menuHandlersRef.current = { addRefToChat, openInExplorer }
2174
+ /**
2175
+ * SVN 更新(CLI,全平台):页签菜单/命令面板/编辑区右键共用;path 缺省 = 工作区根。
2176
+ * P3:结果含条目级明细,成功后展示可关闭的结果条(冲突红字高亮)。
2177
+ */
2178
+ const runSvnUpdate = (relPath) => {
2179
+ void svnUpdate(sessionId, relPath ?? '').then((o) => {
2180
+ setStatus(o.message)
2181
+ // 更新会改工作副本状态:成功后重查变更清单(徽标/面板跟随)
2182
+ if (o.ok) refreshSvnChanges(sessionId, scope)
2183
+ // P3:有条目级明细时展示结果条(无变化则清空,不留旧结果)
2184
+ setSvnUpdateResult(o.ok ? { summary: o.message, entries: o.entries ?? [], conflicts: o.conflicts ?? [] } : null)
2185
+ })
2186
+ }
2187
+ /** 强制重查 SVN 变更清单(命令栏「SVN 刷新变更」)。 */
2188
+ const runSvnReload = () => {
2189
+ refreshSvnChanges(sessionId, scope)
2190
+ setStatus('已刷新 SVN 变更')
2191
+ }
2192
+
2193
+ /**
2194
+ * 打开基线差异视图(BASE 左 / 工作区右,只读)。
2195
+ * 编辑区右上差异视图以活动文件为语义:无活动文件直接提示,不猜目标。
2196
+ * @author ddj 2026年09月16号
2197
+ * @param relPath 目标工作区相对路径
2198
+ */
2199
+ const runSvnDiffBase = (relPath) => {
2200
+ if (!relPath) { setStatus('无活动文件'); return }
2201
+ setStatus('读取基线…')
2202
+ void svnDiffBase(sessionId, relPath).then((outcome) => {
2203
+ if (!outcome.ok) { setStatus(outcome.message); return }
2204
+ setSvnDiff({
2205
+ path: relPath,
2206
+ base: outcome.base,
2207
+ working: outcome.working,
2208
+ reason: outcome.reason,
2209
+ message: outcome.base === null ? '' : outcome.message,
2210
+ leftLabel: 'BASE',
2211
+ rightLabel: '工作区',
2212
+ scheme: 'edrv-svn-base',
2213
+ })
2214
+ setStatus(outcome.message)
2215
+ })
2216
+ }
2217
+
2218
+ /**
2219
+ * 打开某版本的差异视图(REV-1 ↔ REV 只读并排)。
2220
+ * 左侧缺失(该版本尚无此文件 = 新增)属正常语义,不改文案为错误。
2221
+ * @author ddj 2026年09月16号
2222
+ * @param relPath 目标工作区相对路径
2223
+ * @param revision 目标版本
2224
+ */
2225
+ const runSvnDiffRev = (relPath, revision) => {
2226
+ if (!relPath) { setStatus('无目标文件'); return }
2227
+ setStatus('读取 r' + revision + ' 差异…')
2228
+ void svnDiffRev(sessionId, relPath, revision).then((outcome) => {
2229
+ if (!outcome.ok) { setStatus('读取版本差异失败:' + (outcome.error ?? '未知错误')); return }
2230
+ const isAdd = outcome.left === null
2231
+ setSvnDiff({
2232
+ path: relPath,
2233
+ base: outcome.left,
2234
+ working: outcome.right,
2235
+ reason: outcome.reason,
2236
+ message: isAdd ? '该版本新增此文件(无上一版可比)' : '',
2237
+ leftLabel: 'r' + Math.max(1, revision - 1),
2238
+ rightLabel: 'r' + revision,
2239
+ scheme: 'edrv-svn-rev' + revision,
2240
+ })
2241
+ setStatus('已打开 r' + revision + ' 差异')
2242
+ })
2243
+ }
2244
+
2245
+ /**
2246
+ * 打开两个版本的差异视图(P1-2 比较两个修订;左右标签按传入顺序,不自动排序)。
2247
+ * 左右任一侧缺失(新增/删除)属正常语义,渲染为空而非报错。
2248
+ * @author ddj 2026年09月17号
2249
+ * @param relPath 目标工作区相对路径
2250
+ * @param revA 左侧版本(选中顺序在前)
2251
+ * @param revB 右侧版本(选中顺序在后)
2252
+ */
2253
+ const runSvnDiffPair = (relPath, revA, revB) => {
2254
+ if (!relPath) { setStatus('仅文件目标支持比较两个修订'); return }
2255
+ setStatus('读取 r' + revA + ' ↔ r' + revB + ' 差异…')
2256
+ void svnDiffPair(sessionId, relPath, revA, revB).then((outcome) => {
2257
+ if (!outcome.ok) { setStatus('读取版本差异失败:' + (outcome.error ?? '未知错误')); return }
2258
+ const notes = []
2259
+ if (outcome.left === null) notes.push('r' + revA + ' 无此文件')
2260
+ if (outcome.right === null) notes.push('r' + revB + ' 无此文件')
2261
+ setSvnDiff({
2262
+ path: relPath,
2263
+ base: outcome.left,
2264
+ working: outcome.right,
2265
+ reason: outcome.reason,
2266
+ message: notes.join(';'),
2267
+ leftLabel: 'r' + revA,
2268
+ rightLabel: 'r' + revB,
2269
+ scheme: 'edrv-svn-pair-' + revA + '-' + revB,
2270
+ })
2271
+ setStatus('已打开 r' + revA + ' ↔ r' + revB + ' 差异')
2272
+ })
2273
+ }
2274
+
2275
+ /**
2276
+ * 打开「某版本 ↔ 工作副本」差异视图(P1-1 Compare with working copy,只读并排)。
2277
+ * 左侧缺失(该版本尚无此文件)属正常语义,渲染为空而非报错。
2278
+ * @author ddj 2026年09月17号
2279
+ * @param relPath 目标工作区相对路径
2280
+ * @param revision 左侧版本
2281
+ */
2282
+ const runSvnDiffWorking = (relPath, revision) => {
2283
+ if (!relPath) { setStatus('仅文件目标支持与工作副本比较'); return }
2284
+ setStatus('读取 r' + revision + ' ↔ 工作副本 差异…')
2285
+ void svnDiffWorking(sessionId, relPath, revision).then((outcome) => {
2286
+ if (!outcome.ok) { setStatus('读取工作副本差异失败:' + (outcome.error ?? '未知错误')); return }
2287
+ setSvnDiff({
2288
+ path: relPath,
2289
+ base: outcome.left,
2290
+ working: outcome.right,
2291
+ reason: outcome.reason,
2292
+ message: outcome.left === null ? '该版本尚无此文件(右侧为工作副本内容)' : '',
2293
+ leftLabel: 'r' + revision,
2294
+ rightLabel: '工作区',
2295
+ scheme: 'edrv-svn-work-' + revision,
2296
+ })
2297
+ setStatus('已打开 r' + revision + ' ↔ 工作副本 差异')
2298
+ })
2299
+ }
2300
+
2301
+ /**
2302
+ * 打开日志弹窗(P3):加载该目标的提交历史。
2303
+ * @author ddj 2026年09月16号
2304
+ * @param relPath 目标相对路径(缺省 = 工作区根)
2305
+ */
2306
+ const openSvnLog = (relPath) => {
2307
+ const target = relPath || ''
2308
+ dshTrace('svnLog.open', { target, scope, hasSession: Boolean(sessionId) })
2309
+ setSvnLog({ target, limit: SVN_LOG_PAGE })
2310
+ setSvnLogData(getSvnLog(scope, target, SVN_LOG_PAGE))
2311
+ setSvnLogError('')
2312
+ setSvnLogBusy(true)
2313
+ ensureSvnLog(sessionId, scope, target, SVN_LOG_PAGE)
2314
+ }
2315
+
2316
+ /** 加入版本控制(活动文件;未纳入版本控制的文件才有意义,host 侧会再校验)。 */
2317
+ const runSvnAdd = (relPath) => {
2318
+ if (!relPath) { setStatus('无活动文件'); return }
2319
+ void svnAdd(sessionId, [relPath]).then((o) => {
2320
+ setStatus(o.message)
2321
+ refreshSvnChanges(sessionId, scope)
2322
+ })
2323
+ }
2324
+
2325
+ /** 还原(活动文件,CLI):confirm 守卫,避免误触丢弃本地改动。 */
2326
+ const runSvnRevertCli = (relPath) => {
2327
+ if (!relPath) { setStatus('无活动文件'); return }
2328
+ if (!window.confirm('确认还原「' + relPath + '」的本地改动?新增(A)文件在磁盘上会保留,仅取消登记。')) return
2329
+ void svnRevert(sessionId, [relPath]).then((o) => {
2330
+ setStatus(o.message)
2331
+ refreshSvnChanges(sessionId, scope)
2332
+ // 当前打开的文件被还原:重载磁盘内容,避免编辑器仍显示已丢弃的改动
2333
+ if (o.ok && activeRef.current === relPath) reloadFile()
2334
+ })
2335
+ }
2336
+
2337
+ /** TortoiseSVN 动作(Windows):需要文件/目录目标(编辑区右键无活动文件时提示)。 */
2338
+ const runSvnTortoise = (action: SvnAction, relPath) => {
2339
+ if (!relPath) { setStatus('无活动文件'); return }
2340
+ void svnTortoise(sessionId, action, relPath).then((o) => setStatus(o.message))
2341
+ }
2342
+
2343
+ // 渲染期镜像(赋值在被引用 const 定义之后,防 TDZ):Monaco addAction 空依赖回调读取最新动作与 svn 状态
2344
+ svnStatusRef.current = svnStatus
2345
+ menuHandlersRef.current = {
2346
+ addRefToChat,
2347
+ openInExplorer,
2348
+ svnUpdate: runSvnUpdate,
2349
+ svnDiffBase: runSvnDiffBase,
2350
+ svnAdd: runSvnAdd,
2351
+ svnRevertCli: runSvnRevertCli,
2352
+ svnTortoise: runSvnTortoise,
2353
+ }
1963
2354
 
1964
2355
  /**
1965
2356
  * 复制文本到剪贴板(状态栏反馈;浏览器拒绝时提示,不抛异常)。
@@ -2009,6 +2400,28 @@ export function EditorView(props) {
2009
2400
  closeTab(activeRef.current)
2010
2401
  }
2011
2402
 
2403
+ /**
2404
+ * 页签菜单的 SVN 动作分派(由 shared 动作目录生成,与树菜单/命令栏共用执行器与显隐规则)。
2405
+ * @author ddj 2026年09月16号
2406
+ * @param path 右键目标页签路径
2407
+ * @returns id → 动作
2408
+ */
2409
+ const svnTabActions = (path) => {
2410
+ const out = {}
2411
+ for (const action of svnActionsFor('tab')) {
2412
+ out['svn-' + action.id] = () => runSvnAction(action, {
2413
+ sessionId,
2414
+ scope,
2415
+ path,
2416
+ notify: (message) => setStatus(message),
2417
+ openSvnDiff: (p) => runSvnDiffBase(p),
2418
+ openSvnLog: (p) => openSvnLog(p ?? path),
2419
+ refreshChanges: () => refreshSvnChanges(sessionId, scope),
2420
+ })
2421
+ }
2422
+ return out
2423
+ }
2424
+
2012
2425
  /**
2013
2426
  * 页签右键菜单动作表(按菜单条目 id 分派;全部读 ref 取最新状态,防陈旧闭包)。
2014
2427
  * @author ddj 2026年09月11号
@@ -2027,6 +2440,7 @@ export function EditorView(props) {
2027
2440
  'reveal-in-os': () => openInExplorer(path),
2028
2441
  'reveal-in-view': () => revealTabInView(path),
2029
2442
  'toggle-pinned': () => toggleTabPin(path),
2443
+ ...svnTabActions(path),
2030
2444
  })
2031
2445
 
2032
2446
  /**
@@ -2037,6 +2451,7 @@ export function EditorView(props) {
2037
2451
  const tabMenuEntries = () => {
2038
2452
  if (!tabMenu) return []
2039
2453
  const actions = tabMenuActions(tabMenu.path)
2454
+ const changeMap = svnChangeMapOf(scope)
2040
2455
  return buildTabMenu({
2041
2456
  path: tabMenu.path,
2042
2457
  tabs: tabsRef.current,
@@ -2045,6 +2460,12 @@ export function EditorView(props) {
2045
2460
  cwd,
2046
2461
  hasSession: Boolean(sessionId),
2047
2462
  canAddToConversation: Boolean(addToConversation),
2463
+ svnReady: Boolean(svnStatus?.managed && svnStatus?.svnCli),
2464
+ tortoiseReady: Boolean(svnStatus?.managed && svnStatus?.tortoise),
2465
+ // 自研替换时间线:自研就绪的能力对应 Tortoise 项不出现
2466
+ svnFeatures: svnStatus?.svnFeatures,
2467
+ // 目标文件的 SVN 状态:CLI 三项(比较/加入/还原)按它显隐
2468
+ svnStatusOfTarget: changeMap[tabMenu.path]?.status,
2048
2469
  closeChord: chordOf('edrv.closeTab'),
2049
2470
  }).map((entry) => Object.assign({}, entry, { onClick: actions[entry.id] }))
2050
2471
  }
@@ -2071,23 +2492,28 @@ export function EditorView(props) {
2071
2492
 
2072
2493
  const openFile = (path, focusDiff) => {
2073
2494
  if (!path) return
2495
+ // 打开文件即离开基线差异审阅态(差异视图是「当前文件」的临时视图)
2496
+ setSvnDiff(null)
2074
2497
  recordNav()
2075
2498
  addTab(path, true)
2076
2499
  if (focusDiff) pendingFocusRef.current = { path, region: null }
2077
2500
  }
2078
2501
 
2079
2502
  /**
2080
- * 打开文件并跳转到指定行列(搜索面板命中跳转)。
2081
- * @author ddj 2026年08月26号
2503
+ * 打开文件并跳转到指定行列(搜索面板命中 / LSP 定义与引用跳转)。
2504
+ * 落地后会把目标区域短暂高亮(见 flashNavTarget),便于在长文件中一眼看到落点。
2505
+ * @author ddj 2026年08月26号 / 2026年09月17号
2082
2506
  * @param path 文件路径
2083
2507
  * @param line 目标行(缺省仅打开不跳转)
2084
2508
  * @param column 目标列(缺省 1)
2509
+ * @param endLine 目标区间结束行(LSP 定义选区,可空 = 按单词/整行推定)
2510
+ * @param endColumn 目标区间结束列(可空)
2085
2511
  */
2086
- const openFileAt = (path, line, column) => {
2512
+ const openFileAt = (path, line, column, endLine, endColumn) => {
2087
2513
  if (!path) return
2088
2514
  recordNav()
2089
2515
  addTab(path, true)
2090
- pendingFocusRef.current = { path, region: null, line: line ?? null, column: column ?? 1 }
2516
+ pendingFocusRef.current = { path, region: null, line: line ?? null, column: column ?? 1, endLine: endLine ?? null, endColumn: endColumn ?? null }
2091
2517
  setFocusRequest((value) => value + 1)
2092
2518
  }
2093
2519
 
@@ -2281,7 +2707,21 @@ export function EditorView(props) {
2281
2707
  retry ? React.createElement('button', { className: 'edrv-pill edrv-pill-ghost', onClick: retry }, '重试') : null)
2282
2708
 
2283
2709
  let body
2284
- if (!monaco && !monacoErr) {
2710
+ if (svnDiff) {
2711
+ // 差异视图优先占位(覆盖编辑器主体):只读审阅态,关闭后回到原编辑器
2712
+ body = React.createElement(SvnDiffPanel, {
2713
+ key: 'edrv-svndiff:' + svnDiff.scheme + ':' + svnDiff.path,
2714
+ monaco,
2715
+ path: svnDiff.path,
2716
+ base: svnDiff.base,
2717
+ working: svnDiff.working,
2718
+ reason: svnDiff.reason,
2719
+ message: svnDiff.message,
2720
+ leftLabel: svnDiff.leftLabel,
2721
+ rightLabel: svnDiff.rightLabel,
2722
+ onClose: () => setSvnDiff(null),
2723
+ })
2724
+ } else if (!monaco && !monacoErr) {
2285
2725
  body = loadingBody(loadStage.message, loadStage.progress)
2286
2726
  } else if (monacoErr) {
2287
2727
  body = React.createElement('div', { className: 'edrv-empty' },
@@ -2427,6 +2867,14 @@ export function EditorView(props) {
2427
2867
  outlineSources: props.outlineSources,
2428
2868
  fileMenuItems: props.fileMenuItems,
2429
2869
  addToConversation,
2870
+ svn: svnStatus,
2871
+ // SVN 变更:面板列表 + 文件树徽标查表(客户端合成,不改 listDir 契约)
2872
+ svnChanges,
2873
+ svnChangeMap: svnChangeMapOf(scope),
2874
+ openSvnDiff: (p) => runSvnDiffBase(p),
2875
+ refreshSvnChanges: () => refreshSvnChanges(sessionId, scope),
2876
+ openSvnLog: (p) => openSvnLog(p),
2877
+ confirm: (message) => (typeof window === 'undefined' ? false : window.confirm(message)),
2430
2878
  notify: (message) => setStatus(message),
2431
2879
  }
2432
2880
 
@@ -2450,11 +2898,49 @@ export function EditorView(props) {
2450
2898
  React.createElement('button', { className: 'edrv-pill edrv-pill-ghost', title: '复制安装命令', onClick: copyInstallCmd }, '复制命令'),
2451
2899
  React.createElement('button', { className: 'edrv-side-hint-close', title: '关闭提示', 'aria-label': '关闭提示', onClick: dismissHint }, '×'))
2452
2900
  : null
2901
+ /**
2902
+ * SVN 更新结果条(P3):逐条展示 update 的条目级状态,冲突红字高亮,可关闭。
2903
+ * 只在本次 update 有条目明细时渲染(无明细/失败时不占用编辑区高度)。
2904
+ * @author ddj 2026年09月16号
2905
+ * @returns 结果条元素或 null
2906
+ */
2907
+ const svnResultBar = () => {
2908
+ const result = svnUpdateResult
2909
+ if (!result || !result.entries.length) return null
2910
+ const conflicts = new Set(result.conflicts ?? [])
2911
+ return React.createElement('div', { className: 'edrv-svnres' },
2912
+ React.createElement('span', { className: 'edrv-svnres-head' }, 'SVN 更新'),
2913
+ React.createElement('div', { className: 'edrv-svnres-list' },
2914
+ result.entries.map((entry, idx) => React.createElement('span', {
2915
+ key: entry.path + ':' + idx,
2916
+ className: 'edrv-svnres-item' + (conflicts.has(entry.path) ? ' edrv-svnres-conflict' : ''),
2917
+ title: entry.label + ' ' + entry.path,
2918
+ }, entry.action + ' ' + entry.path))),
2919
+ React.createElement('span', { style: { flex: 1 } }),
2920
+ (conflicts.size
2921
+ ? React.createElement('button', {
2922
+ className: 'edrv-svn-act',
2923
+ title: '在工作区中显示第一个冲突文件',
2924
+ onClick: () => {
2925
+ const first = [...conflicts][0]
2926
+ const hit = result.entries.find((entry) => entry.path === first)
2927
+ if (hit?.path) openFile(hit.path, false)
2928
+ },
2929
+ }, '查看冲突文件')
2930
+ : null),
2931
+ React.createElement('button', {
2932
+ className: 'edrv-svn-act',
2933
+ title: '关闭结果条',
2934
+ onClick: () => setSvnUpdateResult(null),
2935
+ }, '✕'))
2936
+ }
2937
+
2453
2938
  const mainCol = React.createElement('div', { className: 'edrv-main-col', style: { flex: 1, minWidth: 0, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' } },
2454
2939
  pathBar,
2455
2940
  tabRow,
2456
2941
  sideHintEl,
2457
2942
  diskBanner(),
2943
+ svnResultBar(),
2458
2944
  editorArea,
2459
2945
  statusBar)
2460
2946
  // 编辑器根节点按 composer 顶部边界动态限高,底部对话区域继续由 DSH 原生渲染。
@@ -2480,16 +2966,89 @@ export function EditorView(props) {
2480
2966
  // 命令栏浮层(Ctrl+Shift+P / F1):portal 到 body,但需挂在插件自己的 React 树里;
2481
2967
  // 三种布局形态都渲染 EditorView,故这里挂载即可,多宿主由 store 单实例认领兜底。
2482
2968
  const paletteEl = React.createElement(CommandPalette, { key: 'edrv-palette', sessionId })
2969
+ // SVN 日志弹窗(P3):portal 到 body,与命令栏/片段选择器同一挂载方式
2970
+ const svnLogEl = svnLog
2971
+ ? React.createElement(SvnLogDialog, {
2972
+ key: 'edrv-svnlog',
2973
+ target: svnLog.target,
2974
+ entries: svnLogData,
2975
+ wcRev: svnLogWcRev,
2976
+ truncated: svnLogTruncated(scope, svnLog.target || '', svnLog.limit || SVN_LOG_PAGE, svnLogOptsOf(svnLog)),
2977
+ error: svnLogError,
2978
+ busy: svnLogBusy,
2979
+ stopOnCopy: svnLog.soc === true,
2980
+ includeMerged: svnLog.merged === true,
2981
+ rangeActive: Array.isArray(svnLog.range),
2982
+ onShowRange: (start, end) => {
2983
+ const next = { ...svnLog, range: [start, end], limit: SVN_LOG_PAGE }
2984
+ setSvnLog(next)
2985
+ setSvnLogBusy(true)
2986
+ // 换键取数(range 维度隔离缓存)
2987
+ ensureSvnLog(sessionId, scope, next.target || '', SVN_LOG_PAGE, false, svnLogOptsOf(next))
2988
+ },
2989
+ onRangeReset: () => {
2990
+ const next = { ...svnLog, range: null, limit: SVN_LOG_PAGE }
2991
+ setSvnLog(next)
2992
+ setSvnLogBusy(true)
2993
+ ensureSvnLog(sessionId, scope, next.target || '', SVN_LOG_PAGE, false, svnLogOptsOf(next))
2994
+ },
2995
+ onStopCopy: (on) => {
2996
+ const next = { ...svnLog, soc: on }
2997
+ setSvnLog(next)
2998
+ setSvnLogBusy(true)
2999
+ // 换键取数(soc 维度隔离缓存);已加载过默认窗口时无需 force
3000
+ ensureSvnLog(sessionId, scope, next.target || '', next.limit || SVN_LOG_PAGE, false, svnLogOptsOf(next))
3001
+ },
3002
+ onShowMerged: (on) => {
3003
+ const next = { ...svnLog, merged: on }
3004
+ setSvnLog(next)
3005
+ setSvnLogBusy(true)
3006
+ // 换键取数(mrg 维度隔离缓存,P1-5);与 soc 同口径
3007
+ ensureSvnLog(sessionId, scope, next.target || '', next.limit || SVN_LOG_PAGE, false, svnLogOptsOf(next))
3008
+ },
3009
+ onRefresh: () => {
3010
+ setSvnLogError('')
3011
+ setSvnLogBusy(true)
3012
+ refreshSvnLog(sessionId, scope, svnLog.target || '', svnLog.limit || SVN_LOG_PAGE, svnLogOptsOf(svnLog))
3013
+ },
3014
+ onLoadMore: () => {
3015
+ // P1-8:按已加载上限递增(防旧 limit 倒退),force 绕过缓存命中后原地替换同键数据
3016
+ const opts = svnLogOptsOf(svnLog)
3017
+ const loaded = svnLogLoadedLimit(scope, svnLog.target || '', opts) || SVN_LOG_PAGE
3018
+ setSvnLogBusy(true)
3019
+ setSvnLog({ ...svnLog, limit: loaded + SVN_LOG_PAGE })
3020
+ ensureSvnLog(sessionId, scope, svnLog.target || '', loaded + SVN_LOG_PAGE, true, opts)
3021
+ },
3022
+ onShowAll: () => {
3023
+ setSvnLogBusy(true)
3024
+ setSvnLog({ ...svnLog, limit: SVN_LOG_SHOW_ALL_LIMIT })
3025
+ ensureSvnLog(sessionId, scope, svnLog.target || '', SVN_LOG_SHOW_ALL_LIMIT, true, svnLogOptsOf(svnLog))
3026
+ },
3027
+ onOpenDiff: (relPath, revision) => { setSvnLog(null); runSvnDiffRev(relPath, revision) },
3028
+ onComparePair: (revA, revB) => { setSvnLog(null); runSvnDiffPair(svnLog.target || '', revA, revB) },
3029
+ onCompareWorking: (revision) => { setSvnLog(null); runSvnDiffWorking(svnLog.target || '', revision) },
3030
+ onOpenFile: (relPath) => { setSvnLog(null); openFile(relPath, false) },
3031
+ onClose: () => { setSvnLog(null); setSvnLogError('') },
3032
+ })
3033
+ : null
3034
+ // 诊断日志弹窗:portal 到 body,与命令栏/SVN 日志弹窗同一挂载方式
3035
+ const dlogEl = dlogOpen
3036
+ ? React.createElement(LogDialog, { key: 'edrv-dlog', sessionId, onClose: () => setDlogOpen(false) })
3037
+ : null
2483
3038
  const rootEl = layout === 'side'
2484
3039
  ? React.createElement('div', { ref: viewRootRef, 'data-edrv-view': '1', 'data-edrv-layout': 'side', className: 'edrv-view-side', style: Object.assign({}, baseStyle, { height: '100%' }) },
2485
3040
  editorRow,
2486
3041
  tabMenuEl,
2487
3042
  paletteEl,
3043
+ svnLogEl,
3044
+ dlogEl,
2488
3045
  snippetPickerEl)
2489
3046
  : React.createElement('div', { ref: viewRootRef, 'data-edrv-view': '1', style: Object.assign({}, baseStyle, { height: 'var(--edrv-editor-height, 100%)', maxHeight: 'var(--edrv-editor-height, 100%)' }) },
2490
3047
  editorRow,
2491
3048
  tabMenuEl,
2492
3049
  paletteEl,
3050
+ svnLogEl,
3051
+ dlogEl,
2493
3052
  snippetPickerEl)
2494
3053
  return rootEl
2495
3054
  }