dsh-vscode-mode 0.1.59 → 0.1.61
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 +21 -11
- package/lib/client.js +800 -107
- package/lib/client.js.map +1 -1
- package/package.json +2 -2
- package/src/client/fileOpeners.ts +41 -1
- package/src/client/index.ts +109 -23
- package/src/client/navHistory.ts +28 -1
- package/src/client/officialSidebar.ts +352 -0
- 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/ui/EditorView.ts +58 -27
- package/src/client/ui/OfficialSideTab.ts +92 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vscode-mode",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻
|
|
3
|
+
"version": "0.1.61",
|
|
4
|
+
"description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 DSH 0.1.5+ 官方右侧 Sidebar 与对话同屏)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚)+ 语言服务器(LSP)智能(跳转定义/引用/hover/大纲 + VSIX 扩展市场安装),状态持久化到工作区旁车",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
7
7
|
"dsh-plugin",
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-vscode-mode client — 文件打开器注册表与自动选择。
|
|
3
|
-
* @author ddj 2026年08月24号
|
|
3
|
+
* @author ddj 2026年08月24号 / 2026年09月09号
|
|
4
4
|
*/
|
|
5
5
|
import { SIDEBAR_PLUGIN } from './compat.js'
|
|
6
|
+
import { buildFileAddress } from './officialSidebar.js'
|
|
6
7
|
|
|
7
8
|
export const AUTO_OPEN_TOOL = 'auto'
|
|
8
9
|
export const SYSTEM_OPEN_TOOL = 'system'
|
|
9
10
|
export const VSCODE_OPEN_TOOL = 'dsh-vscode-mode'
|
|
10
11
|
export const SIDEBAR_OPEN_TOOL = SIDEBAR_PLUGIN
|
|
12
|
+
/** 官方右侧侧边栏打开器 id(「文件链接使用工具」下拉的官方选项;DSH 0.1.5+)。 */
|
|
13
|
+
export const OFFICIAL_OPEN_TOOL = 'dsh-official-sidebar'
|
|
11
14
|
|
|
12
15
|
export interface FileOpenContext {
|
|
13
16
|
sessionId?: string
|
|
@@ -171,3 +174,40 @@ export function baseName(path: string): string {
|
|
|
171
174
|
const normalized = path.replace(/\\/g, '/')
|
|
172
175
|
return normalized.slice(normalized.lastIndexOf('/') + 1) || path
|
|
173
176
|
}
|
|
177
|
+
|
|
178
|
+
/** 官方 sidebarRight.openResource 的最小结构面(结构性使用,不 import 官方类型)。 */
|
|
179
|
+
export interface OfficialOpenResourceService {
|
|
180
|
+
openResource?: (address: string, options?: { params?: unknown }) => void
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* 文件链接是否由本插件认领官方 file 地址:自动/VSCodeMode 档接管(链接进本插件编辑器),
|
|
185
|
+
* 官方侧边栏/system/better-sidebar 档不认领(官方查看器或旧版路由生效)。
|
|
186
|
+
* @author ddj 2026年09月09号
|
|
187
|
+
* @param selected 「文件链接使用工具」当前值
|
|
188
|
+
* @returns 是否认领
|
|
189
|
+
*/
|
|
190
|
+
export function shouldClaimFiles(selected: unknown): boolean {
|
|
191
|
+
return selected === AUTO_OPEN_TOOL || selected === VSCODE_OPEN_TOOL
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* 创建「官方侧边栏」文件打开器(DSH 0.1.5+,经 ctx.sidebarRight.openResource 打开官方查看器)。
|
|
196
|
+
* 仅官方服务探测命中时注册;下拉选项随注册动态出现(McpSettings 动态渲染)。
|
|
197
|
+
* @author ddj 2026年09月09号
|
|
198
|
+
* @param service 官方导航控制器
|
|
199
|
+
* @returns 文件打开器
|
|
200
|
+
*/
|
|
201
|
+
export function officialSidebarOpener(service: OfficialOpenResourceService): FileOpener {
|
|
202
|
+
return {
|
|
203
|
+
id: OFFICIAL_OPEN_TOOL,
|
|
204
|
+
label: '官方侧边栏',
|
|
205
|
+
description: '在 DSH 官方右侧侧边栏查看器中打开(0.1.5+)',
|
|
206
|
+
priority: 90,
|
|
207
|
+
isAvailable: () => typeof service.openResource === 'function',
|
|
208
|
+
open: (path, context) => {
|
|
209
|
+
if (typeof service.openResource !== 'function') throw new Error('官方侧边栏 openResource 能力不可用')
|
|
210
|
+
service.openResource(buildFileAddress(path, context.sessionId))
|
|
211
|
+
},
|
|
212
|
+
}
|
|
213
|
+
}
|
package/src/client/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-vscode-mode client — 浏览器半入口:slot 注册 + 装配。
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* 挂点:官方右侧 Sidebar「文件编辑」Tab(DSH 0.1.5+,对话+编辑同屏)> betterSidebar Tab
|
|
4
|
+
* (归档,仅旧版 DSH 回退)> conversation.view 中央页签 + conversation.input.dock 差异条 + header 差异角标。
|
|
5
5
|
* 与 Host 通信:同源 fetch('/edrv/rpc')(shared/rpc 契约)。
|
|
6
6
|
*
|
|
7
7
|
* ⚠️ 跨版本 slot 装配(2026-08-21):新版 DSH 的 slots 系统要求 slot 必须由父 entry
|
|
@@ -22,7 +22,7 @@ import { ConversationDiffDock } from './ui/ConversationDiffDock.js'
|
|
|
22
22
|
import { McpSettings } from './ui/McpSettings.js'
|
|
23
23
|
import { rpc } from './rpc.js'
|
|
24
24
|
import { loadMonaco } from './monaco/loader.js'
|
|
25
|
-
import { createFileOpenerRegistry, scanSidebar, type FileOpenContext } from './fileOpeners.js'
|
|
25
|
+
import { createFileOpenerRegistry, scanSidebar, officialSidebarOpener, shouldClaimFiles, type FileOpenContext } from './fileOpeners.js'
|
|
26
26
|
import type { FileOpenerRegistry } from './fileOpeners.js'
|
|
27
27
|
import { installOpenPathRouter, vscodeOpener, autoValue } from './openPathRouter.js'
|
|
28
28
|
import { patchRemoteOpen, probeRemoteOpen } from './remoteOpenRouter.js'
|
|
@@ -30,7 +30,9 @@ import { setupExtOpen } from './externalOpen.js'
|
|
|
30
30
|
import { SettingsContext } from './settingsContext.js'
|
|
31
31
|
import { SIDEBAR_PLUGIN, pickSettingsBinder, registerSlotSafely } from './compat.js'
|
|
32
32
|
import { detectSidebarService, installSideEditor, setEnsureSideEditor, SIDEBAR_INSTALL_CMD } from './sidebarBridge.js'
|
|
33
|
+
import { detectOfficial, installOfficial, registerOfficialFileClaim, OFFICIAL_TAB_KIND, OFFICIAL_TAB_TITLE, isEditorTabActive, restoreEditorTab } from './officialSidebar.js'
|
|
33
34
|
import { SideEditorTab } from './ui/SideEditorTab.js'
|
|
35
|
+
import { OfficialSideTab } from './ui/OfficialSideTab.js'
|
|
34
36
|
import { createAddToConversation } from './addToConversation.js'
|
|
35
37
|
import { createSidebarPanelRegistry } from './sidebar/registry.js'
|
|
36
38
|
import { createFilePanel } from './sidebar/panels/index.js'
|
|
@@ -96,6 +98,24 @@ export function apply(ctx: any): void {
|
|
|
96
98
|
setSession(list.getSnapshot()?.current)
|
|
97
99
|
}), 'vscode-mode: lsp session sync')
|
|
98
100
|
}
|
|
101
|
+
// 会话切换 → 编辑 Tab 跨会话自动恢复:上一会话编辑区激活时,新会话自动展开并激活
|
|
102
|
+
//(官方停靠面按会话隔离,新会话默认收起;先试 openTabIn 直写新会话面,未 adopt 走有界重试)。
|
|
103
|
+
// 注意:首次打开某会话时通知早于 current 字段落定,故通知只调度延迟任务,执行时再读最新快照
|
|
104
|
+
if (monacoList && typeof monacoList.subscribe === 'function') {
|
|
105
|
+
const list = monacoList as { getSnapshot: () => { current?: string }; subscribe: (listener: () => void) => () => void }
|
|
106
|
+
ctx.effect(() => {
|
|
107
|
+
let lastCurrent = list.getSnapshot?.()?.current
|
|
108
|
+
const check = () => {
|
|
109
|
+
const current = list.getSnapshot?.()?.current
|
|
110
|
+
if (!current || current === lastCurrent) return
|
|
111
|
+
lastCurrent = current
|
|
112
|
+
if (!isEditorTabActive() || officialService === undefined) return
|
|
113
|
+
try { officialService.service.openTabIn?.(current, OFFICIAL_TAB_KIND, {}) } catch { /* 未 adopt 等 seat 就绪走重试 */ }
|
|
114
|
+
restoreEditorTab(officialService.service, schedule)
|
|
115
|
+
}
|
|
116
|
+
return list.subscribe(() => schedule(check, 0))
|
|
117
|
+
}, 'vscode-mode: editor tab restore')
|
|
118
|
+
}
|
|
99
119
|
const originalOpenPath = workspaces?.openPath
|
|
100
120
|
const binder = pickSettingsBinder(ctx)
|
|
101
121
|
const settings = binder.scope
|
|
@@ -113,6 +133,10 @@ export function apply(ctx: any): void {
|
|
|
113
133
|
}
|
|
114
134
|
/** 可选探测 betterSidebar 服务(不进 inject:缺失会让插件停靠等待,杀死回退路径)。 */
|
|
115
135
|
let sideService = detectSidebarService(ctx)
|
|
136
|
+
/** 可选探测官方右侧 Sidebar(DSH 0.1.5-alpha.1+;探测命中 ≡ 版本判定,不进 inject 同上)。 */
|
|
137
|
+
let officialService = detectOfficial(ctx)
|
|
138
|
+
/** 官方 file 地址认领的当前注销器(null=未认领;由 syncFileClaim 依设置值切换)。 */
|
|
139
|
+
let claimDisposer: (() => void) | null = null
|
|
116
140
|
/** 「添加到对话」动作集:编辑区/Tab 右键菜单注入文件引用与代码块(conversation 服务缺失时各动作安全降级)。 */
|
|
117
141
|
const addToConversation = createAddToConversation(ctx)
|
|
118
142
|
|
|
@@ -122,7 +146,9 @@ export function apply(ctx: any): void {
|
|
|
122
146
|
{ name: '文件打开路由(workspaces.openPath)', active: Boolean(workspaces?.openPath), note: 'vscode 打开器优先,失败回退系统打开(0.1.2 及更早 DSH 的对话文件链接主路径)' },
|
|
123
147
|
{ name: '会话文件链接路由(remote.session.openWorkspacePath)', active: remoteOpenInstalled, note: '0.1.3+ 对话文件引用/产物打开优先走插件打开器,失败回退系统打开' },
|
|
124
148
|
{ name: '侧边栏打开器(' + SIDEBAR_PLUGIN + ')', active: registry.get(SIDEBAR_PLUGIN) !== undefined, note: registry.get(SIDEBAR_PLUGIN) !== undefined ? '已注册(优先级 80)' : '未检测到侧边栏打开能力' },
|
|
125
|
-
{ name: '
|
|
149
|
+
{ name: '侧边栏编辑区(官方 Sidebar)', active: officialService !== undefined, note: officialService !== undefined ? '编辑区=官方右侧 Sidebar Tab(对话+编辑同屏,DSH 0.1.5+)' : '官方侧边栏服务未探测到(DSH < 0.1.5-alpha.1 时属预期)' },
|
|
150
|
+
{ name: '侧边栏编辑区(' + SIDEBAR_PLUGIN + ',归档)', active: sideService !== undefined, note: sideService !== undefined ? '编辑区=侧边栏 Tab(旧版 DSH 回退形态)' : '未检测到;DSH ≥ 0.1.5 优先官方侧边栏,旧版可安装(' + SIDEBAR_INSTALL_CMD + ')' },
|
|
151
|
+
{ name: '文件链接官方认领(dsh-resource://file)', active: claimDisposer !== null, note: claimDisposer !== null ? '聊天文件链接由本插件编辑器接管(官方侧边栏内打开)' : '链接走官方查看器或旧版路由(未认领)' },
|
|
126
152
|
]
|
|
127
153
|
|
|
128
154
|
ctx.provide('fileOpeners', registry)
|
|
@@ -157,12 +183,29 @@ export function apply(ctx: any): void {
|
|
|
157
183
|
const sidebar = scanSidebar(ctx)
|
|
158
184
|
return sidebar ? registry.register(sidebar) : undefined
|
|
159
185
|
}, 'vscode-mode: sidebar file opener')
|
|
186
|
+
// 官方侧边栏正文组件装配(页类型与 file 认领两类 Tab 共用同一 EditorView 形态)
|
|
187
|
+
const officialRenderTab = (props: Record<string, unknown>) => React.createElement(OfficialSideTab, Object.assign({}, props, { schedule, addToConversation, sidebarPanels, outlineSources, fileMenuItems, sessions }))
|
|
188
|
+
/** 官方 file 地址认领同步:自动/VSCodeMode 档认领(链接进本插件编辑器),其余交官方查看器。 */
|
|
189
|
+
const syncFileClaim = (): void => {
|
|
190
|
+
const official = officialService
|
|
191
|
+
const want = official !== undefined && shouldClaimFiles(selected)
|
|
192
|
+
if (want && claimDisposer === null && official) {
|
|
193
|
+
const disposer = registerOfficialFileClaim({ tabs: official.tabs, slots: ctx.slots, renderTab: officialRenderTab })
|
|
194
|
+
if (disposer !== null) claimDisposer = ctx.effect(() => disposer, 'vscode-mode: official file claim')
|
|
195
|
+
return
|
|
196
|
+
}
|
|
197
|
+
if (!want && claimDisposer !== null) {
|
|
198
|
+
claimDisposer()
|
|
199
|
+
claimDisposer = null
|
|
200
|
+
}
|
|
201
|
+
}
|
|
160
202
|
ctx.effect(() => {
|
|
161
203
|
if (!settings) return undefined
|
|
162
204
|
const sync = (): void => {
|
|
163
205
|
const snapshot = settings.getSnapshot()
|
|
164
206
|
if (snapshot.status === 'loading') return
|
|
165
207
|
selected = autoValue(snapshot.value?.fileOpenTool)
|
|
208
|
+
syncFileClaim()
|
|
166
209
|
window.dispatchEvent(new CustomEvent('edrv:file-open-tool-change', { detail: { value: selected } }))
|
|
167
210
|
}
|
|
168
211
|
sync()
|
|
@@ -222,8 +265,8 @@ export function apply(ctx: any): void {
|
|
|
222
265
|
}
|
|
223
266
|
retryRemoteOpen()
|
|
224
267
|
|
|
225
|
-
// 中央「文件编辑」页签(旧形态回退):类 VSCode
|
|
226
|
-
//
|
|
268
|
+
// 中央「文件编辑」页签(旧形态回退):类 VSCode 编辑器;侧边栏形态(官方/better-sidebar)可用时
|
|
269
|
+
// 编辑器住侧边栏 Tab,本页签不注册(避免双实例:Monaco×2 + diff dock 每会话单源抢占)。
|
|
227
270
|
const registerLegacyTab = (): (() => void) | null => {
|
|
228
271
|
return registerSlotSafely(ctx, {
|
|
229
272
|
name: 'conversation.view',
|
|
@@ -234,16 +277,51 @@ export function apply(ctx: any): void {
|
|
|
234
277
|
}, (props: unknown) => React.createElement(EditorView, Object.assign({}, props, { layout: 'tab', sideHint: SIDEBAR_INSTALL_CMD, schedule, addToConversation, sidebarPanels, outlineSources, fileMenuItems, sessions })))
|
|
235
278
|
}
|
|
236
279
|
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
// bundle
|
|
240
|
-
//
|
|
241
|
-
//
|
|
280
|
+
// 侧边栏三形态互斥切换:官方(DSH 0.1.5+)> better-sidebar(归档,仅旧版回退)> 中央页签。
|
|
281
|
+
// officialDisposer / sideDisposer / legacyDisposer 任一时刻只保留其一(避免双实例 Monaco 抢占)。
|
|
282
|
+
// ⚠️ 两个可选服务都可能晚于本插件就绪(better-sidebar bundle 1MB+ 后加载;官方服务旧 DSH
|
|
283
|
+
// 不存在),一次性探测会误报——故启动后按 2s 间隔重试(窗口 ~30s):每 tick 先探官方再探
|
|
284
|
+
// better-sidebar;better-sidebar 先命中而官方后到时自动切换(官方优先),窗口结束仍未命中
|
|
285
|
+
// 则保持中央页签回退。
|
|
286
|
+
let officialDisposer: (() => void) | null = null
|
|
242
287
|
let sideDisposer: (() => void) | null = null
|
|
243
288
|
let legacyDisposer: (() => void) | null = null
|
|
289
|
+
const installOfficialForm = (): void => {
|
|
290
|
+
const official = officialService
|
|
291
|
+
if (officialDisposer !== null || !official) return
|
|
292
|
+
if (sideDisposer !== null) { sideDisposer(); sideDisposer = null }
|
|
293
|
+
if (legacyDisposer !== null) { legacyDisposer(); legacyDisposer = null }
|
|
294
|
+
const outcome = installOfficial({
|
|
295
|
+
tabs: official.tabs,
|
|
296
|
+
service: official.service,
|
|
297
|
+
slots: ctx.slots,
|
|
298
|
+
renderTab: officialRenderTab,
|
|
299
|
+
guide: {
|
|
300
|
+
title: () => OFFICIAL_TAB_TITLE,
|
|
301
|
+
description: () => 'Monaco 文件编辑器:对话与编辑同屏(差异审查/跳转/大纲)',
|
|
302
|
+
},
|
|
303
|
+
})
|
|
304
|
+
if (outcome === null) {
|
|
305
|
+
// 官方类型注册失败(API 变更等):撤下官方标记,回退旧形态链
|
|
306
|
+
officialService = undefined
|
|
307
|
+
applySideForm()
|
|
308
|
+
return
|
|
309
|
+
}
|
|
310
|
+
officialDisposer = ctx.effect(() => outcome, 'vscode-mode: official sidebar editor tab')
|
|
311
|
+
// 官方打开器(下拉「官方侧边栏」选项,动态出现;openResource 能力缺失时跳过)+ 按当前设置同步 file 地址认领
|
|
312
|
+
if (typeof official.service.openResource === 'function') {
|
|
313
|
+
ctx.effect(() => registry.register(officialSidebarOpener(official.service)), 'vscode-mode: official sidebar opener')
|
|
314
|
+
}
|
|
315
|
+
syncFileClaim()
|
|
316
|
+
}
|
|
244
317
|
const applySideForm = (): void => {
|
|
318
|
+
if (officialService) {
|
|
319
|
+
installOfficialForm()
|
|
320
|
+
return
|
|
321
|
+
}
|
|
245
322
|
if (sideService) {
|
|
246
323
|
if (sideDisposer !== null) return
|
|
324
|
+
if (officialDisposer !== null) { officialDisposer(); officialDisposer = null }
|
|
247
325
|
if (legacyDisposer !== null) { legacyDisposer(); legacyDisposer = null }
|
|
248
326
|
sideDisposer = ctx.effect(() => installSideEditor({
|
|
249
327
|
service: sideService as NonNullable<typeof sideService>,
|
|
@@ -256,27 +334,35 @@ export function apply(ctx: any): void {
|
|
|
256
334
|
},
|
|
257
335
|
registerLegacyFallback: registerLegacyTab,
|
|
258
336
|
}), 'vscode-mode: sidebar editor tab')
|
|
259
|
-
|
|
260
|
-
if (legacyDisposer !== null) return
|
|
261
|
-
if (sideDisposer !== null) { sideDisposer(); sideDisposer = null }
|
|
262
|
-
setEnsureSideEditor(null)
|
|
263
|
-
legacyDisposer = registerLegacyTab()
|
|
337
|
+
return
|
|
264
338
|
}
|
|
339
|
+
if (legacyDisposer !== null) return
|
|
340
|
+
if (officialDisposer !== null) { officialDisposer(); officialDisposer = null }
|
|
341
|
+
if (sideDisposer !== null) { sideDisposer(); sideDisposer = null }
|
|
342
|
+
setEnsureSideEditor(null)
|
|
343
|
+
legacyDisposer = registerLegacyTab()
|
|
265
344
|
}
|
|
266
345
|
applySideForm()
|
|
267
346
|
let sideRetries = 0
|
|
268
347
|
const retrySideService = (): void => {
|
|
269
|
-
if (
|
|
348
|
+
if (officialService !== undefined || sideRetries >= 15) return
|
|
270
349
|
sideRetries += 1
|
|
271
350
|
schedule(() => {
|
|
272
|
-
if (
|
|
273
|
-
|
|
274
|
-
if (
|
|
275
|
-
log.info('
|
|
351
|
+
if (officialService !== undefined) return
|
|
352
|
+
officialService = detectOfficial(ctx)
|
|
353
|
+
if (officialService !== undefined) {
|
|
354
|
+
log.info('检测到官方右侧 Sidebar(DSH 0.1.5+),切换官方侧边栏编辑形态')
|
|
276
355
|
applySideForm()
|
|
277
|
-
|
|
278
|
-
|
|
356
|
+
return
|
|
357
|
+
}
|
|
358
|
+
if (sideService === undefined) {
|
|
359
|
+
sideService = detectSidebarService(ctx)
|
|
360
|
+
if (sideService !== undefined) {
|
|
361
|
+
log.info('检测到 ' + SIDEBAR_PLUGIN + ',切换侧边栏编辑形态(官方未探测到,后续仍优先官方)')
|
|
362
|
+
applySideForm()
|
|
363
|
+
}
|
|
279
364
|
}
|
|
365
|
+
retrySideService()
|
|
280
366
|
}, 2000)
|
|
281
367
|
}
|
|
282
368
|
retrySideService()
|
package/src/client/navHistory.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* 后退回到上一处焦点,前进回到下一处,跨文件生效。
|
|
5
5
|
* 数据模型:past(末位 = 当前)+ future 两个栈;record 压栈并清空 future;
|
|
6
6
|
* 与栈顶同 path+line+column 视为同一位置:仅刷新 viewState 快照,不压栈、不清 future。
|
|
7
|
-
* 纯逻辑无 DOM
|
|
7
|
+
* 纯逻辑无 DOM 依赖,可单测;navHistoryFor 提供按工作区作用域的实例仓(跨会话复用)。
|
|
8
8
|
* 作者 ddj 2026年09月04号
|
|
9
9
|
*/
|
|
10
10
|
|
|
@@ -41,6 +41,12 @@ export interface NavHistory {
|
|
|
41
41
|
/** 后退栈默认容量上限(超出逐出最旧)。 */
|
|
42
42
|
export const NAV_HISTORY_CAP = 200
|
|
43
43
|
|
|
44
|
+
/** 工作区历史实例缓存上限(FIFO 逐出最久未用作用域,防长期驻留泄漏)。 */
|
|
45
|
+
export const NAV_SCOPE_CAP = 6
|
|
46
|
+
|
|
47
|
+
/** scope → 导航历史实例(同工作区跨会话复用同一份历史)。 */
|
|
48
|
+
const scopeHistories = new Map<string, NavHistory>()
|
|
49
|
+
|
|
44
50
|
/** 路径一致(容忍相对路径大小写差异,对齐既有 sameFile 语义常用写法)。 */
|
|
45
51
|
function samePath(a: string, b: string): boolean {
|
|
46
52
|
if (a === b) return true
|
|
@@ -102,3 +108,24 @@ export function createNavHistory(cap = NAV_HISTORY_CAP): NavHistory {
|
|
|
102
108
|
},
|
|
103
109
|
}
|
|
104
110
|
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 取(或建)某工作区作用域的导航历史实例:同 scope 跨会话复用同一实例,
|
|
114
|
+
* 后退/前进历史在切换对话后保留(对齐「同一工作区同一编辑区」体感)。
|
|
115
|
+
* 超出 NAV_SCOPE_CAP 时逐出最旧的实例(Map 插入序近似 LRU)。
|
|
116
|
+
* @author ddj 2026年09月09号
|
|
117
|
+
* @param scope 作用域键(scopeStore.workspaceScopeOf 产物)
|
|
118
|
+
* @param cap 后退栈容量上限(透传 createNavHistory)
|
|
119
|
+
* @returns 导航历史实例
|
|
120
|
+
*/
|
|
121
|
+
export function navHistoryFor(scope: string, cap = NAV_HISTORY_CAP): NavHistory {
|
|
122
|
+
const existing = scopeHistories.get(scope)
|
|
123
|
+
if (existing) return existing
|
|
124
|
+
const created = createNavHistory(cap)
|
|
125
|
+
scopeHistories.set(scope, created)
|
|
126
|
+
if (scopeHistories.size > NAV_SCOPE_CAP) {
|
|
127
|
+
const oldest = scopeHistories.keys().next().value
|
|
128
|
+
if (typeof oldest === 'string') scopeHistories.delete(oldest)
|
|
129
|
+
}
|
|
130
|
+
return created
|
|
131
|
+
}
|