dsh-vscode-mode 0.1.60 → 0.1.62
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/lib/client.js +364 -91
- package/lib/client.js.map +1 -1
- package/package.json +1 -1
- package/src/client/index.ts +19 -1
- package/src/client/navHistory.ts +28 -1
- package/src/client/officialSidebar.ts +72 -1
- package/src/client/paths.ts +3 -3
- package/src/client/sidebar/panels/FileExplorer.ts +14 -11
- package/src/client/sidebar/panels/SearchPanel.ts +10 -7
- package/src/client/sidebar/types.ts +2 -0
- package/src/client/state/explorerCache.ts +9 -9
- package/src/client/state/explorerEntriesCache.ts +31 -31
- package/src/client/state/modelCache.ts +77 -0
- package/src/client/state/scopeStore.ts +96 -0
- package/src/client/state/viewStateCache.ts +9 -9
- package/src/client/styles/editor.css +1 -1
- package/src/client/ui/EditorView.ts +64 -28
- package/src/client/ui/OfficialSideTab.ts +21 -2
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — 编辑区状态作用域键(scope key)。
|
|
3
|
+
* 编辑区 UI 状态(页签/展开树/视图状态/侧边栏等)按「工作区」隔离:同一工作区下
|
|
4
|
+
* 切换对话恢复同一份状态,无体感差别;会话没有 cwd 时回退按会话隔离(旧行为)。
|
|
5
|
+
* 另提供旧「会话键」数据到「工作区键」的一次性迁移(只复制不删除,幂等)。
|
|
6
|
+
* 键前缀统一走 paths.ts PathConst(CACHE_KEY),本模块只产出拼接用的作用域串。
|
|
7
|
+
* 作者 ddj 2026-09-09
|
|
8
|
+
*/
|
|
9
|
+
import { CACHE_KEY } from '../paths.js'
|
|
10
|
+
|
|
11
|
+
// --region 归一化与作用域键
|
|
12
|
+
|
|
13
|
+
/** Windows 盘符绝对路径(如 D:/ 或 d:\),大小写不敏感。 */
|
|
14
|
+
const WIN_DRIVE_RE = /^[a-z]:\//i
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 归一化工作区路径:trim、反斜杠→斜杠、去尾部斜杠;
|
|
18
|
+
* Windows 盘符绝对路径整体小写(文件系统大小写不敏感),POSIX 路径保留大小写。
|
|
19
|
+
* @author ddj 2026年09月09号
|
|
20
|
+
* @param cwd 工作区目录(sessions 快照 byId[sessionId].cwd)
|
|
21
|
+
* @returns 归一化路径;空值返回 ''
|
|
22
|
+
*/
|
|
23
|
+
export function normalizeWsPath(cwd: string | undefined | null): string {
|
|
24
|
+
const s = String(cwd ?? '').trim().replace(/\\/g, '/').replace(/\/+$/, '')
|
|
25
|
+
if (!s) return ''
|
|
26
|
+
return WIN_DRIVE_RE.test(s) ? s.toLowerCase() : s
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 计算编辑区状态作用域键:有 cwd → 'ws:' + 归一化路径(同工作区共享);
|
|
31
|
+
* 无 cwd → 'sid:' + sessionId(回退按会话隔离,对齐旧行为)。
|
|
32
|
+
* @author ddj 2026年09月09号
|
|
33
|
+
* @param cwd 工作区目录(可为 null/undefined)
|
|
34
|
+
* @param sessionId 会话 id(cwd 缺失时的隔离键)
|
|
35
|
+
* @returns 作用域键字符串
|
|
36
|
+
*/
|
|
37
|
+
export function workspaceScopeOf(cwd: string | undefined | null, sessionId: string | undefined): string {
|
|
38
|
+
const ws = normalizeWsPath(cwd)
|
|
39
|
+
if (ws) return 'ws:' + ws
|
|
40
|
+
return 'sid:' + String(sessionId ?? '')
|
|
41
|
+
}
|
|
42
|
+
// --endregion
|
|
43
|
+
|
|
44
|
+
// --region 旧会话键迁移
|
|
45
|
+
|
|
46
|
+
/** 参与迁移的 localStorage 键前缀(编辑区全部按会话持久化的旧键)。 */
|
|
47
|
+
const MIGRATE_PREFIXES: readonly string[] = [
|
|
48
|
+
CACHE_KEY.editor,
|
|
49
|
+
CACHE_KEY.viewstate,
|
|
50
|
+
CACHE_KEY.sidebar + 'side.',
|
|
51
|
+
CACHE_KEY.sidebar,
|
|
52
|
+
CACHE_KEY.search,
|
|
53
|
+
CACHE_KEY.expanded,
|
|
54
|
+
CACHE_KEY.entries,
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
/** 已迁移过的 scope+sessionId 组合(按 storage 实例分组;每页生命周期只跑一次)。 */
|
|
58
|
+
const migratedPairs = new WeakMap<object, Set<string>>()
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 把旧「前缀+sessionId」键的数据一次性复制到「前缀+scope」键(目标已存在则跳过)。
|
|
62
|
+
* 只复制不删除旧键;幂等,可安全重复调用。升级后首个工作区键为空时兜底恢复旧数据。
|
|
63
|
+
* @author ddj 2026年09月09号
|
|
64
|
+
* @param scope 目标作用域键
|
|
65
|
+
* @param sessionId 旧数据所属会话 id
|
|
66
|
+
* @param storage localStorage 注入口(缺省 window.localStorage,测试可传内存桩)
|
|
67
|
+
*/
|
|
68
|
+
export function migrateScopedKeys(scope: string, sessionId: string | undefined, storage?: Storage): void {
|
|
69
|
+
if (!scope || !sessionId) return
|
|
70
|
+
let store: Storage | null = null
|
|
71
|
+
try {
|
|
72
|
+
store = storage ?? window.localStorage
|
|
73
|
+
} catch (error) {
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
if (!store) return
|
|
77
|
+
let pairs = migratedPairs.get(store)
|
|
78
|
+
if (!pairs) {
|
|
79
|
+
pairs = new Set()
|
|
80
|
+
migratedPairs.set(store, pairs)
|
|
81
|
+
}
|
|
82
|
+
const pair = scope + '|' + sessionId
|
|
83
|
+
if (pairs.has(pair)) return
|
|
84
|
+
pairs.add(pair)
|
|
85
|
+
const legacy = String(sessionId)
|
|
86
|
+
for (const prefix of MIGRATE_PREFIXES) {
|
|
87
|
+
const target = prefix + scope
|
|
88
|
+
try {
|
|
89
|
+
if (store.getItem(target) !== null) continue
|
|
90
|
+
const raw = store.getItem(prefix + legacy)
|
|
91
|
+
if (raw === null) continue
|
|
92
|
+
store.setItem(target, raw)
|
|
93
|
+
} catch (error) { /* 配额满/隐私模式忽略,不阻塞 */ }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// --endregion
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { CACHE_KEY } from '../paths.js'
|
|
9
9
|
|
|
10
|
-
/** localStorage
|
|
10
|
+
/** localStorage 键前缀(按工作区作用域隔离,无 cwd 会话回退会话键)。 */
|
|
11
11
|
const KEY_PREFIX = CACHE_KEY.viewstate
|
|
12
12
|
|
|
13
13
|
/** 视图状态条目容量上限。 */
|
|
@@ -95,27 +95,27 @@ export function upsertViewState(
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
/**
|
|
98
|
-
*
|
|
98
|
+
* 读取某作用域的视图状态缓存(localStorage 不可用 → 空映射)。
|
|
99
99
|
* @author ddj 2026年08月28号
|
|
100
|
-
* @param
|
|
100
|
+
* @param scope 作用域键(scopeStore.workspaceScopeOf 产物)
|
|
101
101
|
* @returns path → viewState 映射
|
|
102
102
|
*/
|
|
103
|
-
export function viewStatesLoad(
|
|
103
|
+
export function viewStatesLoad(scope: string): Record<string, unknown> {
|
|
104
104
|
try {
|
|
105
|
-
return parseViewStates(window.localStorage.getItem(KEY_PREFIX + String(
|
|
105
|
+
return parseViewStates(window.localStorage.getItem(KEY_PREFIX + String(scope))) ?? {}
|
|
106
106
|
} catch (error) {
|
|
107
107
|
return {}
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
/**
|
|
112
|
-
*
|
|
112
|
+
* 写入某作用域的视图状态缓存(损坏/配额满 → 忽略)。
|
|
113
113
|
* @author ddj 2026年08月28号
|
|
114
|
-
* @param
|
|
114
|
+
* @param scope 作用域键(scopeStore.workspaceScopeOf 产物)
|
|
115
115
|
* @param states path → viewState 映射
|
|
116
116
|
*/
|
|
117
|
-
export function viewStatesSave(
|
|
117
|
+
export function viewStatesSave(scope: string, states: Record<string, unknown>): void {
|
|
118
118
|
try {
|
|
119
|
-
window.localStorage.setItem(KEY_PREFIX + String(
|
|
119
|
+
window.localStorage.setItem(KEY_PREFIX + String(scope), serializeViewStates(states))
|
|
120
120
|
} catch (error) { /* 配额满/隐私模式忽略 */ }
|
|
121
121
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
[data-edrv-view] .edrv-tab { display: inline-flex; align-items: center; gap: 6px; height: 28px; padding: 0 8px 0 12px; border: none; border-radius: 6px 6px 0 0; background: transparent; color: var(--dsw-alias-label-secondary, #aaa); font-size: 12px; cursor: pointer; white-space: nowrap; flex-shrink: 0; }
|
|
8
8
|
[data-edrv-view] .edrv-tab:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,.06)); }
|
|
9
9
|
[data-edrv-view] .edrv-tab.edrv-tab-active { background: var(--dsw-alias-bg-base, #111); color: var(--dsw-alias-label-primary, #eee); box-shadow: inset 0 -2px 0 var(--dsw-alias-brand-primary, #4f8cff); }
|
|
10
|
-
[data-edrv-view] .edrv-tab .edrv-tab-
|
|
10
|
+
[data-edrv-view] .edrv-tab .edrv-tab-star { color: var(--dsw-alias-brand-primary, #4f8cff); font-size: 13px; font-weight: 700; line-height: 1; flex-shrink: 0; } /* 未保存修改星号 @author ddj 2026年09月09号 */
|
|
11
11
|
[data-edrv-view] .edrv-tab .edrv-tab-x { display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; border-radius: 4px; color: var(--dsw-alias-label-tertiary, #888); font-size: 11px; }
|
|
12
12
|
[data-edrv-view] .edrv-tab .edrv-tab-x:hover { background: rgba(255,255,255,.12); color: var(--dsw-alias-label-primary, #eee); }
|
|
13
13
|
[data-edrv-view] .edrv-tab-add { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; margin-left: 2px; border: none; border-radius: 6px; background: transparent; color: var(--dsw-alias-label-secondary, #aaa); font-size: 14px; cursor: pointer; flex-shrink: 0; }
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* dsh-vscode-mode client — EditorView:中央 VSCode 式文件编辑器(编排层)。
|
|
4
4
|
* 迁移自原 src/client/index.ts 的 EditorView,语义不改;差异自绘已抽到 monaco/diffRender。
|
|
5
5
|
* 职责:页签/QuickOpen/Monaco 编辑器/差异审查(DiffBox/Launcher/Badge 事件装配)/状态栏/自动保存。
|
|
6
|
+
* 状态作用域:页签/视图状态/侧边栏/展开树等 UI 状态按工作区作用域(cwd)隔离,
|
|
7
|
+
* 同一工作区切换对话恢复同一份状态;无 cwd 会话回退按会话隔离(scopeStore)。
|
|
6
8
|
* 作者 ddj 2026-08-20
|
|
7
9
|
*/
|
|
8
10
|
import React from 'react'
|
|
@@ -25,9 +27,11 @@ import { editorHeight } from '../editorLayout.js'
|
|
|
25
27
|
import { revealInExplorer as revealPathInExplorer } from '../fileReveal.js'
|
|
26
28
|
import { setSidePending, SIDEBAR_INSTALL_CMD } from '../sidebarBridge.js'
|
|
27
29
|
import { upsertViewState, viewStatesLoad, viewStatesSave } from '../state/viewStateCache.js'
|
|
30
|
+
import { migrateScopedKeys, workspaceScopeOf } from '../state/scopeStore.js'
|
|
31
|
+
import { modelsForScope, rememberModel } from '../state/modelCache.js'
|
|
28
32
|
import { bindingsOf, chordOf, matchEvent, useKeybindingsVersion } from '../keybindings.js'
|
|
29
33
|
import { getSidebarMinWidth } from '../sidebarMin.js'
|
|
30
|
-
import {
|
|
34
|
+
import { navHistoryFor } from '../navHistory.js'
|
|
31
35
|
import { statusOfAdd } from '../addToConversation.js'
|
|
32
36
|
import { CACHE_KEY } from '../paths.js'
|
|
33
37
|
import { bindLspEditor, runGoToDefinition, runFindReferences, hideReferencesOverlay } from '../monaco/lsp/providers.js'
|
|
@@ -49,6 +53,22 @@ export function EditorView(props) {
|
|
|
49
53
|
const addToConversation = props.addToConversation
|
|
50
54
|
const layout = props?.layout === 'side' ? 'side' : 'tab'
|
|
51
55
|
const sideHint = props?.sideHint
|
|
56
|
+
// 当前会话工作区 cwd(sessions 快照反应式读取;cwd 变化时作用域跟随刷新)
|
|
57
|
+
const sessionsList = props?.sessions?.list
|
|
58
|
+
const cwd = React.useSyncExternalStore(
|
|
59
|
+
React.useCallback((onStoreChange) => (
|
|
60
|
+
typeof sessionsList?.subscribe === 'function' ? sessionsList.subscribe(onStoreChange) : () => {}
|
|
61
|
+
), [sessionsList]),
|
|
62
|
+
() => sessionsList?.getSnapshot?.()?.byId?.[sessionId]?.cwd ?? null,
|
|
63
|
+
() => null,
|
|
64
|
+
)
|
|
65
|
+
// 状态作用域(scopeStore):同工作区共享一份编辑区状态,无 cwd 回退会话隔离;
|
|
66
|
+
// 迁移放渲染期(useMemo)——子面板恢复 effect 先于父 effect 执行,须先补齐工作区键
|
|
67
|
+
const scope = React.useMemo(() => {
|
|
68
|
+
const s = workspaceScopeOf(cwd, sessionId)
|
|
69
|
+
migrateScopedKeys(s, sessionId)
|
|
70
|
+
return s
|
|
71
|
+
}, [cwd, sessionId])
|
|
52
72
|
const [monaco, setMonaco] = React.useState(null)
|
|
53
73
|
const [monacoErr, setMonacoErr] = React.useState(null)
|
|
54
74
|
const [records, setRecords] = React.useState({})
|
|
@@ -93,17 +113,19 @@ export function EditorView(props) {
|
|
|
93
113
|
const editorRef = React.useRef(null)
|
|
94
114
|
const viewRootRef = React.useRef(null)
|
|
95
115
|
const monacoRef = React.useRef(null)
|
|
96
|
-
const modelsRef = React.useRef(
|
|
116
|
+
const modelsRef = React.useRef(null)
|
|
117
|
+
// model 跨挂载缓存(按工作区作用域):重挂载秒显内容,消除切换对话闪烁
|
|
118
|
+
if (!modelsRef.current) modelsRef.current = modelsForScope(scope)
|
|
97
119
|
// 视图状态缓存(path → Monaco viewState;重启后恢复光标/滚动/折叠位置)
|
|
98
120
|
const viewStatesRef = React.useRef({})
|
|
99
121
|
const saveTimerRef = React.useRef(null)
|
|
100
122
|
const loadSeqRef = React.useRef(0)
|
|
101
123
|
const programmaticRef = React.useRef(false)
|
|
102
|
-
const
|
|
124
|
+
const restoredScopeRef = React.useRef(null) // 已恢复状态的作用域(cwd 晚到 sid→ws 时允许重恢复)
|
|
103
125
|
const pendingFocusRef = React.useRef(null) // { path, region } 内容加载后跳转
|
|
104
126
|
// 导航历史(后退/前进):跨文件焦点位置;按会话隔离,会话切换重置
|
|
105
127
|
const navRef = React.useRef(null)
|
|
106
|
-
if (!navRef.current) navRef.current =
|
|
128
|
+
if (!navRef.current) navRef.current = navHistoryFor(scope)
|
|
107
129
|
const navPendingRef = React.useRef(null) // { path,line,column,viewState } 待恢复条目(内容就绪后消费)
|
|
108
130
|
const navCursorTimerRef = React.useRef(null) // 光标位置防抖记录计时器
|
|
109
131
|
const navBackRef = React.useRef(null) // 后退动作最新闭包(窗口级键盘监听读取)
|
|
@@ -120,6 +142,8 @@ export function EditorView(props) {
|
|
|
120
142
|
const batchBusyRef = React.useRef(false) // 批量 Keep All/Undo All 防重入
|
|
121
143
|
const menuHandlersRef = React.useRef(null) // 右键菜单动作的最新闭包(Monaco addAction 空依赖回调读取)
|
|
122
144
|
const doSaveRef = React.useRef(null) // 保存动作的最新闭包(窗口级保存监听读取)
|
|
145
|
+
const onEditRef = React.useRef(null) // 编辑置脏的最新闭包(Monaco 内容变化监听经 ref 调用,防首帧 active=null 陈旧闭包)
|
|
146
|
+
const saveViewStateRef = React.useRef(null) // 视图状态保存的最新闭包(卸载清理读取,避免过期 active)
|
|
123
147
|
const diffRendererRef = React.useRef(null)
|
|
124
148
|
const layoutRef = React.useRef(layout) // ensureEditor 空依赖闭包读取的稳定布局
|
|
125
149
|
layoutRef.current = layout
|
|
@@ -163,7 +187,7 @@ export function EditorView(props) {
|
|
|
163
187
|
}
|
|
164
188
|
|
|
165
189
|
/**
|
|
166
|
-
*
|
|
190
|
+
* 保存当前活动文件的视图状态(光标/滚动/折叠)到工作区作用域缓存。
|
|
167
191
|
* @author ddj 2026年08月28号
|
|
168
192
|
* @param path 要保存的文件路径(缺省 = 当前 active)
|
|
169
193
|
*/
|
|
@@ -175,9 +199,10 @@ export function EditorView(props) {
|
|
|
175
199
|
const state = ed.saveViewState()
|
|
176
200
|
if (!state) return
|
|
177
201
|
viewStatesRef.current = upsertViewState(viewStatesRef.current, target, state)
|
|
178
|
-
viewStatesSave(
|
|
202
|
+
viewStatesSave(scope, viewStatesRef.current)
|
|
179
203
|
} catch (e) { /* 视图状态保存失败忽略 */ }
|
|
180
204
|
}
|
|
205
|
+
saveViewStateRef.current = saveViewState
|
|
181
206
|
|
|
182
207
|
/**
|
|
183
208
|
* 恢复指定文件的视图状态(内容就绪、setModel 后调用;恢复后即消费删除)。
|
|
@@ -618,13 +643,15 @@ export function EditorView(props) {
|
|
|
618
643
|
}
|
|
619
644
|
}, [sessionId, layout])
|
|
620
645
|
|
|
621
|
-
// localStorage v2
|
|
646
|
+
// localStorage v2 恢复页签(按工作区作用域;cwd 晚到 sid→ws 切换时重恢复一次)
|
|
622
647
|
React.useEffect(() => {
|
|
623
|
-
if (
|
|
624
|
-
|
|
625
|
-
|
|
648
|
+
if (!sessionId || restoredScopeRef.current === scope) return
|
|
649
|
+
restoredScopeRef.current = scope
|
|
650
|
+
// model 缓存跟随作用域(切工作区释放旧作用域模型;同工作区跨挂载复用)
|
|
651
|
+
modelsRef.current = modelsForScope(scope)
|
|
652
|
+
viewStatesRef.current = viewStatesLoad(scope)
|
|
626
653
|
try {
|
|
627
|
-
const raw = localStorage.getItem(CACHE_KEY.editor + String(
|
|
654
|
+
const raw = localStorage.getItem(CACHE_KEY.editor + String(scope))
|
|
628
655
|
if (raw) {
|
|
629
656
|
const saved = JSON.parse(raw)
|
|
630
657
|
if (Array.isArray(saved.tabs) && saved.tabs.length) {
|
|
@@ -633,25 +660,25 @@ export function EditorView(props) {
|
|
|
633
660
|
}
|
|
634
661
|
}
|
|
635
662
|
} catch (e) { /* 损坏忽略 */ }
|
|
636
|
-
}, [sessionId])
|
|
663
|
+
}, [sessionId, scope])
|
|
637
664
|
|
|
638
|
-
//
|
|
665
|
+
// 导航历史按工作区作用域隔离:同工作区切换对话保留历史,跨工作区换实例
|
|
639
666
|
React.useEffect(() => {
|
|
640
|
-
navRef.current =
|
|
667
|
+
navRef.current = navHistoryFor(scope)
|
|
641
668
|
navPendingRef.current = null
|
|
642
669
|
if (navCursorTimerRef.current) { clearTimeout(navCursorTimerRef.current); navCursorTimerRef.current = null }
|
|
643
670
|
setNavTick((v) => v + 1)
|
|
644
|
-
}, [
|
|
671
|
+
}, [scope])
|
|
645
672
|
|
|
646
673
|
React.useEffect(() => {
|
|
647
674
|
if (!sessionId) return
|
|
648
|
-
try { localStorage.setItem(CACHE_KEY.editor + String(
|
|
675
|
+
try { localStorage.setItem(CACHE_KEY.editor + String(scope), JSON.stringify({ tabs: tabs.map((t) => t.path), active })) }
|
|
649
676
|
catch (e) { /* 忽略 */ }
|
|
650
|
-
}, [tabs, active, sessionId])
|
|
677
|
+
}, [tabs, active, sessionId, scope])
|
|
651
678
|
|
|
652
679
|
// 侧边栏状态:恢复(显隐/宽度/激活面板);侧栏形态独立键(默认收起,不共享页签形态偏好)
|
|
653
680
|
// 宽度下限来自通用设置 sidebarMinWidth(默认 300),恢复值按其重夹
|
|
654
|
-
const sidebarKey = CACHE_KEY.sidebar + (layout === 'side' ? 'side.' : '') + String(
|
|
681
|
+
const sidebarKey = CACHE_KEY.sidebar + (layout === 'side' ? 'side.' : '') + String(scope)
|
|
655
682
|
React.useEffect(() => {
|
|
656
683
|
if (!sessionId) return
|
|
657
684
|
try {
|
|
@@ -682,7 +709,6 @@ export function EditorView(props) {
|
|
|
682
709
|
try { localStorage.setItem(sidebarKey, JSON.stringify({ on: sidebarOn, width: sidebarW, panel: activePanel })) }
|
|
683
710
|
catch (e) { /* 忽略 */ }
|
|
684
711
|
}, [sidebarOn, sidebarW, activePanel, sessionId, sidebarKey])
|
|
685
|
-
|
|
686
712
|
// 切换侧边栏(capture 抢占,避免与 DSH 全局冲突;键位随快捷键配置)
|
|
687
713
|
React.useEffect(() => {
|
|
688
714
|
const onKey = (e) => {
|
|
@@ -836,8 +862,10 @@ export function EditorView(props) {
|
|
|
836
862
|
const cache = modelsRef.current
|
|
837
863
|
let model = cache.get(path)
|
|
838
864
|
if (!model) {
|
|
839
|
-
|
|
840
|
-
|
|
865
|
+
// 注册表守卫:同 URI 已存在的 model 直接复用(HMR 换 bundle 后防重复创建抛错)
|
|
866
|
+
const uri = window.monaco.Uri.parse('edrv:///' + encodeURI(path))
|
|
867
|
+
model = window.monaco.editor.getModel(uri) || window.monaco.editor.createModel(text ?? '', langOf(path), uri)
|
|
868
|
+
rememberModel(cache, path, model)
|
|
841
869
|
} else if (text !== undefined && model.getValue() !== text) {
|
|
842
870
|
programmaticRef.current = true
|
|
843
871
|
model.setValue(text)
|
|
@@ -886,6 +914,8 @@ export function EditorView(props) {
|
|
|
886
914
|
if (saveTimerRef.current) saveTimerRef.current()
|
|
887
915
|
saveTimerRef.current = schedule(() => doSave(true), 700)
|
|
888
916
|
}
|
|
917
|
+
// @author ddj 2026年09月09号 空依赖监听只持有首帧 onEdit(active 恒 null→提前返回,星号与自动保存从未生效):每次渲染同步最新闭包
|
|
918
|
+
onEditRef.current = onEdit
|
|
889
919
|
|
|
890
920
|
// model 同步(当前内容)
|
|
891
921
|
React.useEffect(() => {
|
|
@@ -930,12 +960,14 @@ export function EditorView(props) {
|
|
|
930
960
|
|
|
931
961
|
React.useEffect(() => () => {
|
|
932
962
|
flushSave()
|
|
933
|
-
|
|
963
|
+
// 卸载即切作用域/重挂载:先把待记录光标落进历史,再存当前文件视图状态
|
|
964
|
+
//(经 ref 取最新闭包,修正旧实现捕获挂载时 active 的问题)
|
|
965
|
+
flushNavCursor()
|
|
966
|
+
saveViewStateRef.current?.()
|
|
934
967
|
diffRendererRef.current?.dispose?.()
|
|
935
968
|
hideReferencesOverlay()
|
|
936
969
|
if (editorRef.current) { editorRef.current.dispose(); editorRef.current = null }
|
|
937
|
-
|
|
938
|
-
modelsRef.current.clear()
|
|
970
|
+
// model 不在此销毁:跨挂载缓存按作用域存活(modelCache 切作用域时统一释放)
|
|
939
971
|
for (const ctl of pdfCtlRef.current.values()) ctl.destroy()
|
|
940
972
|
pdfCtlRef.current.clear()
|
|
941
973
|
const root = editorRef.current && editorRef.current.getDomNode ? editorRef.current.getDomNode() : null
|
|
@@ -978,7 +1010,7 @@ export function EditorView(props) {
|
|
|
978
1010
|
// 保存改由窗口级快捷键监听执行(键位可配置;见上方 edrv.save 监听)
|
|
979
1011
|
ed.onDidChangeModelContent(() => {
|
|
980
1012
|
if (!ed.getModel() || programmaticRef.current) return
|
|
981
|
-
|
|
1013
|
+
onEditRef.current?.()
|
|
982
1014
|
})
|
|
983
1015
|
ed.onDidChangeCursorPosition((e) => {
|
|
984
1016
|
setCursor('Ln ' + e.position.lineNumber + ', Col ' + e.position.column)
|
|
@@ -1451,7 +1483,9 @@ export function EditorView(props) {
|
|
|
1451
1483
|
},
|
|
1452
1484
|
},
|
|
1453
1485
|
React.createElement('span', { style: { overflow: 'hidden', textOverflow: 'ellipsis', maxWidth: 180 } }, t.path.split(/[\\/]/).pop() || t.path),
|
|
1454
|
-
|
|
1486
|
+
// 未保存修改页签显示 * 星号(保存成功消失、失败持续);替换原圆点脏标记
|
|
1487
|
+
// @author ddj 2026年09月09号
|
|
1488
|
+
(dirtyMap[t.path] ? React.createElement('span', { className: 'edrv-tab-star', title: '未保存修改' }, '*') : null),
|
|
1455
1489
|
React.createElement('span', { className: 'edrv-tab-x', onClick: (e) => { e.stopPropagation(); closeTab(t.path) } }, '×'))),
|
|
1456
1490
|
(openInput
|
|
1457
1491
|
? React.createElement('input', { className: 'edrv-path-input', autoFocus: true, placeholder: '输入工作区相对/绝对路径,回车打开', value: pathDraft, onChange: (e) => setPathDraft(e.target.value), onKeyDown: (e) => { if (e.key === 'Enter') openPath(); if (e.key === 'Escape') setOpenInput(false) } })
|
|
@@ -1683,8 +1717,10 @@ export function EditorView(props) {
|
|
|
1683
1717
|
const sidebarPanels = props.sidebarPanels
|
|
1684
1718
|
const sidebarCtx = {
|
|
1685
1719
|
sessionId,
|
|
1686
|
-
//
|
|
1687
|
-
cwd:
|
|
1720
|
+
// 当前会话工作区(反应式 cwd;规则面板项目 Tab 自动匹配)
|
|
1721
|
+
cwd: cwd ?? null,
|
|
1722
|
+
// 状态作用域键(工作区优先,无 cwd 回退会话;面板持久化统一走它)
|
|
1723
|
+
scope,
|
|
1688
1724
|
openFile: (p) => openFile(p, false),
|
|
1689
1725
|
openFileAt,
|
|
1690
1726
|
activePath: active,
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import React from 'react'
|
|
12
12
|
import { EditorView } from './EditorView.js'
|
|
13
13
|
import { setSideEditorMounted } from '../sidebarBridge.js'
|
|
14
|
-
import { parseOfficialFileAddress, resolveNavOpen } from '../officialSidebar.js'
|
|
14
|
+
import { editorMountEpoch, markEditorMounted, parseOfficialFileAddress, resolveNavOpen } from '../officialSidebar.js'
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* 官方侧边栏 Tab 正文组件(keyed slot sidebar.right.pane.tab 装配)。
|
|
@@ -32,10 +32,29 @@ export function OfficialSideTab(props) {
|
|
|
32
32
|
}
|
|
33
33
|
const revision = typeof navigation?.revision === 'number' ? navigation.revision : -1
|
|
34
34
|
const revisionRef = React.useRef(-1)
|
|
35
|
+
// 最新 sessionId 镜像(卸载清理判读「切会话 vs 本会话关闭」用)
|
|
36
|
+
const sessionIdRef = React.useRef(sessionId)
|
|
37
|
+
sessionIdRef.current = sessionId
|
|
35
38
|
|
|
39
|
+
// 编辑 Tab 激活标记:挂载即激活;卸载时延迟判定「切会话(保持,供新会话自动恢复)
|
|
40
|
+
// vs 本会话关闭/切走其他 Tab(清除,尊重用户选择)」——通知早于会话状态落定,必须延后读;
|
|
41
|
+
// 纪元守卫:判定期间有新正文挂载(HMR/自动恢复)则放弃清除
|
|
36
42
|
React.useEffect(() => {
|
|
37
43
|
setSideEditorMounted(true)
|
|
38
|
-
|
|
44
|
+
markEditorMounted()
|
|
45
|
+
return () => {
|
|
46
|
+
setSideEditorMounted(false)
|
|
47
|
+
const epoch = editorMountEpoch()
|
|
48
|
+
const self = sessionIdRef.current
|
|
49
|
+
const schedule = props?.schedule
|
|
50
|
+
const decide = () => {
|
|
51
|
+
if (editorMountEpoch() !== epoch) return
|
|
52
|
+
const current = props?.sessions?.list?.getSnapshot?.()?.current
|
|
53
|
+
markEditorActive(current === self)
|
|
54
|
+
}
|
|
55
|
+
if (typeof schedule === 'function') schedule(decide, 0)
|
|
56
|
+
else decide()
|
|
57
|
+
}
|
|
39
58
|
}, [])
|
|
40
59
|
|
|
41
60
|
React.useEffect(() => {
|