mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-17 12:56:41 +02:00
137 lines
3.5 KiB
TypeScript
137 lines
3.5 KiB
TypeScript
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<string, FileViewState>
|
|
}>({
|
|
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(),
|
|
}
|
|
}
|