import { createEffect, createRoot } from "solid-js" import { createStore, produce } from "solid-js/store" import { Persist, persisted } from "@/utils/persist" import { createScopedCache } from "@/utils/scoped-cache" import type { FileViewState, SelectedLineRange } from "./types" const WORKSPACE_KEY = "__workspace__" const MAX_FILE_VIEW_SESSIONS = 20 const MAX_VIEW_FILES = 500 function normalizeSelectedLines(range: SelectedLineRange): SelectedLineRange { if (range.start <= range.end) return range const startSide = range.side const endSide = range.endSide ?? startSide return { ...range, start: range.end, end: range.start, side: endSide, endSide: startSide !== endSide ? startSide : undefined, } } function createViewSession(dir: string, id: string | undefined) { const legacyViewKey = `${dir}/file${id ? "/" + id : ""}.v1` const [view, setView, _, ready] = persisted( Persist.scoped(dir, id, "file-view", [legacyViewKey]), createStore<{ file: Record }>({ file: {}, }), ) const meta = { pruned: false } const pruneView = (keep?: string) => { const keys = Object.keys(view.file) if (keys.length <= MAX_VIEW_FILES) return const drop = keys.filter((key) => key !== keep).slice(0, keys.length - MAX_VIEW_FILES) if (drop.length === 0) return setView( produce((draft) => { for (const key of drop) { delete draft.file[key] } }), ) } createEffect(() => { if (!ready()) return if (meta.pruned) return meta.pruned = true pruneView() }) const scrollTop = (path: string) => view.file[path]?.scrollTop const scrollLeft = (path: string) => view.file[path]?.scrollLeft const selectedLines = (path: string) => view.file[path]?.selectedLines const setScrollTop = (path: string, top: number) => { setView("file", path, (current) => { if (current?.scrollTop === top) return current return { ...(current ?? {}), scrollTop: top, } }) pruneView(path) } const setScrollLeft = (path: string, left: number) => { setView("file", path, (current) => { if (current?.scrollLeft === left) return current return { ...(current ?? {}), scrollLeft: left, } }) pruneView(path) } const setSelectedLines = (path: string, range: SelectedLineRange | null) => { const next = range ? normalizeSelectedLines(range) : null setView("file", path, (current) => { if (current?.selectedLines === next) return current return { ...(current ?? {}), selectedLines: next, } }) pruneView(path) } return { ready, scrollTop, scrollLeft, selectedLines, setScrollTop, setScrollLeft, setSelectedLines, } } export function createFileViewCache() { const cache = createScopedCache( (key) => { const split = key.lastIndexOf("\n") const dir = split >= 0 ? key.slice(0, split) : key const id = split >= 0 ? key.slice(split + 1) : WORKSPACE_KEY return createRoot((dispose) => ({ value: createViewSession(dir, id === WORKSPACE_KEY ? undefined : id), dispose, })) }, { maxEntries: MAX_FILE_VIEW_SESSIONS, dispose: (entry) => entry.dispose(), }, ) return { load: (dir: string, id: string | undefined) => { const key = `${dir}\n${id ?? WORKSPACE_KEY}` return cache.get(key).value }, clear: () => cache.clear(), } }