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.
- package/README.md +32 -2
- package/lib/client.js +8854 -4023
- package/lib/client.js.map +1 -1
- package/lib/index.js +1842 -65
- package/lib/index.js.map +1 -1
- package/package.json +2 -2
- package/src/client/index.ts +3 -0
- package/src/client/monaco/lsp/providers.ts +28 -6
- package/src/client/navHighlight.ts +96 -0
- package/src/client/paths.ts +2 -0
- package/src/client/rpc.ts +32 -12
- package/src/client/sidebar/SidebarView.ts +6 -2
- package/src/client/sidebar/menuItems.ts +93 -2
- package/src/client/sidebar/panels/FileExplorer.ts +18 -1
- package/src/client/sidebar/panels/SvnPanel.ts +303 -0
- package/src/client/sidebar/panels/index.ts +44 -3
- package/src/client/sidebar/types.ts +20 -0
- package/src/client/styles/editor.css +177 -0
- package/src/client/svnActions.ts +153 -0
- package/src/client/svnLog.ts +356 -0
- package/src/client/svnStatus.ts +363 -0
- package/src/client/svnStore.ts +150 -0
- package/src/client/tabMenu.ts +51 -0
- package/src/client/ui/EditorView.ts +583 -24
- package/src/client/ui/LogDialog.ts +333 -0
- package/src/client/ui/McpSettings.ts +98 -0
- package/src/client/ui/ModalShell.ts +79 -0
- package/src/client/ui/SideBySideDiff.tsx +139 -0
- package/src/client/ui/SnippetsPicker.ts +70 -58
- package/src/client/ui/SvnDiffPanel.ts +50 -0
- package/src/client/ui/SvnLogDialog.ts +1141 -0
- package/src/client/ui/SvnLogStats.ts +243 -0
- package/src/client/ui/commandCatalog.ts +72 -0
- package/src/debugLog.ts +81 -12
- package/src/fileOpenSettings.ts +40 -3
- package/src/index.ts +5 -1
- package/src/rpc.ts +41 -4
- package/src/shared/rpc.ts +53 -1
- package/src/shared/svn.ts +377 -0
- package/src/shared/svnActions.ts +296 -0
- package/src/svn.ts +870 -0
- package/src/svnLog.ts +315 -0
- package/src/svnText.ts +175 -0
- 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 ||
|
|
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 =
|
|
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 ||
|
|
1788
|
+
if (!ed || !contentReady) return
|
|
1580
1789
|
// 导航历史恢复优先:viewState 快照恢复滚动/折叠,行列补定位
|
|
1581
1790
|
const nav = navPendingRef.current
|
|
1582
|
-
if (nav && nav.path
|
|
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
|
|
1804
|
+
if (!pf || !sameFile(pf.path, active)) return
|
|
1596
1805
|
if (pf.line != null) {
|
|
1597
|
-
//
|
|
1806
|
+
// 搜索命中 / LSP 跳转:直接定位到行/列(不依赖差异区域)并高亮目标区域
|
|
1598
1807
|
pendingFocusRef.current = null
|
|
1599
|
-
|
|
1600
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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 (
|
|
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
|
}
|