dsh-vscode-mode 0.13.1 → 0.14.1
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 +47 -5
- package/lib/client.js +927 -147
- package/lib/client.js.map +1 -1
- package/lib/index.js +235 -14
- package/lib/index.js.map +1 -1
- package/package.json +8 -8
- package/src/client/index.ts +76 -2
- package/src/client/navHistory.ts +45 -0
- package/src/client/officialSidebar.ts +59 -1
- package/src/client/paths.ts +6 -0
- package/src/client/state/mdPreviewCache.ts +74 -0
- package/src/client/state/navStateCache.ts +83 -0
- package/src/client/state/uiStatePersist.ts +562 -0
- package/src/client/ui/EditorView.ts +65 -10
- package/src/dshVersion.ts +4 -2
- package/src/paths.ts +59 -3
- package/src/rpc.ts +50 -3
- package/src/shared/rpc.ts +24 -0
- package/src/uiState.ts +165 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vscode-mode",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 DSH 0.1.5+ 官方右侧 Sidebar 与对话同屏)+ 命令栏(Ctrl+Shift+P)与可扩展指令系统 + VS Code 兼容代码片段(.code-snippets 全局/项目,IntelliSense 展开)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚)+ SVN 集成(侧栏变更面板/日志弹窗/并排差异)+ 语言服务器(LSP)智能(跳转定义/引用/hover/大纲 + VSIX
|
|
3
|
+
"version": "0.14.1",
|
|
4
|
+
"description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 DSH 0.1.5+ 官方右侧 Sidebar 与对话同屏)+ 命令栏(Ctrl+Shift+P)与可扩展指令系统 + VS Code 兼容代码片段(.code-snippets 全局/项目,IntelliSense 展开)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚)+ SVN 集成(侧栏变更面板/日志弹窗/并排差异)+ 语言服务器(LSP)智能(跳转定义/引用/hover/大纲 + VSIX 扩展市场安装),状态持久化到工作区旁车,界面状态(页签/光标/侧栏/编辑 Tab/导航历史)镜像到 DSH home 跨重启恢复",
|
|
5
5
|
"icon": "assets/icon.svg",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"dsh",
|
|
@@ -48,12 +48,12 @@
|
|
|
48
48
|
"@vscode/ripgrep": "^1.18.0"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
|
-
"@deepseek-ai/dsh-client-ui-slots": ">=0.0.1-rc.1 <0.1.0 || >=0.1.0-rc.1 <0.2.0-0 || >=0.1.1-0 <0.2.0-0 || >=0.1.2-0 <0.2.0-0 || >=0.1.3-0 <0.2.0-0 || >=0.1.4-0 <0.2.0-0 || >=0.1.5-0 <0.2.0-0 || >=0.1.6-0 <0.2.0-0 || >=0.1.7-0 <0.2.0-0",
|
|
52
|
-
"@deepseek-ai/dsh-settings": ">=0.1.0-rc.1 <0.2.0-0 || >=0.1.1-0 <0.2.0-0 || >=0.1.2-0 <0.2.0-0 || >=0.1.3-0 <0.2.0-0 || >=0.1.4-0 <0.2.0-0 || >=0.1.5-0 <0.2.0-0 || >=0.1.6-0 <0.2.0-0 || >=0.1.7-0 <0.2.0-0",
|
|
53
|
-
"@deepseek-ai/dsh-client-ui-settings": ">=0.1.0-rc.1 <0.2.0-0 || >=0.1.1-0 <0.2.0-0 || >=0.1.2-0 <0.2.0-0 || >=0.1.3-0 <0.2.0-0 || >=0.1.4-0 <0.2.0-0 || >=0.1.5-0 <0.2.0-0 || >=0.1.6-0 <0.2.0-0 || >=0.1.7-0 <0.2.0-0",
|
|
54
|
-
"@deepseek-ai/dsh-client-ui-primitives": ">=0.1.0-rc.1 <0.2.0-0 || >=0.1.1-0 <0.2.0-0 || >=0.1.2-0 <0.2.0-0 || >=0.1.3-0 <0.2.0-0 || >=0.1.4-0 <0.2.0-0 || >=0.1.5-0 <0.2.0-0 || >=0.1.6-0 <0.2.0-0 || >=0.1.7-0 <0.2.0-0",
|
|
55
|
-
"@deepseek-ai/dsh-llm": ">=0.0.1-rc.1 <0.1.0 || >=0.1.0-rc.1 <0.2.0-0 || >=0.1.1-0 <0.2.0-0 || >=0.1.2-0 <0.2.0-0 || >=0.1.3-0 <0.2.0-0 || >=0.1.4-0 <0.2.0-0 || >=0.1.5-0 <0.2.0-0 || >=0.1.6-0 <0.2.0-0 || >=0.1.7-0 <0.2.0-0",
|
|
56
|
-
"@deepseek-ai/dsh-tools": ">=0.0.1-rc.1 <0.1.0 || >=0.1.0-rc.1 <0.2.0-0 || >=0.1.1-0 <0.2.0-0 || >=0.1.2-0 <0.2.0-0 || >=0.1.3-0 <0.2.0-0 || >=0.1.4-0 <0.2.0-0 || >=0.1.5-0 <0.2.0-0 || >=0.1.6-0 <0.2.0-0 || >=0.1.7-0 <0.2.0-0",
|
|
51
|
+
"@deepseek-ai/dsh-client-ui-slots": ">=0.0.1-rc.1 <0.1.0 || >=0.1.0-rc.1 <0.2.0-0 || >=0.1.1-0 <0.2.0-0 || >=0.1.2-0 <0.2.0-0 || >=0.1.3-0 <0.2.0-0 || >=0.1.4-0 <0.2.0-0 || >=0.1.5-0 <0.2.0-0 || >=0.1.6-0 <0.2.0-0 || >=0.1.7-0 <0.2.0-0 || =0.2.0-rc.1",
|
|
52
|
+
"@deepseek-ai/dsh-settings": ">=0.1.0-rc.1 <0.2.0-0 || >=0.1.1-0 <0.2.0-0 || >=0.1.2-0 <0.2.0-0 || >=0.1.3-0 <0.2.0-0 || >=0.1.4-0 <0.2.0-0 || >=0.1.5-0 <0.2.0-0 || >=0.1.6-0 <0.2.0-0 || >=0.1.7-0 <0.2.0-0 || =0.2.0-rc.1",
|
|
53
|
+
"@deepseek-ai/dsh-client-ui-settings": ">=0.1.0-rc.1 <0.2.0-0 || >=0.1.1-0 <0.2.0-0 || >=0.1.2-0 <0.2.0-0 || >=0.1.3-0 <0.2.0-0 || >=0.1.4-0 <0.2.0-0 || >=0.1.5-0 <0.2.0-0 || >=0.1.6-0 <0.2.0-0 || >=0.1.7-0 <0.2.0-0 || =0.2.0-rc.1",
|
|
54
|
+
"@deepseek-ai/dsh-client-ui-primitives": ">=0.1.0-rc.1 <0.2.0-0 || >=0.1.1-0 <0.2.0-0 || >=0.1.2-0 <0.2.0-0 || >=0.1.3-0 <0.2.0-0 || >=0.1.4-0 <0.2.0-0 || >=0.1.5-0 <0.2.0-0 || >=0.1.6-0 <0.2.0-0 || >=0.1.7-0 <0.2.0-0 || =0.2.0-rc.1",
|
|
55
|
+
"@deepseek-ai/dsh-llm": ">=0.0.1-rc.1 <0.1.0 || >=0.1.0-rc.1 <0.2.0-0 || >=0.1.1-0 <0.2.0-0 || >=0.1.2-0 <0.2.0-0 || >=0.1.3-0 <0.2.0-0 || >=0.1.4-0 <0.2.0-0 || >=0.1.5-0 <0.2.0-0 || >=0.1.6-0 <0.2.0-0 || >=0.1.7-0 <0.2.0-0 || =0.2.0-rc.1",
|
|
56
|
+
"@deepseek-ai/dsh-tools": ">=0.0.1-rc.1 <0.1.0 || >=0.1.0-rc.1 <0.2.0-0 || >=0.1.1-0 <0.2.0-0 || >=0.1.2-0 <0.2.0-0 || >=0.1.3-0 <0.2.0-0 || >=0.1.4-0 <0.2.0-0 || >=0.1.5-0 <0.2.0-0 || >=0.1.6-0 <0.2.0-0 || >=0.1.7-0 <0.2.0-0 || =0.2.0-rc.1",
|
|
57
57
|
"cordis": ">=4.0.0-rc <5",
|
|
58
58
|
"react": "^18.2.0",
|
|
59
59
|
"react-dom": "^18.2.0",
|
package/src/client/index.ts
CHANGED
|
@@ -33,7 +33,7 @@ import { setupExtOpen } from './externalOpen.js'
|
|
|
33
33
|
import { SettingsContext } from './settingsContext.js'
|
|
34
34
|
import { SIDEBAR_PLUGIN, registerSlotSafely, settingsBridge } from './compat.js'
|
|
35
35
|
import { detectSidebarService, hasSideRoute, installSideEditor, routeSideEditor, setEnsureSideEditor, SIDEBAR_INSTALL_CMD } from './sidebarBridge.js'
|
|
36
|
-
import { detectOfficial, installOfficial, registerOfficialFileClaim, OFFICIAL_TAB_KIND, OFFICIAL_TAB_TITLE, isEditorTabActive, restoreEditorTab, buildFileAddress } from './officialSidebar.js'
|
|
36
|
+
import { detectOfficial, installOfficial, registerOfficialFileClaim, OFFICIAL_TAB_KIND, OFFICIAL_TAB_TITLE, isEditorTabActive, markEditorActive, restoreEditorTab, buildFileAddress, readSavedTab, setEditorTabScope } from './officialSidebar.js'
|
|
37
37
|
import { setNativeCsv, setNativeOpenHandler, resetNativeOpen } from './nativeOpenStore.js'
|
|
38
38
|
import { SideEditorTab } from './ui/SideEditorTab.js'
|
|
39
39
|
import { OfficialSideTab } from './ui/OfficialSideTab.js'
|
|
@@ -64,6 +64,8 @@ import { disposeSnippets } from './snippets/provider.js'
|
|
|
64
64
|
import { disposeLaunchJson } from './dap/launchSnippetProvider.js'
|
|
65
65
|
import { disposeAiInline } from './ai/inlineProvider.js'
|
|
66
66
|
import { readSessionScope, subscribeScope } from './sessionScope.js'
|
|
67
|
+
import { flushUiState, hydrateUiState, installUiStatePersist, onUiStateReady, reportUiStateEvent, uiStateSettled } from './state/uiStatePersist.js'
|
|
68
|
+
import { workspaceScopeOf } from './state/scopeStore.js'
|
|
67
69
|
import { dapStore } from './dap/store.js'
|
|
68
70
|
import type { CompatAdapter } from '../shared/compat.js'
|
|
69
71
|
|
|
@@ -114,8 +116,48 @@ function setupCommands(ctx: any): CommandBridge {
|
|
|
114
116
|
* @param ctx 客户端根上下文(slots + timer 服务)
|
|
115
117
|
*/
|
|
116
118
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
117
|
-
export function apply(ctx: any): void {
|
|
119
|
+
export async function apply(ctx: any): Promise<void> {
|
|
118
120
|
const schedule = (fn: () => void, ms: number) => ctx.timeout(fn, ms)
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 无门控上报界面状态镜像事件(绕过客户端 edrv.debug 开关,直接落 host 诊断日志)。
|
|
124
|
+
* 开关关闭时 dbg 直接 return,「重启后没恢复」这类问题会在客户端侧彻底无痕,故另开一条通路。
|
|
125
|
+
* @author ddj 2026年09月29号
|
|
126
|
+
* @param text 事件文本
|
|
127
|
+
* @param level 日志级别(缺省 'debug')
|
|
128
|
+
*/
|
|
129
|
+
const reportMirror = (text: string, level?: string): void => {
|
|
130
|
+
// 只认 host 侧已知级别,其余落 'debug'(镜像内部只发 'debug'/'warn')
|
|
131
|
+
const lv = level === 'info' || level === 'warn' || level === 'error' ? level : 'debug'
|
|
132
|
+
try {
|
|
133
|
+
void rpc('edrv.debug', { sessionId: readSessionScope(ctx).sessionId, text: 'ui-state: ' + text, level: lv })
|
|
134
|
+
.catch(() => { /* 诊断上报失败忽略 */ })
|
|
135
|
+
} catch (error) { /* 会话未就绪等异常忽略 */ }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 界面状态镜像:把 `edrv.*` 的 localStorage 状态写穿到 host,换 origin(重启换端口)后回填。
|
|
139
|
+
// 必须早于任何界面首次读写 localStorage(编辑器页签/侧栏/预览态的恢复逻辑都读它)。
|
|
140
|
+
if (installUiStatePersist(ctx, schedule, reportMirror)) {
|
|
141
|
+
reportMirror('镜像装配成功(origin=' + (typeof location === 'undefined' ? '?' : location.origin) + ')')
|
|
142
|
+
log.info('界面状态镜像已装配:界面状态随 host 持久化(重启/换端口可恢复)')
|
|
143
|
+
} else {
|
|
144
|
+
reportMirror('镜像装配失败:localStorage 不可用', 'warn')
|
|
145
|
+
log.warn('界面状态镜像不可用(无 localStorage):界面状态仅存当前 origin')
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 争取「回填先于任何界面组件挂载」:面板/编辑器的恢复逻辑都在挂载时读 localStorage,
|
|
149
|
+
// 挂载后再回填只能靠 `edrv:ui-state-ready` 事件补跑(目前编辑器页签/侧栏已支持)。
|
|
150
|
+
// 已知会话时这次 RPC 是本地往返(通常几十毫秒),故短超时即可;会话未就绪则不在此等待,
|
|
151
|
+
// 交给镜像内部重试与迟到回填事件,避免拖慢启动。
|
|
152
|
+
if (readSessionScope(ctx).sessionId) {
|
|
153
|
+
try {
|
|
154
|
+
await Promise.race([
|
|
155
|
+
hydrateUiState(),
|
|
156
|
+
new Promise<void>((resolve) => { schedule(() => resolve(), 600) }),
|
|
157
|
+
])
|
|
158
|
+
} catch (error) { /* 水合失败已在镜像内降级 */ }
|
|
159
|
+
}
|
|
160
|
+
ctx.effect(() => () => { void flushUiState() }, 'vscode-mode: ui state flush')
|
|
119
161
|
ctx.effect(() => {
|
|
120
162
|
let disposed = false
|
|
121
163
|
let attempts = 0
|
|
@@ -482,6 +524,22 @@ export function apply(ctx: any): void {
|
|
|
482
524
|
return
|
|
483
525
|
}
|
|
484
526
|
officialDisposer = ctx.effect(() => outcome, 'vscode-mode: official sidebar editor tab')
|
|
527
|
+
// 编辑 Tab 展开/激活态按工作区落 localStorage(镜像层写穿 host,重启/换端口后仍在):
|
|
528
|
+
// 冷启动据此自动回到上次的编辑 Tab;官方 openTab 幂等,已激活时不重复调用。
|
|
529
|
+
setEditorTabScope(() => {
|
|
530
|
+
const scope = readSessionScope(ctx)
|
|
531
|
+
return workspaceScopeOf(scope.cwd, scope.sessionId)
|
|
532
|
+
})
|
|
533
|
+
const restoreActiveTab = (): void => {
|
|
534
|
+
const saved = readSavedTab()
|
|
535
|
+
const active = isEditorTabActive()
|
|
536
|
+
reportUiStateEvent('编辑 Tab 恢复:本地存档=' + String(saved) + '、当前激活=' + String(active))
|
|
537
|
+
if (!active && saved) markEditorActive(true)
|
|
538
|
+
restoreEditorTab(official.service, schedule)
|
|
539
|
+
}
|
|
540
|
+
restoreActiveTab()
|
|
541
|
+
// 换 origin 时本地 localStorage 为空:镜像回填完成后重判一次,迟到的回填同样能恢复编辑 Tab
|
|
542
|
+
ctx.effect(() => onUiStateReady(restoreActiveTab), 'vscode-mode: editor tab state hydration')
|
|
485
543
|
// 官方打开器(下拉「官方侧边栏」选项,动态出现;openResource 能力缺失时跳过)+ 按当前设置同步 file 地址认领
|
|
486
544
|
if (typeof official.service.openResource === 'function') {
|
|
487
545
|
ctx.effect(() => registry.register(officialSidebarOpener(official.service)), 'vscode-mode: official sidebar opener')
|
|
@@ -593,4 +651,20 @@ export function apply(ctx: any): void {
|
|
|
593
651
|
// 原生打开范围复位回默认集(handler 解绑已由其自身 ctx.effect disposer 承担)
|
|
594
652
|
try { resetNativeOpen() } catch { /* 同上 */ }
|
|
595
653
|
}, 'vscode-mode: monaco providers teardown')
|
|
654
|
+
|
|
655
|
+
// 会话晚到补试:冷启动早期 `sessions` 未就绪时 host 答「会话不存在」,首次水合会失败重试;
|
|
656
|
+
// 作用域变化(sid→ws)后同样补一次,保证界面状态尽早回填。
|
|
657
|
+
ctx.effect(() => subscribeScope(ctx, () => {
|
|
658
|
+
if (!uiStateSettled()) void hydrateUiState()
|
|
659
|
+
}), 'vscode-mode: ui state rehydrate')
|
|
660
|
+
|
|
661
|
+
// 争取「回填先于界面首次读 localStorage」:宿主若 await 本 apply,此处等到水合结束(或 1.2s 超时兜底)。
|
|
662
|
+
// 失败/超时不抛:镜像内部自带重试与结束事件,界面侧还有迟到回填重跑。
|
|
663
|
+
try {
|
|
664
|
+
await Promise.race([
|
|
665
|
+
hydrateUiState(),
|
|
666
|
+
new Promise<void>((resolve) => { schedule(() => resolve(), 1200) }),
|
|
667
|
+
])
|
|
668
|
+
} catch (error) { /* 水合失败已在镜像内降级 */ }
|
|
669
|
+
reportUiStateEvent('客户端装配完成:水合' + (uiStateSettled() ? '已结束' : '仍在进行(界面稍后重跑恢复)'))
|
|
596
670
|
}
|
package/src/client/navHistory.ts
CHANGED
|
@@ -36,6 +36,10 @@ export interface NavHistory {
|
|
|
36
36
|
peekBack(): NavEntry | null
|
|
37
37
|
/** 前进目标条目(不改变状态;不可进 → null)。 */
|
|
38
38
|
peekForward(): NavEntry | null
|
|
39
|
+
/** 导出双栈快照(浅拷贝;持久化用)。 */
|
|
40
|
+
snapshot(): NavState
|
|
41
|
+
/** 用存档恢复双栈(调用方仅在实例为空时调用;非法项丢弃、超限截断)。 */
|
|
42
|
+
restore(state: NavState | null | undefined): void
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
/** 后退栈默认容量上限(超出逐出最旧)。 */
|
|
@@ -44,6 +48,37 @@ export const NAV_HISTORY_CAP = 200
|
|
|
44
48
|
/** 工作区历史实例缓存上限(FIFO 逐出最久未用作用域,防长期驻留泄漏)。 */
|
|
45
49
|
export const NAV_SCOPE_CAP = 6
|
|
46
50
|
|
|
51
|
+
/** 持久化单栈容量上限(存档只留最近若干条,防 localStorage 膨胀)。 */
|
|
52
|
+
export const NAV_PERSIST_CAP = 30
|
|
53
|
+
|
|
54
|
+
/** 导航历史双栈快照(持久化/恢复用;viewState 由 viewStateCache 按路径另行恢复)。 */
|
|
55
|
+
export interface NavState {
|
|
56
|
+
past: NavEntry[]
|
|
57
|
+
future: NavEntry[]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 归一化条目数组:丢弃无路径项;line/column 仅保留正整数,剥掉 viewState(体积大且另有存档);
|
|
62
|
+
* 超限时保留末尾(最近的)cap 条。
|
|
63
|
+
* @author ddj 2026年09月28号
|
|
64
|
+
* @param entries 待归一化的原始数组(非数组 → 空表)
|
|
65
|
+
* @param cap 单栈上限(默认 NAV_PERSIST_CAP)
|
|
66
|
+
* @returns 归一化条目数组
|
|
67
|
+
*/
|
|
68
|
+
export function cleanNavEntries(entries: unknown, cap = NAV_PERSIST_CAP): NavEntry[] {
|
|
69
|
+
if (!Array.isArray(entries)) return []
|
|
70
|
+
const out: NavEntry[] = []
|
|
71
|
+
for (const raw of entries) {
|
|
72
|
+
const entry = raw as NavEntry
|
|
73
|
+
if (!entry || typeof entry.path !== 'string' || !entry.path) continue
|
|
74
|
+
const item: NavEntry = { path: entry.path }
|
|
75
|
+
if (typeof entry.line === 'number' && Number.isInteger(entry.line) && entry.line > 0) item.line = entry.line
|
|
76
|
+
if (typeof entry.column === 'number' && Number.isInteger(entry.column) && entry.column > 0) item.column = entry.column
|
|
77
|
+
out.push(item)
|
|
78
|
+
}
|
|
79
|
+
return out.length > cap ? out.slice(out.length - cap) : out
|
|
80
|
+
}
|
|
81
|
+
|
|
47
82
|
/** scope → 导航历史实例(同工作区跨会话复用同一份历史)。 */
|
|
48
83
|
const scopeHistories = new Map<string, NavHistory>()
|
|
49
84
|
|
|
@@ -106,6 +141,16 @@ export function createNavHistory(cap = NAV_HISTORY_CAP): NavHistory {
|
|
|
106
141
|
peekForward(): NavEntry | null {
|
|
107
142
|
return future.length ? future[future.length - 1] : null
|
|
108
143
|
},
|
|
144
|
+
snapshot(): NavState {
|
|
145
|
+
return { past: past.map((entry) => ({ ...entry })), future: future.map((entry) => ({ ...entry })) }
|
|
146
|
+
},
|
|
147
|
+
restore(state: NavState | null | undefined): void {
|
|
148
|
+
if (!state) return
|
|
149
|
+
past.length = 0
|
|
150
|
+
future.length = 0
|
|
151
|
+
past.push(...cleanNavEntries(state.past, cap))
|
|
152
|
+
future.push(...cleanNavEntries(state.future, cap))
|
|
153
|
+
},
|
|
109
154
|
}
|
|
110
155
|
}
|
|
111
156
|
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import { log } from './log.js'
|
|
13
13
|
import { setEnsureSideEditor } from './sidebarBridge.js'
|
|
14
|
+
import { CACHE_KEY } from './paths.js'
|
|
14
15
|
import { OFFICE_EXT, BLIND_EXT, suffixOf } from '../shared/nativeOpen.js'
|
|
15
16
|
import { inNativePath } from './nativeOpenStore.js'
|
|
16
17
|
|
|
@@ -365,6 +366,61 @@ export function registerOfficialFileClaim(options: {
|
|
|
365
366
|
let editorTabActive = false
|
|
366
367
|
/** 正文挂载纪元(每次挂载自增;延迟判定期间有新挂载则放弃清除)。 */
|
|
367
368
|
let mountEpoch = 0
|
|
369
|
+
/** 作用域解析器(index.ts 装配时注入;未注入则不落盘,退回纯内存标记)。 */
|
|
370
|
+
let tabScopeOf: (() => string) | null = null
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* 注入编辑 Tab 状态的作用域解析器(index.ts 装配一次;键跟随会话/工作区切换)。
|
|
374
|
+
* @author ddj 2026年09月28号
|
|
375
|
+
* @param resolve 返回工作区作用域键(scopeStore.workspaceScopeOf 产物)
|
|
376
|
+
*/
|
|
377
|
+
export function setEditorTabScope(resolve: () => string): void {
|
|
378
|
+
tabScopeOf = resolve
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* 编辑 Tab 状态的作用域键(未注入/解析失败 → 空串,调用方跳过持久化)。
|
|
383
|
+
* @author ddj 2026年09月28号
|
|
384
|
+
* @returns localStorage 键
|
|
385
|
+
*/
|
|
386
|
+
function editorTabKey(): string {
|
|
387
|
+
if (!tabScopeOf) return ''
|
|
388
|
+
try {
|
|
389
|
+
const scope = tabScopeOf()
|
|
390
|
+
return scope ? CACHE_KEY.editorTab + scope : ''
|
|
391
|
+
} catch (error) {
|
|
392
|
+
return ''
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* 读回「上次退出时编辑 Tab 是否处于激活态」(供冷启动自动展开编辑 Tab)。
|
|
398
|
+
* @author ddj 2026年09月28号
|
|
399
|
+
* @returns 是否曾被激活(存储不可用/未注入 → false)
|
|
400
|
+
*/
|
|
401
|
+
export function readSavedTab(): boolean {
|
|
402
|
+
const key = editorTabKey()
|
|
403
|
+
if (!key) return false
|
|
404
|
+
try {
|
|
405
|
+
return localStorage.getItem(key) === '1'
|
|
406
|
+
} catch (error) {
|
|
407
|
+
return false
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* 持久化编辑 Tab 激活态(存储不可用静默;镜像层会把它写穿到 host 跨重启保留)。
|
|
413
|
+
* @author ddj 2026年09月28号
|
|
414
|
+
* @param active 是否激活
|
|
415
|
+
*/
|
|
416
|
+
export function saveTabActive(active: boolean): void {
|
|
417
|
+
const key = editorTabKey()
|
|
418
|
+
if (!key) return
|
|
419
|
+
try {
|
|
420
|
+
if (active) localStorage.setItem(key, '1')
|
|
421
|
+
else localStorage.removeItem(key)
|
|
422
|
+
} catch (error) { /* 存储不可用:仅内存标记生效 */ }
|
|
423
|
+
}
|
|
368
424
|
|
|
369
425
|
/**
|
|
370
426
|
* 标记编辑 Tab 激活并推进挂载纪元(正文挂载时调用)。
|
|
@@ -377,11 +433,13 @@ export function markEditorMounted(): void {
|
|
|
377
433
|
|
|
378
434
|
/**
|
|
379
435
|
* 直接设置激活标记(正文卸载延迟判定用;测试 also 用作状态编排入口)。
|
|
380
|
-
*
|
|
436
|
+
* 同时写穿到 localStorage(镜像层负责跨重启保留),供下次冷启动恢复。
|
|
437
|
+
* @author ddj 2026年09月09号 / 2026年09月28号
|
|
381
438
|
* @param active 是否激活
|
|
382
439
|
*/
|
|
383
440
|
export function markEditorActive(active: boolean): void {
|
|
384
441
|
editorTabActive = active
|
|
442
|
+
saveTabActive(active)
|
|
385
443
|
}
|
|
386
444
|
|
|
387
445
|
/**
|
package/src/client/paths.ts
CHANGED
|
@@ -30,6 +30,12 @@ export const CACHE_KEY = {
|
|
|
30
30
|
rules: 'edrv.rules.v1.',
|
|
31
31
|
/** SVN 变更面板 UI 状态(SvnPanel v1:未版本控制/忽略项显示开关,按作用域隔离)。 */
|
|
32
32
|
svn: 'edrv.svn.v1.',
|
|
33
|
+
/** Markdown 预览态(EditorView v1:处于预览态的 Markdown 路径集合,按作用域隔离)。 */
|
|
34
|
+
mdPreview: 'edrv.md-preview.v1.',
|
|
35
|
+
/** 官方侧边栏「文件编辑」Tab 展开/激活态(officialSidebar v1:重启后自动回到编辑 Tab)。 */
|
|
36
|
+
editorTab: 'edrv.editor-tab.v1.',
|
|
37
|
+
/** 编辑器导航历史双栈(navStateCache v1:重启后前进/后退与当前位置仍在,按作用域隔离)。 */
|
|
38
|
+
navHistory: 'edrv.nav-history.v1.',
|
|
33
39
|
} as const
|
|
34
40
|
|
|
35
41
|
/** 按前缀拼作用域键(scopeStore 产出作用域;侧边栏/编辑器等需要布局段时自行拼接后传入)。 */
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — Markdown 预览态缓存(localStorage,按工作区作用域)。
|
|
3
|
+
* 语义对齐编辑器布局记忆:切换页签/刷新/重启后,处于预览态的 Markdown 仍以预览打开。
|
|
4
|
+
* 键 = CACHE_KEY.mdPreview + scope;值为路径数组。解析/归一化纯函数可单测,IO 收在 load/save 两端。
|
|
5
|
+
* 作者 ddj 2026-09-28
|
|
6
|
+
*/
|
|
7
|
+
import { CACHE_KEY } from '../paths.js'
|
|
8
|
+
|
|
9
|
+
/** 单作用域记忆的预览态路径上限(超出按顺序丢弃最早,防 localStorage 膨胀)。 */
|
|
10
|
+
export const MD_PREVIEW_CAP = 50
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 归一化预览态路径表:丢弃非字符串/空串、去重(保留首次出现顺序),超限截断。
|
|
14
|
+
* @author ddj 2026年09月28号
|
|
15
|
+
* @param value 待归一化的解析产物(任意输入,非数组 → 空表)
|
|
16
|
+
* @returns 路径数组
|
|
17
|
+
*/
|
|
18
|
+
export function normalizeMdPreview(value: unknown): string[] {
|
|
19
|
+
if (!Array.isArray(value)) return []
|
|
20
|
+
const out: string[] = []
|
|
21
|
+
for (const item of value) {
|
|
22
|
+
if (typeof item !== 'string' || !item) continue
|
|
23
|
+
if (out.includes(item)) continue
|
|
24
|
+
out.push(item)
|
|
25
|
+
if (out.length >= MD_PREVIEW_CAP) break
|
|
26
|
+
}
|
|
27
|
+
return out
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 解析存档文本(空/损坏/非数组 → 空表)。
|
|
32
|
+
* @author ddj 2026年09月28号
|
|
33
|
+
* @param text localStorage 原文
|
|
34
|
+
* @returns 路径数组
|
|
35
|
+
*/
|
|
36
|
+
export function parseMdPreview(text: string | null): string[] {
|
|
37
|
+
if (!text) return []
|
|
38
|
+
try {
|
|
39
|
+
return normalizeMdPreview(JSON.parse(text))
|
|
40
|
+
} catch (error) {
|
|
41
|
+
return []
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 读某作用域处于预览态的 Markdown 路径集合。
|
|
47
|
+
* @author ddj 2026年09月28号
|
|
48
|
+
* @param scope 作用域键(scopeStore.workspaceScopeOf 产物)
|
|
49
|
+
* @returns 预览态路径集合(存储不可用/无存档 → 空集)
|
|
50
|
+
*/
|
|
51
|
+
export function mdPreviewLoad(scope: string): Set<string> {
|
|
52
|
+
try {
|
|
53
|
+
return new Set(parseMdPreview(localStorage.getItem(CACHE_KEY.mdPreview + scope)))
|
|
54
|
+
} catch (error) {
|
|
55
|
+
return new Set()
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 写某作用域的预览态集合(空集删除键,避免残留空数组)。
|
|
61
|
+
* @author ddj 2026年09月28号
|
|
62
|
+
* @param scope 作用域键
|
|
63
|
+
* @param paths 预览态路径集合
|
|
64
|
+
*/
|
|
65
|
+
export function mdPreviewSave(scope: string, paths: Set<string>): void {
|
|
66
|
+
try {
|
|
67
|
+
const list = normalizeMdPreview(Array.from(paths))
|
|
68
|
+
if (!list.length) {
|
|
69
|
+
localStorage.removeItem(CACHE_KEY.mdPreview + scope)
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
localStorage.setItem(CACHE_KEY.mdPreview + scope, JSON.stringify(list))
|
|
73
|
+
} catch (error) { /* 存储不可用:预览态仅存活本次会话 */ }
|
|
74
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — 编辑器导航历史存档(localStorage,按工作区作用域)。
|
|
3
|
+
* 语义:刷新/重启后「后退/前进」双栈与当前位置仍在(对齐 VSCode navigation history 的体感,
|
|
4
|
+
* 只是延长到跨会话/跨 origin)。条目不含 viewState——它体积大且已由 viewStateCache 按路径存档。
|
|
5
|
+
* 键 = CACHE_KEY.navHistory + scope;序列化/解析为纯函数,IO 收在 load/save 两端。
|
|
6
|
+
* 作者 ddj 2026-09-28
|
|
7
|
+
*/
|
|
8
|
+
import { CACHE_KEY } from '../paths.js'
|
|
9
|
+
import { cleanNavEntries, NAV_PERSIST_CAP, type NavState } from '../navHistory.js'
|
|
10
|
+
|
|
11
|
+
/** 存档格式版本(结构不兼容时递增,旧档自动作废)。 */
|
|
12
|
+
export const NAV_STATE_VERSION = 1
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 序列化双栈(空栈 → 空串,调用方据此删键)。
|
|
16
|
+
* @author ddj 2026年09月28号
|
|
17
|
+
* @param state 双栈快照(null/undefined → 空串)
|
|
18
|
+
* @returns 存档文本
|
|
19
|
+
*/
|
|
20
|
+
export function serializeNavState(state: NavState | null | undefined): string {
|
|
21
|
+
if (!state) return ''
|
|
22
|
+
const past = cleanNavEntries(state.past, NAV_PERSIST_CAP)
|
|
23
|
+
const future = cleanNavEntries(state.future, NAV_PERSIST_CAP)
|
|
24
|
+
if (!past.length && !future.length) return ''
|
|
25
|
+
return JSON.stringify({ v: NAV_STATE_VERSION, past, future })
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 解析存档文本(空/损坏/版本不符/无可恢复条目 → null)。
|
|
30
|
+
* @author ddj 2026年09月28号
|
|
31
|
+
* @param text localStorage 原文
|
|
32
|
+
* @returns 双栈快照
|
|
33
|
+
*/
|
|
34
|
+
export function parseNavState(text: string | null): NavState | null {
|
|
35
|
+
if (!text) return null
|
|
36
|
+
try {
|
|
37
|
+
const value = JSON.parse(text) as { v?: unknown; past?: unknown; future?: unknown }
|
|
38
|
+
if (!value || typeof value !== 'object' || value.v !== NAV_STATE_VERSION) return null
|
|
39
|
+
const past = cleanNavEntries(value.past, NAV_PERSIST_CAP)
|
|
40
|
+
const future = cleanNavEntries(value.future, NAV_PERSIST_CAP)
|
|
41
|
+
if (!past.length && !future.length) return null
|
|
42
|
+
return { past, future }
|
|
43
|
+
} catch (error) {
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** scope → 最近写入的原文(去重写:光标移动触发的快照大多与上次相同)。 */
|
|
49
|
+
const lastWritten = new Map<string, string>()
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 读某作用域的导航历史存档。
|
|
53
|
+
* @author ddj 2026年09月28号
|
|
54
|
+
* @param scope 作用域键(scopeStore.workspaceScopeOf 产物)
|
|
55
|
+
* @returns 双栈快照(无存档/存储不可用 → null)
|
|
56
|
+
*/
|
|
57
|
+
export function navStateLoad(scope: string): NavState | null {
|
|
58
|
+
try {
|
|
59
|
+
return parseNavState(localStorage.getItem(CACHE_KEY.navHistory + scope))
|
|
60
|
+
} catch (error) {
|
|
61
|
+
return null
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 写某作用域的导航历史存档(空存档删键;原文未变跳过写入)。
|
|
67
|
+
* @author ddj 2026年09月28号
|
|
68
|
+
* @param scope 作用域键
|
|
69
|
+
* @param state 双栈快照(null → 删除存档)
|
|
70
|
+
*/
|
|
71
|
+
export function navStateSave(scope: string, state: NavState | null | undefined): void {
|
|
72
|
+
try {
|
|
73
|
+
const text = serializeNavState(state)
|
|
74
|
+
if (lastWritten.get(scope) === text) return
|
|
75
|
+
lastWritten.set(scope, text)
|
|
76
|
+
const key = CACHE_KEY.navHistory + scope
|
|
77
|
+
if (!text) {
|
|
78
|
+
localStorage.removeItem(key)
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
localStorage.setItem(key, text)
|
|
82
|
+
} catch (error) { /* 存储不可用:导航历史仅存活本次会话 */ }
|
|
83
|
+
}
|