返回 DeepSeek-Reasonix
workspaceViewMemory.ts
根目录 / desktop / frontend / src / lib / workspaceViewMemory.ts
1 import * as projectMemory from "./workspaceTreeMemory";
2 import { createWorkspaceTreePersistenceScheduler } from "./workspaceTreePersistence";
3 import type { WorkspaceTreeMemorySnapshot } from "./workspaceTreeMemory";
4
5 interface ViewState {
6 openDirs: string[];
7 selectedFilePath: string | null;
8 selectedChangePath: string | null;
9 openTabs: string[];
10 filter: string;
11 scrollTop: number;
12 treeWidth: number | null;
13 treeWidthMode: "manual" | "even";
14 }
15 interface ProjectViews {
16 seeded: boolean;
17 views: Record<string, ViewState>;
18 }
19 type Snapshot = WorkspaceTreeMemorySnapshot & { openTabs?: string[]; filter?: string };
20 const STORAGE_KEY = "reasonix.workspaceViews.v1";
21 const PREFIX = "dock-view:";
22 const projects = new Map<string, ProjectViews>();
23 let hydrated = false;
24 let writable = true;
25
26 export function workspaceViewMemoryKey(project: string, tab: string, seedLegacy = false): string {
27 return PREFIX + JSON.stringify([project, tab, seedLegacy]);
28 }
29
30 function identity(key: string): [string, string, boolean] | null {
31 if (!key.startsWith(PREFIX)) return null;
32 try {
33 const value = JSON.parse(key.slice(PREFIX.length));
34 return Array.isArray(value) && typeof value[0] === "string" && typeof value[1] === "string"
35 ? [value[0], value[1], value[2] === true] : null;
36 } catch { return null; }
37 }
38
39 function sanitize(value: Partial<ViewState> = {}): ViewState {
40 const paths = (items: unknown): string[] => Array.isArray(items)
41 ? items.filter((path): path is string => typeof path === "string") : [];
42 return {
43 openDirs: paths(value.openDirs).length ? paths(value.openDirs) : [""],
44 selectedFilePath: typeof value.selectedFilePath === "string" ? value.selectedFilePath : null,
45 selectedChangePath: typeof value.selectedChangePath === "string" ? value.selectedChangePath : null,
46 openTabs: [...new Set(paths(value.openTabs))].slice(-5),
47 filter: typeof value.filter === "string" ? value.filter : "",
48 scrollTop: Number.isFinite(value.scrollTop) && value.scrollTop! >= 0 ? value.scrollTop! : 0,
49 treeWidth: Number.isFinite(value.treeWidth) && value.treeWidth! > 0 ? value.treeWidth! : null,
50 treeWidthMode: value.treeWidthMode === "even" ? "even" : "manual",
51 };
52 }
53
54 function hydrate() {
55 if (hydrated) return;
56 hydrated = true;
57 try {
58 const data = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? "null");
59 if (!data) return;
60 if (data.version !== 1) { writable = false; return; }
61 for (const [key, value] of Object.entries(data.projects ?? {})) {
62 const project = value as ProjectViews;
63 if (!project || typeof project.views !== "object" || !project.views) continue;
64 projects.set(key, { seeded: project.seeded === true, views: Object.fromEntries(
65 Object.entries(project.views).filter(([, view]) => view && typeof view === "object")
66 .map(([id, view]) => [id, sanitize(view)]),
67 ) });
68 }
69 } catch { /* Storage is optional. */ }
70 }
71
72 function persist() {
73 if (!writable) return;
74 try {
75 localStorage.setItem(STORAGE_KEY, JSON.stringify({ version: 1, projects: Object.fromEntries(projects) }));
76 } catch { /* Keep the session's in-memory state when storage is unavailable. */ }
77 }
78 const deferred = createWorkspaceTreePersistenceScheduler(persist);
79
80 function resolve(key: string) {
81 const id = identity(key);
82 if (!id) return null;
83 hydrate();
84 const [projectKey, tab, seedLegacy] = id;
85 const project = projects.get(projectKey) ?? { seeded: false, views: Object.create(null) as Record<string, ViewState> };
86 const legacy = projectMemory.readWorkspaceTreeMemory(projectKey);
87 if (!Object.prototype.hasOwnProperty.call(project.views, tab)) {
88 const seed = !project.seeded && seedLegacy ? legacy : null;
89 project.views[tab] = sanitize(seed ? {
90 ...seed, openDirs: [...seed.openDirs],
91 openTabs: seed.selectedFilePath ? [seed.selectedFilePath] : [],
92 } : {});
93 if (seedLegacy) project.seeded = true;
94 projects.set(projectKey, project);
95 persist();
96 }
97 return { projectKey, project, tab, legacy };
98 }
99
100 export function readWorkspaceTreeMemory(key: string): Snapshot | null {
101 const view = resolve(key);
102 if (!view) return projectMemory.readWorkspaceTreeMemory(key);
103 const state = view.project.views[view.tab];
104 return {
105 ...state, openDirs: new Set(state.openDirs), openTabs: [...state.openTabs], visitId: 0,
106 recentPaths: view.legacy?.recentPaths ?? [],
107 dockTreeWidth: view.legacy?.dockTreeWidth ?? null,
108 dockPreviewWidth: view.legacy?.dockPreviewWidth ?? null,
109 };
110 }
111
112 export function rememberWorkspaceTreeState(
113 key: string, patch: Partial<Omit<Snapshot, "openDirs">> & { openDirs?: ReadonlySet<string> },
114 ) {
115 const view = resolve(key);
116 if (!view) { projectMemory.rememberWorkspaceTreeState(key, patch); return; }
117 const { recentPaths, dockTreeWidth, dockPreviewWidth, visitId: _visitId, ...browse } = patch;
118 const projectPatch = {
119 ...(recentPaths !== undefined ? { recentPaths } : {}),
120 ...(dockTreeWidth !== undefined ? { dockTreeWidth } : {}),
121 ...(dockPreviewWidth !== undefined ? { dockPreviewWidth } : {}),
122 };
123 if (Object.keys(projectPatch).length) projectMemory.rememberWorkspaceTreeState(view.projectKey, projectPatch);
124 if (Object.keys(browse).length) {
125 view.project.views[view.tab] = sanitize({
126 ...view.project.views[view.tab], ...browse,
127 openDirs: browse.openDirs ? [...browse.openDirs] : view.project.views[view.tab].openDirs,
128 });
129 deferred.cancel();
130 persist();
131 }
132 }
133
134 export function rememberWorkspaceTreeScroll(key: string, scrollTop: number) {
135 const view = resolve(key);
136 if (!view) { projectMemory.rememberWorkspaceTreeScroll(key, scrollTop); return; }
137 if (!Number.isFinite(scrollTop) || scrollTop < 0) return;
138 view.project.views[view.tab] = { ...view.project.views[view.tab], scrollTop };
139 deferred.schedule(key);
140 }
141 export function flushWorkspaceTreeMemory() { deferred.flush(); projectMemory.flushWorkspaceTreeMemory(); }
142 export function workspaceTreeVisitId(key: string) { return identity(key) ? 0 : projectMemory.workspaceTreeVisitId(key); }
143 export function touchWorkspaceTreeVisit(key: string, visitId: number) {
144 if (!identity(key)) projectMemory.touchWorkspaceTreeVisit(key, visitId);
145 }
146 export function rememberWorkspaceTreeOpenDirs(key: string, openDirs: ReadonlySet<string>, visitId: number) {
147 rememberWorkspaceTreeState(key, { openDirs, visitId });
148 }
149
150 export function reloadWorkspaceViewMemoryForTests() {
151 deferred.flush();
152 projects.clear();
153 hydrated = false;
154 writable = true;
155 }
156
156 lines TYPESCRIPT