dsh-vscode-mode 0.14.13 → 0.15.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -3
- package/lib/client.js +2967 -1329
- package/lib/client.js.map +1 -1
- package/lib/index.js +217 -5
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/paths.ts +2 -0
- package/src/client/state/documentStore.ts +148 -0
- package/src/client/state/editorLayout.ts +179 -0
- package/src/client/state/modelCache.ts +16 -7
- package/src/client/styles/editor.css +33 -3
- package/src/client/svnNotice.ts +1 -1
- package/src/client/tabDrag.ts +137 -0
- package/src/client/tabMenu.ts +15 -3
- package/src/client/ui/EditorPane.ts +130 -0
- package/src/client/ui/EditorView.ts +358 -97
- package/src/client/ui/FloatingEditor.ts +87 -0
- package/src/client/ui/SvnUpdateDialog.ts +143 -29
- package/src/client/ui/svnUpdateModel.ts +45 -1
- package/src/shared/rpc.ts +5 -1
- package/src/shared/svn.ts +10 -0
- package/src/svn.ts +70 -5
- package/src/svnResolve.ts +33 -0
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
* 作者 ddj 2026-08-20
|
|
9
9
|
*/
|
|
10
10
|
import React from 'react'
|
|
11
|
+
import { createPortal } from 'react-dom'
|
|
12
|
+
import { EditorPane, EditorSplit, PassiveEditor } from './EditorPane.js'
|
|
13
|
+
import { FloatingEditor } from './FloatingEditor.js'
|
|
14
|
+
import { createLayout, parseLayout, syncTabs, ownerOf, focusGroup, moveTab, returnGroup, GROUP_LIMIT, MAIN_GROUP } from '../state/editorLayout.js'
|
|
15
|
+
import { createTabDrag, hitTarget } from '../tabDrag.js'
|
|
16
|
+
import { createDocStore } from '../state/documentStore.js'
|
|
11
17
|
import { useOpenReceipt } from '../useOpenReceipt.js'
|
|
12
18
|
import { acceptsLoad, receiptErrorFor, receiptPath, receiptReadyFor, receiptTabPath } from '../receiptState.js'
|
|
13
19
|
import { bindPanelCommand } from '../panelCommands.js'
|
|
@@ -59,7 +65,7 @@ import { createViewKeeper } from '../state/editorViewState.js'
|
|
|
59
65
|
import { mdPreviewLoad, mdPreviewSave } from '../state/mdPreviewCache.js'
|
|
60
66
|
import { onUiStateReady, reportUiStateEvent, uiStateSettled } from '../state/uiStatePersist.js'
|
|
61
67
|
import { migrateScopedKeys, workspaceScopeOf } from '../state/scopeStore.js'
|
|
62
|
-
import { modelsForScope, rememberModel } from '../state/modelCache.js'
|
|
68
|
+
import { modelsForScope, rememberModel, retainModels } from '../state/modelCache.js'
|
|
63
69
|
import { navFlashRangeOf } from '../navHighlight.js'
|
|
64
70
|
import { chordOf, useKeybindingsVersion } from '../keybindings.js'
|
|
65
71
|
import { getSidebarMinWidth } from '../sidebarMin.js'
|
|
@@ -253,7 +259,33 @@ export function EditorView(props) {
|
|
|
253
259
|
const planHostRef = React.useRef(null)
|
|
254
260
|
React.useEffect(() => { setPlanTabs([]); setPlanActive(null) }, [sessionId, cwd])
|
|
255
261
|
/** @private @author ddj 2026年10月08号 @param path Disk tab selection. Leave read-only plan mode before any disk navigation. */
|
|
256
|
-
function setActive(path) {
|
|
262
|
+
function setActive(path) {
|
|
263
|
+
setPlanActive(null)
|
|
264
|
+
setDiskActive(path)
|
|
265
|
+
setWorkbench((current) => syncTabs(current, tabsRef.current, path))
|
|
266
|
+
}
|
|
267
|
+
const [workbench, setWorkbench] = React.useState(() => createLayout())
|
|
268
|
+
const [layoutScope, setLayoutScope] = React.useState(null)
|
|
269
|
+
const benchRef = React.useRef(workbench)
|
|
270
|
+
benchRef.current = workbench
|
|
271
|
+
const [dragPreview, setDragPreview] = React.useState(null)
|
|
272
|
+
const dragRef = React.useRef(null)
|
|
273
|
+
const benchOps = React.useRef(null)
|
|
274
|
+
const fullHost = React.useMemo(() => document.createElement('div'), [])
|
|
275
|
+
fullHost.className = 'edrv-full-view'
|
|
276
|
+
const pdfNodes = React.useRef(new Map())
|
|
277
|
+
const paneEditors = React.useRef(new Map())
|
|
278
|
+
const docRef = React.useRef(null)
|
|
279
|
+
const docStores = React.useRef(new Map())
|
|
280
|
+
const docOps = React.useRef(null)
|
|
281
|
+
React.useEffect(() => {
|
|
282
|
+
setWorkbench((current) => syncTabs(current, tabs, active))
|
|
283
|
+
}, [tabs, active])
|
|
284
|
+
React.useEffect(() => {
|
|
285
|
+
dragRef.current?.cancel()
|
|
286
|
+
return () => dragRef.current?.cancel()
|
|
287
|
+
}, [sessionId, scope])
|
|
288
|
+
|
|
257
289
|
const [dirtyMap, setDirtyMap] = React.useState({})
|
|
258
290
|
const [content, setContent] = React.useState(null)
|
|
259
291
|
const [contentPath, setContentPath] = React.useState(null)
|
|
@@ -368,6 +400,7 @@ export function EditorView(props) {
|
|
|
368
400
|
tabsRef.current = tabs
|
|
369
401
|
const tabPathsRef = React.useRef([]) // 页签路径镜像(外部改动轮询读取)
|
|
370
402
|
tabPathsRef.current = tabs.map((t) => t.path)
|
|
403
|
+
retainModels(modelsRef.current, [...tabPathsRef.current, ...Object.keys(dirtyMap).filter((path) => dirtyMap[path])])
|
|
371
404
|
const dirtyRef = React.useRef({}) // 脏标记最新值(菜单禁用判定与关闭时落盘读)
|
|
372
405
|
dirtyRef.current = dirtyMap
|
|
373
406
|
const tabsHostRef = React.useRef(null) // 页签栏容器(切换后把当前页签滚入可见区)
|
|
@@ -571,7 +604,7 @@ export function EditorView(props) {
|
|
|
571
604
|
/** @private @author ddj 2026年10月12号 @returns Unsafe editor state without saving or discarding file content. */
|
|
572
605
|
function checkReload() {
|
|
573
606
|
if (Object.values(dirtyRef.current).some(Boolean)) return '存在未保存文件'
|
|
574
|
-
if (saveTimerRef.current.isPending() || savingRef.current.size) return '文件正在等待保存或保存中'
|
|
607
|
+
if (saveTimerRef.current.isPending() || savingRef.current.size || docRef.current?.unsafe()) return '文件正在等待保存或保存中'
|
|
575
608
|
for (const ctl of pdfCtlRef.current.values()) {
|
|
576
609
|
if (ctl.isDirty?.()) return '存在未保存 PDF'
|
|
577
610
|
}
|
|
@@ -690,6 +723,8 @@ export function EditorView(props) {
|
|
|
690
723
|
if (path === activeRef.current) { saveViewState(path); recordNav() }
|
|
691
724
|
const pdfCtl = pdfCtlRef.current.get(path)
|
|
692
725
|
if (pdfCtl) { pdfCtl.destroy(); pdfCtlRef.current.delete(path) }
|
|
726
|
+
pdfNodes.current.delete(path)
|
|
727
|
+
docRef.current?.release(path)
|
|
693
728
|
pdfB64CacheRef.current.delete(path)
|
|
694
729
|
// 关闭即失去跟踪:清磁盘版本基线/已读台账/同步标记(重开时按最新内容重建)
|
|
695
730
|
clearBaseline(scope, path)
|
|
@@ -764,7 +799,12 @@ export function EditorView(props) {
|
|
|
764
799
|
for (const key of Object.keys(prev)) next[remapPathOf(key, from, to)] = prev[key]
|
|
765
800
|
return next
|
|
766
801
|
})
|
|
767
|
-
|
|
802
|
+
setWorkbench((current) => ({ ...current, groups: current.groups.map((group) => ({ ...group,
|
|
803
|
+
tabs: group.tabs.map((tab) => ({ ...tab, path: remapPathOf(tab.path, from, to) })),
|
|
804
|
+
active: group.active ? remapPathOf(group.active, from, to) : null,
|
|
805
|
+
})) }))
|
|
806
|
+
setPlanActive(null)
|
|
807
|
+
setDiskActive((prev) => (prev ? remapPathOf(prev, from, to) : prev))
|
|
768
808
|
}
|
|
769
809
|
|
|
770
810
|
// 文件树右键的重命名/删除 → 页签同步(动作在 sidebar/menuItems.ts,经窗口事件桥接;
|
|
@@ -797,9 +837,10 @@ export function EditorView(props) {
|
|
|
797
837
|
* @param step 步进(+1 下一个 / -1 上一个)
|
|
798
838
|
*/
|
|
799
839
|
const cycleTab = (step) => {
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
const
|
|
840
|
+
const groupTabs = benchRef.current.groups.find((group) => group.id === benchRef.current.focused)?.tabs ?? tabs
|
|
841
|
+
if (groupTabs.length < 2) return
|
|
842
|
+
const idx = groupTabs.findIndex((t) => t.path === active)
|
|
843
|
+
const next = groupTabs[(idx + step + groupTabs.length) % groupTabs.length]
|
|
803
844
|
if (!next || next.path === active) return
|
|
804
845
|
flushSave()
|
|
805
846
|
saveViewState(active)
|
|
@@ -1083,9 +1124,16 @@ export function EditorView(props) {
|
|
|
1083
1124
|
* @param version 已知磁盘版本(外部改动轮询触发时带入)
|
|
1084
1125
|
*/
|
|
1085
1126
|
const loadPdf = (path, sid, seq, force, version) => {
|
|
1127
|
+
if (force && pdfCtlRef.current.get(path)?.isDirty()) { setStatus('PDF 含未保存批注,未覆盖'); return }
|
|
1086
1128
|
setPdfBytes(null)
|
|
1087
1129
|
setLoadStage({ progress: monaco ? 72 : 12, message: '读取 PDF…' })
|
|
1088
1130
|
const cache = pdfB64CacheRef.current
|
|
1131
|
+
if (force && pdfCtlRef.current.has(path)) {
|
|
1132
|
+
const controller = pdfCtlRef.current.get(path)
|
|
1133
|
+
controller.destroy()
|
|
1134
|
+
pdfCtlRef.current.delete(path)
|
|
1135
|
+
pdfNodes.current.delete(path)
|
|
1136
|
+
}
|
|
1089
1137
|
const hit = !force ? cache.get(path) : undefined
|
|
1090
1138
|
if (typeof hit === 'string' && hit) {
|
|
1091
1139
|
// 刷新插入序(Map 按插入序 FIFO 驱逐)后秒切
|
|
@@ -1164,6 +1212,22 @@ export function EditorView(props) {
|
|
|
1164
1212
|
if (!change || !change.path) return
|
|
1165
1213
|
// 文件树同步:目录缓存按该路径失效并强制重列(外部新增/删除/改名都能看到)
|
|
1166
1214
|
const eventId = emitFileChanged(change.path, { scope, kind: change.kind === 'deleted' ? 'structure' : 'text' })
|
|
1215
|
+
if (change.kind === 'modified' && change.path !== activeRef.current) {
|
|
1216
|
+
const currentScope = scope, map = modelsRef.current
|
|
1217
|
+
void rpc('edrv.read', { sessionId, path: change.path }).then((result) => {
|
|
1218
|
+
if (!result?.ok || map !== modelsRef.current || dirtyRef.current[change.path]) return
|
|
1219
|
+
const model = map.get(change.path)
|
|
1220
|
+
if (model && !model.isDisposed?.()) {
|
|
1221
|
+
programmaticRef.current = true
|
|
1222
|
+
try { model.setValue(result.content) } finally { programmaticRef.current = false }
|
|
1223
|
+
}
|
|
1224
|
+
revRef.current[change.path] = result.version
|
|
1225
|
+
docRef.current?.baseline(change.path, result.version, false)
|
|
1226
|
+
recordBaseline(currentScope, change.path, result.version)
|
|
1227
|
+
clearSync(currentScope, change.path)
|
|
1228
|
+
}).catch(() => {})
|
|
1229
|
+
return
|
|
1230
|
+
}
|
|
1167
1231
|
if (change.kind === 'modified') {
|
|
1168
1232
|
// 干净缓冲:强制从磁盘重读(跳过 model 复用),差异标记随之重算
|
|
1169
1233
|
loadContent(change.path, sessionId, true)
|
|
@@ -1361,11 +1425,42 @@ export function EditorView(props) {
|
|
|
1361
1425
|
React.useEffect(() => {
|
|
1362
1426
|
if (!sessionId || restoredScopeRef.current === scope) return
|
|
1363
1427
|
restoredScopeRef.current = scope
|
|
1428
|
+
setLayoutScope(scope)
|
|
1364
1429
|
// model 缓存跟随作用域(切工作区释放旧作用域模型;同工作区跨挂载复用)
|
|
1365
1430
|
modelsRef.current = modelsForScope(scope)
|
|
1366
1431
|
// Markdown 预览态同样按作用域恢复(重启/换端口后由界面状态镜像回填,见 lateHydrateRef 分支)
|
|
1367
1432
|
mdPreviewRef.current = mdPreviewLoad(String(scope))
|
|
1433
|
+
docRef.current = docStores.current.get(scope) ?? null
|
|
1434
|
+
for (const controller of pdfCtlRef.current.values()) controller.destroy()
|
|
1435
|
+
pdfCtlRef.current.clear()
|
|
1436
|
+
pdfNodes.current.clear()
|
|
1437
|
+
pdfB64CacheRef.current.clear()
|
|
1438
|
+
revRef.current = {}
|
|
1439
|
+
savingRef.current.clear()
|
|
1440
|
+
const restoredDirty = {}
|
|
1441
|
+
for (const path of modelsRef.current.keys()) {
|
|
1442
|
+
const item = docRef.current?.state(path)
|
|
1443
|
+
if (item?.dirty) restoredDirty[path] = true
|
|
1444
|
+
if (item?.rev) revRef.current[path] = item.rev
|
|
1445
|
+
if (item?.saving) savingRef.current.add(path)
|
|
1446
|
+
}
|
|
1447
|
+
setDirtyMap(restoredDirty)
|
|
1448
|
+
setTabs([])
|
|
1449
|
+
setDiskActive(null)
|
|
1450
|
+
setContent(null)
|
|
1451
|
+
setContentPath(null)
|
|
1368
1452
|
try {
|
|
1453
|
+
const savedLayout = parseLayout(localStorage.getItem(CACHE_KEY.editorLayout + String(scope)), cwd)
|
|
1454
|
+
if (savedLayout) {
|
|
1455
|
+
const restored = savedLayout.groups.flatMap((group) => group.tabs)
|
|
1456
|
+
const selected = savedLayout.groups.find((group) => group.id === savedLayout.focused)?.active ?? null
|
|
1457
|
+
setWorkbench(savedLayout)
|
|
1458
|
+
setTabs(restored)
|
|
1459
|
+
setPlanActive(null)
|
|
1460
|
+
setDiskActive(selected)
|
|
1461
|
+
return
|
|
1462
|
+
}
|
|
1463
|
+
setWorkbench(createLayout())
|
|
1369
1464
|
// v3 优先;缺失时回读 v2(旧版纯路径数组)——normalizeTabs 兼容两种形状
|
|
1370
1465
|
const raw = localStorage.getItem(CACHE_KEY.editor + String(scope))
|
|
1371
1466
|
?? localStorage.getItem(CACHE_KEY.editorLegacy + String(scope))
|
|
@@ -1429,6 +1524,11 @@ export function EditorView(props) {
|
|
|
1429
1524
|
setNavTick((v) => v + 1)
|
|
1430
1525
|
}, [scope, hydrateTick])
|
|
1431
1526
|
|
|
1527
|
+
React.useEffect(() => {
|
|
1528
|
+
if (!sessionId || layoutScope !== scope || !uiStateSettled()) return
|
|
1529
|
+
try { localStorage.setItem(CACHE_KEY.editorLayout + String(scope), JSON.stringify(workbench)) } catch { /* Storage can be unavailable. */ }
|
|
1530
|
+
}, [workbench, layoutScope, sessionId, scope, hydrateTick])
|
|
1531
|
+
|
|
1432
1532
|
// 导航双栈落 localStorage(同原文去重写;镜像层负责跨重启):刷新/重启后前进后退与当前位置仍在
|
|
1433
1533
|
React.useEffect(() => {
|
|
1434
1534
|
if (!sessionId || !uiStateSettled()) return
|
|
@@ -1479,7 +1579,9 @@ export function EditorView(props) {
|
|
|
1479
1579
|
// 复用 closeTabs:它已实现 flushSave + persistDirty(脏页签先静默落盘)+ releaseTab + commitClose,
|
|
1480
1580
|
// 故「超限淘汰脏页签」不会丢用户编辑。淘汰后 tabs 变化 → 本 effect 复算 evictPlan 返回空 → 自然收敛。
|
|
1481
1581
|
React.useEffect(() => {
|
|
1582
|
+
const protectedPaths = new Set(benchRef.current.groups.map((group) => group.active).filter(Boolean))
|
|
1482
1583
|
const plan = evictPlan(tabsRef.current, tabLimit, { active: activeRef.current, used: tabUseRef.current.used })
|
|
1584
|
+
.filter((path) => !protectedPaths.has(path) && !dirtyRef.current[path] && !savingRef.current.has(path))
|
|
1483
1585
|
if (!plan.length) return
|
|
1484
1586
|
closeTabs(applyClose(tabsRef.current, new Set(plan), activeRef.current), '已关闭最久未使用的页签')
|
|
1485
1587
|
}, [tabs, tabLimit, active])
|
|
@@ -1917,7 +2019,7 @@ export function EditorView(props) {
|
|
|
1917
2019
|
const uri = window.monaco.Uri.parse('edrv:///' + encodeURI(String(path ?? '').replace(/^\/+/, '')))
|
|
1918
2020
|
model = window.monaco.editor.getModel(uri) || window.monaco.editor.createModel(text ?? '', langOf(path), uri)
|
|
1919
2021
|
rememberModel(cache, path, model)
|
|
1920
|
-
} else if (text !== undefined && model.getValue() !== text) {
|
|
2022
|
+
} else if (text !== undefined && !dirtyRef.current[path] && model.getValue() !== text) {
|
|
1921
2023
|
programmaticRef.current = true
|
|
1922
2024
|
model.setValue(text)
|
|
1923
2025
|
programmaticRef.current = false
|
|
@@ -1939,6 +2041,7 @@ export function EditorView(props) {
|
|
|
1939
2041
|
const target = path ?? active
|
|
1940
2042
|
if (target && readSync(scope, target)) return false
|
|
1941
2043
|
saveTimerRef.current?.flush()
|
|
2044
|
+
if (docRef.current) void docRef.current.flush(target)
|
|
1942
2045
|
return true
|
|
1943
2046
|
}
|
|
1944
2047
|
|
|
@@ -1959,6 +2062,59 @@ export function EditorView(props) {
|
|
|
1959
2062
|
setDiskFlag((prev) => (prev && prev.path === path ? null : prev))
|
|
1960
2063
|
}
|
|
1961
2064
|
|
|
2065
|
+
/** @private @author ddj 2026年10月14号 @returns One document save owner using latest workspace callbacks. */
|
|
2066
|
+
function documents() {
|
|
2067
|
+
if (!docRef.current) {
|
|
2068
|
+
const documentScope = scope
|
|
2069
|
+
const store = createDocStore({
|
|
2070
|
+
save: (request) => rpc('edrv.save', { sessionId, ...request }),
|
|
2071
|
+
changed: (path, item) => { if (docRef.current === store) docOps.current.changed(path, item) },
|
|
2072
|
+
saved: (path, text, model, result) => {
|
|
2073
|
+
recordBaseline(documentScope, path, result.rev)
|
|
2074
|
+
if (docRef.current === store) docOps.current.saved(path, text, model, result)
|
|
2075
|
+
},
|
|
2076
|
+
failed: (path, result) => { if (docRef.current === store) docOps.current.failed(path, result) },
|
|
2077
|
+
blocked: (path) => !!readSync(documentScope, path),
|
|
2078
|
+
})
|
|
2079
|
+
docRef.current = store
|
|
2080
|
+
docStores.current.set(scope, store)
|
|
2081
|
+
}
|
|
2082
|
+
return docRef.current
|
|
2083
|
+
}
|
|
2084
|
+
docOps.current = {
|
|
2085
|
+
save: (request) => rpc('edrv.save', { sessionId, ...request }),
|
|
2086
|
+
changed: (path, item) => {
|
|
2087
|
+
if (item.saving) savingRef.current.add(path)
|
|
2088
|
+
else savingRef.current.delete(path)
|
|
2089
|
+
setDirtyMap((current) => current[path] === item.dirty ? current : { ...current, [path]: item.dirty })
|
|
2090
|
+
},
|
|
2091
|
+
saved: (path, text, model, result) => {
|
|
2092
|
+
if (result.rev) revRef.current[path] = result.rev
|
|
2093
|
+
if (/\.code-snippets$/i.test(path)) window.dispatchEvent(new CustomEvent('edrv:snippets-changed'))
|
|
2094
|
+
recordBaseline(scope, path, result.rev)
|
|
2095
|
+
markReadVersion(scope, path, result.rev, true)
|
|
2096
|
+
clearSync(scope, path)
|
|
2097
|
+
acceptSaved(path, text, model)
|
|
2098
|
+
refreshRecords()
|
|
2099
|
+
emitRefresh({ scope, paths: [path], kind: 'text' })
|
|
2100
|
+
},
|
|
2101
|
+
failed: (path, result) => {
|
|
2102
|
+
if (result.conflict) markDiskFlag({ path, kind: 'conflict', at: Date.now() })
|
|
2103
|
+
setStatus(result.conflict ? '保存被拒:文件已被外部修改' : '保存失败')
|
|
2104
|
+
if (result.error) setError(result.error)
|
|
2105
|
+
},
|
|
2106
|
+
}
|
|
2107
|
+
/** @private @author ddj 2026年10月14号 @param path Canonical path. @param manual Explicit save. @returns Guarded save, never another pane's content. */
|
|
2108
|
+
function saveDocument(path, manual) {
|
|
2109
|
+
const model = modelsRef.current.get(path)
|
|
2110
|
+
if (!model) return Promise.resolve({ ok: false })
|
|
2111
|
+
const store = documents()
|
|
2112
|
+
store.bind(path, model, revRef.current[path])
|
|
2113
|
+
store.state(path).dirty = !!dirtyRef.current[path]
|
|
2114
|
+
store.baseline(path, revRef.current[path], !!readSync(scope, path))
|
|
2115
|
+
return store.save(path, manual)
|
|
2116
|
+
}
|
|
2117
|
+
|
|
1962
2118
|
const doSave = (silent) => {
|
|
1963
2119
|
if (!active) return
|
|
1964
2120
|
// 外部改动待处理:自动保存(silent)直接放弃,避免陈旧缓冲覆盖磁盘;
|
|
@@ -1972,40 +2128,9 @@ export function EditorView(props) {
|
|
|
1972
2128
|
void ctl.savePdf()
|
|
1973
2129
|
return
|
|
1974
2130
|
}
|
|
1975
|
-
const ed = editorRef.current
|
|
1976
|
-
if (!ed) return
|
|
1977
2131
|
if (isImageActive) { setStatus('图片只读预览'); return }
|
|
1978
|
-
const text = ed.getValue()
|
|
1979
|
-
const savedModel = ed.getModel?.()
|
|
1980
|
-
const path = active
|
|
1981
2132
|
if (!silent) setStatus('保存中…')
|
|
1982
|
-
|
|
1983
|
-
savingRef.current.add(path)
|
|
1984
|
-
// 版本守卫令牌:上次读取/写入时的磁盘版本;与磁盘当前版本不符时 host 拒绝写入
|
|
1985
|
-
const rev = revRef.current[path]
|
|
1986
|
-
rpc('edrv.save', { sessionId, path, content: text, rev: typeof rev === 'string' && rev ? rev : undefined }).then((res) => {
|
|
1987
|
-
if (res && res.ok) {
|
|
1988
|
-
if (res.rev) revRef.current[path] = res.rev
|
|
1989
|
-
recordBaseline(scope, path, res.rev)
|
|
1990
|
-
markReadVersion(scope, path, res.rev, true) // 缓冲即磁盘内容:同版本无需再读盘
|
|
1991
|
-
clearSync(scope, path)
|
|
1992
|
-
acceptSaved(path, text, savedModel)
|
|
1993
|
-
refreshRecords()
|
|
1994
|
-
emitRefresh({ scope, paths: [path], kind: 'text' })
|
|
1995
|
-
// 片段配置文件保存后失效补全缓存(下次补全即读到新片段)
|
|
1996
|
-
if (/\.code-snippets$/i.test(path)) window.dispatchEvent(new CustomEvent('edrv:snippets-changed'))
|
|
1997
|
-
return
|
|
1998
|
-
}
|
|
1999
|
-
if (res && res.conflict) {
|
|
2000
|
-
// 磁盘已被外部改过:保留缓冲内容与脏标记,交给用户显式选择
|
|
2001
|
-
markDiskFlag({ path, kind: 'conflict', at: Date.now() })
|
|
2002
|
-
setStatus('保存被拒:文件已被外部修改')
|
|
2003
|
-
return
|
|
2004
|
-
}
|
|
2005
|
-
setStatus('保存失败')
|
|
2006
|
-
setError(res?.error ? String(res.error) : '保存失败')
|
|
2007
|
-
}).catch((e) => { setStatus('保存失败'); setError('保存异常:' + String(e)) })
|
|
2008
|
-
.finally(() => { savingRef.current.delete(path) })
|
|
2133
|
+
void saveDocument(active, !silent)
|
|
2009
2134
|
}
|
|
2010
2135
|
doSaveRef.current = doSave
|
|
2011
2136
|
|
|
@@ -2029,38 +2154,26 @@ export function EditorView(props) {
|
|
|
2029
2154
|
if (savingRef.current.has(path)) continue
|
|
2030
2155
|
const pdfCtl = pdfCtlRef.current.get(path)
|
|
2031
2156
|
if (pdfCtl) { if (pdfCtl.isDirty()) void pdfCtl.savePdf(); continue }
|
|
2032
|
-
|
|
2033
|
-
if (!model) { setStatus('未保存的修改无法落盘:' + baseNameOf(path)); continue }
|
|
2034
|
-
const content = model.getValue()
|
|
2035
|
-
savingRef.current.add(path)
|
|
2036
|
-
const rev = revRef.current[path]
|
|
2037
|
-
rpc('edrv.save', { sessionId, path, content, rev: typeof rev === 'string' && rev ? rev : undefined })
|
|
2038
|
-
.then((res) => {
|
|
2039
|
-
if (res && res.ok) {
|
|
2040
|
-
if (res.rev) revRef.current[path] = res.rev
|
|
2041
|
-
recordBaseline(scope, path, res.rev)
|
|
2042
|
-
markReadVersion(scope, path, res.rev, true) // 缓冲即磁盘内容:同版本无需再读盘
|
|
2043
|
-
setDirtyMap((d) => Object.assign({}, d, { [path]: false }))
|
|
2044
|
-
}
|
|
2045
|
-
})
|
|
2046
|
-
.catch((e) => dbg(sessionId, '关闭前落盘失败:' + path + ' · ' + String(e)))
|
|
2047
|
-
.finally(() => { savingRef.current.delete(path) })
|
|
2157
|
+
void saveDocument(path, false)
|
|
2048
2158
|
}
|
|
2049
2159
|
}
|
|
2050
2160
|
|
|
2051
2161
|
const onEdit = () => {
|
|
2052
2162
|
const ed = editorRef.current
|
|
2053
|
-
|
|
2054
|
-
|
|
2163
|
+
const model = ed?.getModel()
|
|
2164
|
+
const path = [...modelsRef.current].find(([, item]) => item === model)?.[0]
|
|
2165
|
+
if (!ed || !path) return
|
|
2166
|
+
setDirtyMap((d) => Object.assign({}, d, { [path]: true }))
|
|
2055
2167
|
// 编辑即「使用」:需求 2 的淘汰口径是「最近没有改动」,故有改动时刷新 LRU 序号,
|
|
2056
2168
|
// 使长时间只在某文件里编辑的页签不会被其它文件的打开动作挤出。
|
|
2057
|
-
touchTab(
|
|
2169
|
+
touchTab(path)
|
|
2058
2170
|
setStatus('编辑中…')
|
|
2059
2171
|
// 外部改动尚未处理(冲突/文件被删):抑制自动保存,避免用陈旧缓冲反复覆盖磁盘;
|
|
2060
2172
|
// 手工 Ctrl+S 不受抑制(doSave 会走版本守卫并给冲突提示)。
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2173
|
+
const store = documents()
|
|
2174
|
+
store.bind(path, model, revRef.current[path])
|
|
2175
|
+
store.edit(path)
|
|
2176
|
+
if (readSync(scope, path)) setStatus('外部已修改(未保存的编辑保留)')
|
|
2064
2177
|
}
|
|
2065
2178
|
// @author ddj 2026年09月09号 空依赖监听只持有首帧 onEdit(active 恒 null→提前返回,星号与自动保存从未生效):每次渲染同步最新闭包
|
|
2066
2179
|
onEditRef.current = onEdit
|
|
@@ -2082,7 +2195,18 @@ export function EditorView(props) {
|
|
|
2082
2195
|
}, [monaco, scope, active, content, contentReady, mdPreviewing, svnDiff])
|
|
2083
2196
|
|
|
2084
2197
|
// PDF 面板外壳 ref 回调(div 仅在 PDF 分支渲染,refs 先于 effect 就绪)
|
|
2085
|
-
|
|
2198
|
+
/** @private @author ddj 2026年10月14号 @param path PDF document. @returns Stable panel container across groups and focus switches. */
|
|
2199
|
+
function pdfNode(path) {
|
|
2200
|
+
let node = pdfNodes.current.get(path)
|
|
2201
|
+
if (!node) { node = document.createElement('div'); node.className = 'edrv-pdf-host'; pdfNodes.current.set(path, node) }
|
|
2202
|
+
return node
|
|
2203
|
+
}
|
|
2204
|
+
const ensurePdfHost = (node) => {
|
|
2205
|
+
if (!node || !active) { pdfHostRef.current = null; return }
|
|
2206
|
+
const stable = pdfNode(active)
|
|
2207
|
+
node.appendChild(stable)
|
|
2208
|
+
pdfHostRef.current = stable
|
|
2209
|
+
}
|
|
2086
2210
|
|
|
2087
2211
|
// PDF 面板装配:控制器挂进外壳 div,切换 tab/卸载即销毁(重开重挂,base64 缓存兜底秒切)
|
|
2088
2212
|
React.useEffect(() => {
|
|
@@ -2099,6 +2223,7 @@ export function EditorView(props) {
|
|
|
2099
2223
|
}
|
|
2100
2224
|
/** @author ddj 2026年09月28号 End startup error attribution before later save operations. */
|
|
2101
2225
|
const mounted = () => { mounting = false }
|
|
2226
|
+
if (pdfCtlRef.current.has(path)) { mounting = false; return }
|
|
2102
2227
|
const ctl = createPdfPanel({
|
|
2103
2228
|
sessionId,
|
|
2104
2229
|
path,
|
|
@@ -2111,8 +2236,7 @@ export function EditorView(props) {
|
|
|
2111
2236
|
void ctl.mount(host, pdfBytes).then(mounted, pdfError)
|
|
2112
2237
|
return () => {
|
|
2113
2238
|
mounting = false
|
|
2114
|
-
|
|
2115
|
-
if (pdfCtlRef.current.get(path) === ctl) pdfCtlRef.current.delete(path)
|
|
2239
|
+
// The document owns this controller. Reparenting/focus changes must retain unsaved annotations.
|
|
2116
2240
|
}
|
|
2117
2241
|
}, [isPdfActive, pdfBytes, active, receiptLoad?.sequence])
|
|
2118
2242
|
|
|
@@ -2127,6 +2251,7 @@ export function EditorView(props) {
|
|
|
2127
2251
|
|
|
2128
2252
|
React.useEffect(() => () => {
|
|
2129
2253
|
flushSave()
|
|
2254
|
+
for (const store of docStores.current.values()) void store.flush().finally(() => store.dispose())
|
|
2130
2255
|
// 卸载即切作用域/重挂载:先把待记录光标落进历史,再存当前文件视图状态
|
|
2131
2256
|
//(经 ref 取最新闭包,修正旧实现捕获挂载时 active 的问题)
|
|
2132
2257
|
flushNavCursor()
|
|
@@ -3287,13 +3412,35 @@ export function EditorView(props) {
|
|
|
3287
3412
|
* @param path 右键目标页签路径
|
|
3288
3413
|
* @returns id → 动作
|
|
3289
3414
|
*/
|
|
3415
|
+
/** @private @author ddj 2026年10月14号 @param path Menu target. @param kind Close operation. Close only tabs in the owning group, using visible order. */
|
|
3416
|
+
function closeInGroup(path, kind) {
|
|
3417
|
+
const group = ownerOf(benchRef.current, path)
|
|
3418
|
+
if (!group) return
|
|
3419
|
+
const actions = {
|
|
3420
|
+
others: () => closeOthers(group.tabs, path, group.active),
|
|
3421
|
+
right: () => closeRight(group.tabs, path, group.active),
|
|
3422
|
+
saved: () => closeSaved(group.tabs, dirtyRef.current, group.active),
|
|
3423
|
+
all: () => closeAll(group.tabs, group.active),
|
|
3424
|
+
}
|
|
3425
|
+
const result = actions[kind]?.()
|
|
3426
|
+
if (!result) return
|
|
3427
|
+
const kept = new Set(result.tabs.map((tab) => tab.path))
|
|
3428
|
+
const closing = new Set(group.tabs.filter((tab) => !kept.has(tab.path)).map((tab) => tab.path))
|
|
3429
|
+
closeTabs(applyClose(tabsRef.current, closing, activeRef.current), '已关闭编辑组页签')
|
|
3430
|
+
}
|
|
3290
3431
|
const tabMenuActions = (path) => ({
|
|
3291
3432
|
'add-to-conversation': () => addRefToChat(path),
|
|
3433
|
+
'split-left': () => splitTab(path, 'left'),
|
|
3434
|
+
'split-right': () => splitTab(path, 'right'),
|
|
3435
|
+
'split-top': () => splitTab(path, 'top'),
|
|
3436
|
+
'split-bottom': () => splitTab(path, 'bottom'),
|
|
3437
|
+
'move-new-window': () => dropTab(path, { kind: 'float', rect: { left: 80, top: 80, width: 600, height: 400 } }),
|
|
3438
|
+
'return-main': () => dropTab(path, { kind: 'group', group: MAIN_GROUP }),
|
|
3292
3439
|
close: () => closeTab(path),
|
|
3293
|
-
'close-others': () =>
|
|
3294
|
-
'close-right': () =>
|
|
3295
|
-
'close-saved': () =>
|
|
3296
|
-
'close-all': () =>
|
|
3440
|
+
'close-others': () => closeInGroup(path, 'others'),
|
|
3441
|
+
'close-right': () => closeInGroup(path, 'right'),
|
|
3442
|
+
'close-saved': () => closeInGroup(path, 'saved'),
|
|
3443
|
+
'close-all': () => closeInGroup(path, 'all'),
|
|
3297
3444
|
'copy-path': () => copyText(absoluteOf(path, cwd), '已复制路径'),
|
|
3298
3445
|
'copy-relative-path': () => copyText(relativeOf(path, cwd), '已复制相对路径'),
|
|
3299
3446
|
'reveal-in-os': () => openInExplorer(path),
|
|
@@ -3313,12 +3460,15 @@ export function EditorView(props) {
|
|
|
3313
3460
|
const changeMap = svnChangeMapOf(scope)
|
|
3314
3461
|
return buildTabMenu({
|
|
3315
3462
|
path: tabMenu.path,
|
|
3316
|
-
tabs: tabsRef.current,
|
|
3317
|
-
active: activeRef.current,
|
|
3463
|
+
tabs: ownerOf(workbench, tabMenu.path)?.tabs ?? tabsRef.current,
|
|
3464
|
+
active: ownerOf(workbench, tabMenu.path)?.active ?? activeRef.current,
|
|
3318
3465
|
dirty: dirtyRef.current,
|
|
3319
3466
|
cwd,
|
|
3320
3467
|
hasSession: Boolean(sessionId),
|
|
3321
3468
|
canAddToConversation: Boolean(addToConversation),
|
|
3469
|
+
canSplit: workbench.groups.length < GROUP_LIMIT && !workbench.floats.some((item) => item.group === ownerOf(workbench, tabMenu.path)?.id),
|
|
3470
|
+
canFloat: workbench.groups.length < GROUP_LIMIT,
|
|
3471
|
+
canReturn: ownerOf(workbench, tabMenu.path)?.id !== MAIN_GROUP,
|
|
3322
3472
|
svnReady: Boolean(svnStatus?.managed && svnStatus?.svnCli),
|
|
3323
3473
|
tortoiseReady: Boolean(svnStatus?.managed && svnStatus?.tortoise),
|
|
3324
3474
|
// 自研替换时间线:自研就绪的能力对应 Tortoise 项不出现
|
|
@@ -3547,26 +3697,136 @@ export function EditorView(props) {
|
|
|
3547
3697
|
}).catch((e) => { setStatus('打开失败'); setError('打开异常:' + String(e)) })
|
|
3548
3698
|
}
|
|
3549
3699
|
|
|
3700
|
+
// --region Workbench drag and independent views
|
|
3701
|
+
/** @private @author ddj 2026年10月14号 @param group Group id. @param path Optional selection. Route global actions only to this group. */
|
|
3702
|
+
function selectGroup(group, path) {
|
|
3703
|
+
saveViewState()
|
|
3704
|
+
recordNav()
|
|
3705
|
+
const next = focusGroup(benchRef.current, group, path)
|
|
3706
|
+
setWorkbench(next)
|
|
3707
|
+
setPlanActive(null)
|
|
3708
|
+
setDiskActive(next.groups.find((item) => item.id === group)?.active ?? null)
|
|
3709
|
+
}
|
|
3710
|
+
/** @private @author ddj 2026年10月14号 @param next Atomic layout. Accept ownership without close/save side effects. */
|
|
3711
|
+
function acceptLayout(next) {
|
|
3712
|
+
saveViewState()
|
|
3713
|
+
setWorkbench(next)
|
|
3714
|
+
setPlanActive(null)
|
|
3715
|
+
setDiskActive(next.groups.find((group) => group.id === next.focused)?.active ?? null)
|
|
3716
|
+
}
|
|
3717
|
+
/** @private @author ddj 2026年10月14号 @param path File. @param edge Split direction. Move the file into a new group. */
|
|
3718
|
+
function splitTab(path, edge) {
|
|
3719
|
+
const group = ownerOf(benchRef.current, path)
|
|
3720
|
+
if (group) dropTab(path, { kind: 'split', group: group.id, edge })
|
|
3721
|
+
}
|
|
3722
|
+
/** @private @author ddj 2026年10月14号 @param path File. @param target Valid destination. Move only after acceptance. */
|
|
3723
|
+
function dropTab(path, target) {
|
|
3724
|
+
if (!ownerOf(benchRef.current, path)) return
|
|
3725
|
+
if (target.kind === 'reference') { addRefToChat(path); return }
|
|
3726
|
+
acceptLayout(moveTab(benchRef.current, path, target))
|
|
3727
|
+
}
|
|
3728
|
+
/** @private @author ddj 2026年10月14号 @param point Viewport pointer. @returns Current same-workbench geometry; ambiguous composers rejected. */
|
|
3729
|
+
function dragHit(point) {
|
|
3730
|
+
const root = viewRootRef.current
|
|
3731
|
+
const groups = [...(root?.querySelectorAll('[data-edrv-group]') ?? []), ...document.querySelectorAll('[data-edrv-float][data-edrv-owner="' + String(scope).replace(/["\\]/g, '') + '"] [data-edrv-group]')]
|
|
3732
|
+
const regions = groups.map((node) => ({
|
|
3733
|
+
group: node.getAttribute('data-edrv-group'), rect: node.getBoundingClientRect(), floating: !!node.closest('[data-edrv-float]'),
|
|
3734
|
+
tabRect: node.querySelector('[data-edrv-group-tabs]')?.getBoundingClientRect(),
|
|
3735
|
+
tabs: [...node.querySelectorAll('[data-edrv-tab-path]')].map((tab) => ({ path: tab.getAttribute('data-edrv-tab-path'), rect: tab.getBoundingClientRect() })),
|
|
3736
|
+
})).filter((region) => region.rect.width && region.rect.height).reverse()
|
|
3737
|
+
const scroll = root?.closest('[data-conversation-scroll]')
|
|
3738
|
+
const seats = [...(scroll ?? document).querySelectorAll('[data-composer-seat]')].filter((node) => { const rect = node.getBoundingClientRect(); return rect.width > 0 && rect.height > 0 })
|
|
3739
|
+
const element = document.elementFromPoint(point.x, point.y)
|
|
3740
|
+
const composer = seats.length === 1 && seats[0].contains(element) ? seats[0].getBoundingClientRect() : null
|
|
3741
|
+
const hitGroup = element?.closest('[data-edrv-group]')?.getAttribute('data-edrv-group')
|
|
3742
|
+
const visibleRegions = regions.filter((region) => region.group === hitGroup)
|
|
3743
|
+
const blank = !!element && !element.closest('button,input,textarea,a,[contenteditable],.monaco-editor,[role="dialog"]')
|
|
3744
|
+
&& (element === document.body || element.matches('[data-conversation-scroll], [data-edrv-view], .edrv-editor-row, .edrv-workbench, .edrv-group-body'))
|
|
3745
|
+
return hitTarget({ point, viewport: { width: window.innerWidth, height: window.innerHeight }, regions: visibleRegions, composer, blank, allowCreate: benchRef.current.groups.length < GROUP_LIMIT })
|
|
3746
|
+
}
|
|
3747
|
+
benchOps.current = { hit: dragHit, drop: dropTab }
|
|
3748
|
+
React.useEffect(() => {
|
|
3749
|
+
const controller = createTabDrag({ hit: (point) => benchOps.current.hit(point), drop: (path, target) => benchOps.current.drop(path, target), preview: setDragPreview }, window)
|
|
3750
|
+
dragRef.current = controller
|
|
3751
|
+
return () => { controller.cancel(); if (dragRef.current === controller) dragRef.current = null }
|
|
3752
|
+
}, [])
|
|
3753
|
+
/** @private @author ddj 2026年10月14号 @param path Trusted file. @param event Tab press. */
|
|
3754
|
+
function dragTab(path, event) { dragRef.current?.start(path, event) }
|
|
3755
|
+
/** @private @author ddj 2026年10月14号 @returns Whether pointer release completed a drag. */
|
|
3756
|
+
function consumeDrag() { return dragRef.current?.consumeClick() ?? false }
|
|
3757
|
+
/** @private @author ddj 2026年10月14号 @param path File. @param event Context trigger. */
|
|
3758
|
+
function paneMenu(path, event) { setTabMenu({ path, x: event.clientX, y: event.clientY }) }
|
|
3759
|
+
/** @private @author ddj 2026年10月14号 @param group Auxiliary group. Return documents rather than closing them. */
|
|
3760
|
+
function returnPane(group) { acceptLayout(returnGroup(benchRef.current, group)) }
|
|
3761
|
+
/** @private @author ddj 2026年10月14号 @param group Float id. @param rect Viewport-safe bounds. */
|
|
3762
|
+
function floatBounds(group, rect) {
|
|
3763
|
+
setWorkbench((current) => ({ ...current, floats: current.floats.map((item) => item.group === group ? { ...rect, group } : item) }))
|
|
3764
|
+
}
|
|
3765
|
+
/** @private @author ddj 2026年10月14号 @param host Group body. Keep the full editor subtree stable while changing group/window ownership. */
|
|
3766
|
+
function mountFull(host) { if (host && fullHost.parentElement !== host) { host.appendChild(fullHost); editorRef.current?.layout() } }
|
|
3767
|
+
/** @private @author ddj 2026年10月14号 @param host Passive body. @param path Canonical file. @returns View-only cleanup, models remain workbench-owned. */
|
|
3768
|
+
function mountPassive(host, path) {
|
|
3769
|
+
let disposed = false, keeper = null, subscription = null, observer = null
|
|
3770
|
+
const focus = () => { const owner = ownerOf(benchRef.current, path); if (owner) selectGroup(owner.id, path) }
|
|
3771
|
+
host.addEventListener('pointerdown', focus)
|
|
3772
|
+
if (isPdfPath(path) && pdfNodes.current.has(path)) {
|
|
3773
|
+
host.appendChild(pdfNode(path))
|
|
3774
|
+
return () => host.removeEventListener('pointerdown', focus)
|
|
3775
|
+
}
|
|
3776
|
+
host.textContent = '加载中…'
|
|
3777
|
+
const binary = isImagePath(path) || isPdfPath(path)
|
|
3778
|
+
const cached = modelsRef.current.get(path)
|
|
3779
|
+
const request = binary ? readBinaryPreview(rpc, { sessionId, path }) : cached ? Promise.resolve({ ok: true, content: cached.getValue() }) : rpc('edrv.read', { sessionId, path })
|
|
3780
|
+
void request.then((result) => {
|
|
3781
|
+
if (disposed) return
|
|
3782
|
+
host.textContent = ''
|
|
3783
|
+
if (!result?.ok) { host.textContent = result?.error || '文件加载失败'; return }
|
|
3784
|
+
if (binary) {
|
|
3785
|
+
const image = document.createElement(isPdfPath(path) ? 'div' : 'img')
|
|
3786
|
+
if (isPdfPath(path)) image.textContent = 'PDF · 点击此编辑组继续查看和编辑'
|
|
3787
|
+
else { image.src = dataUrlOf(result.via === 'binary' ? bytesToBase64(result.bytes) : result.content, result.mime); image.alt = path; image.style.maxWidth = '100%' }
|
|
3788
|
+
host.appendChild(image)
|
|
3789
|
+
return
|
|
3790
|
+
}
|
|
3791
|
+
if (!window.monaco) { host.textContent = '编辑器正在加载'; return }
|
|
3792
|
+
const model = cached || getModel(path, result.content)
|
|
3793
|
+
if (result.version) { revRef.current[path] = result.version; recordBaseline(scope, path, result.version) }
|
|
3794
|
+
const editor = window.monaco.editor.create(host, { model, automaticLayout: true, minimap: { enabled: false }, scrollBeyondLastLine: false })
|
|
3795
|
+
keeper = createViewKeeper(editor)
|
|
3796
|
+
keeper.bind(scope, path, model)
|
|
3797
|
+
paneEditors.current.set(path, editor)
|
|
3798
|
+
const store = documents()
|
|
3799
|
+
store.bind(path, model, revRef.current[path])
|
|
3800
|
+
subscription = editor.onDidChangeModelContent(() => { if (!programmaticRef.current) store.edit(path) })
|
|
3801
|
+
observer = new ResizeObserver(() => editor.layout())
|
|
3802
|
+
observer.observe(host)
|
|
3803
|
+
}).catch((failure) => { if (!disposed) host.textContent = '文件加载失败:' + String(failure) })
|
|
3804
|
+
return () => { disposed = true; host.removeEventListener('pointerdown', focus); observer?.disconnect(); subscription?.dispose(); keeper?.dispose(); paneEditors.current.delete(path) }
|
|
3805
|
+
}
|
|
3806
|
+
const passiveOps = React.useRef(mountPassive)
|
|
3807
|
+
passiveOps.current = mountPassive
|
|
3808
|
+
const passiveMount = React.useCallback((host, path) => passiveOps.current(host, path), [])
|
|
3809
|
+
/** @private @author ddj 2026年10月14号 @param node Split branch. @param ratio Updated branch ratio. */
|
|
3810
|
+
function changeRatio(node, ratio) {
|
|
3811
|
+
const replace = (current) => current === node ? { ...current, ratio } : 'group' in current ? current : { ...current, first: replace(current.first), second: replace(current.second) }
|
|
3812
|
+
setWorkbench((current) => ({ ...current, tree: replace(current.tree) }))
|
|
3813
|
+
}
|
|
3814
|
+
/** @private @author ddj 2026年10月14号 @param id Leaf id. @returns Tabs and either focused full-feature editor or independent passive view. */
|
|
3815
|
+
function renderGroup(id) {
|
|
3816
|
+
const group = workbench.groups.find((item) => item.id === id)
|
|
3817
|
+
if (!group) return null
|
|
3818
|
+
const focused = id === workbench.focused
|
|
3819
|
+
return React.createElement(EditorPane, { key: id, group, focused, dirty: dirtyMap, onSelect: selectGroup, onClose: closeTab, onMenu: paneMenu, onDrag: dragTab, consumeClick: consumeDrag },
|
|
3820
|
+
focused ? React.createElement('div', { className: 'edrv-full-slot', ref: mountFull }) : React.createElement(PassiveEditor, { key: String(scope) + ':' + group.active, path: group.active, mount: passiveMount }))
|
|
3821
|
+
}
|
|
3822
|
+
/** @private @author ddj 2026年10月14号 @param node Layout subtree. @returns Recursive resizable panes. */
|
|
3823
|
+
function renderTree(node) {
|
|
3824
|
+
if ('group' in node) return renderGroup(node.group)
|
|
3825
|
+
return React.createElement(EditorSplit, { axis: node.axis, ratio: node.ratio, first: renderTree(node.first), second: renderTree(node.second), onRatio: (ratio) => changeRatio(node, ratio) })
|
|
3826
|
+
}
|
|
3827
|
+
// --endregion
|
|
3828
|
+
|
|
3550
3829
|
const tabsEl = React.createElement('div', { className: 'edrv-tabs', ref: tabsHostRef, style: { flex: '1 1 auto', minWidth: 0 } },
|
|
3551
|
-
tabs.map((t) => React.createElement('div', {
|
|
3552
|
-
key: t.path,
|
|
3553
|
-
className: 'edrv-tab' + (t.path === active ? ' edrv-tab-active' : '') + (t.pinned ? ' edrv-tab-pinned' : ''),
|
|
3554
|
-
title: t.pinned ? t.path + '(已固定)' : t.path,
|
|
3555
|
-
onClick: () => { if (t.path !== active) { flushSave(); saveViewState(active); recordNav(); setActive(t.path) } },
|
|
3556
|
-
onContextMenu: (e) => {
|
|
3557
|
-
e.preventDefault()
|
|
3558
|
-
e.stopPropagation()
|
|
3559
|
-
setTabMenu({ x: e.clientX, y: e.clientY, path: t.path })
|
|
3560
|
-
},
|
|
3561
|
-
},
|
|
3562
|
-
// 固定页签在文件名前显示 📌 标记,且不渲染 × (与参考图一致:固定页签不显示关闭按钮)
|
|
3563
|
-
// @author ddj 2026年09月11号
|
|
3564
|
-
(t.pinned ? React.createElement('span', { className: 'edrv-tab-pin', title: '已固定', 'aria-label': '已固定' }, '📌') : null),
|
|
3565
|
-
React.createElement('span', { style: { overflow: 'hidden', textOverflow: 'ellipsis', maxWidth: 180 } }, t.path.split(/[\\/]/).pop() || t.path),
|
|
3566
|
-
// 未保存修改页签显示 * 星号(保存成功消失、失败持续);替换原圆点脏标记
|
|
3567
|
-
// @author ddj 2026年09月09号
|
|
3568
|
-
(dirtyMap[t.path] ? React.createElement('span', { className: 'edrv-tab-star', title: '未保存修改' }, '*') : null),
|
|
3569
|
-
(t.pinned ? null : React.createElement('span', { className: 'edrv-tab-x', title: '关闭', onClick: (e) => { e.stopPropagation(); closeTab(t.path) } }, '×')))),
|
|
3570
3830
|
planTabs.map((plan) => React.createElement('div', {
|
|
3571
3831
|
key: plan.address, className: 'edrv-tab' + (plan.address === planActive ? ' edrv-tab-active' : ''),
|
|
3572
3832
|
title: plan.title + '(计划预览 · 只读)', onClick: () => openPlanDoc(plan),
|
|
@@ -4107,13 +4367,14 @@ export function EditorView(props) {
|
|
|
4107
4367
|
React.createElement('button', { className: 'edrv-pill edrv-pill-ghost', title: '复制安装命令', onClick: copyInstallCmd }, '复制命令'),
|
|
4108
4368
|
React.createElement('button', { className: 'edrv-side-hint-close', title: '关闭提示', 'aria-label': '关闭提示', onClick: dismissHint }, '×'))
|
|
4109
4369
|
: null
|
|
4110
|
-
const
|
|
4111
|
-
|
|
4112
|
-
tabRow,
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4370
|
+
const fullView = createPortal(React.createElement('div', { 'data-edrv-view': '1', style: { display: 'flex', flexDirection: 'column', flex: 1, minWidth: 0, minHeight: 0, overflow: 'hidden' } }, pathBar, sideHintEl, diskBanner(), editorArea, statusBar), fullHost, 'edrv-full-editor')
|
|
4371
|
+
const mainCol = React.createElement('div', { className: 'edrv-main-col edrv-workbench', style: { flex: 1, minWidth: 0, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' } },
|
|
4372
|
+
React.createElement('div', { className: 'edrv-workbench-tools' }, tabRow),
|
|
4373
|
+
renderTree(workbench.tree), fullView,
|
|
4374
|
+
workbench.floats.map((item, layer) => React.createElement(FloatingEditor, { key: item.group, owner: String(scope), group: item.group, rect: item, layer, focused: item.group === workbench.focused, onBounds: floatBounds, onFocus: selectGroup, onReturn: returnPane }, renderGroup(item.group))),
|
|
4375
|
+
dragPreview ? createPortal(React.createElement('div', { 'data-edrv-view': '1', style: { display: 'contents', pointerEvents: 'none' } },
|
|
4376
|
+
dragPreview.rect ? React.createElement('div', { className: 'edrv-drag-overlay', style: dragPreview.rect }, React.createElement('span', { className: 'edrv-drop-label' }, dragPreview.label)) : null,
|
|
4377
|
+
React.createElement('div', { className: 'edrv-drag-ghost', style: { left: dragPreview.point.x + 12, top: dragPreview.point.y + 12 } }, baseNameOf(dragPreview.path) + ' · ' + dragPreview.label)), document.body) : null)
|
|
4117
4378
|
// 编辑器根节点按 composer 顶部边界动态限高,底部对话区域继续由 DSH 原生渲染。
|
|
4118
4379
|
// 侧栏形态由面板容器给高(100%),不做 composer 几何同步。
|
|
4119
4380
|
const editorRow = React.createElement('div', { className: 'edrv-editor-row' },
|