diff --git a/packages/ui/src/components/file-ssr.tsx b/packages/ui/src/components/file-ssr.tsx index ad05555bdf..9cf83a4eac 100644 --- a/packages/ui/src/components/file-ssr.tsx +++ b/packages/ui/src/components/file-ssr.tsx @@ -1,4 +1,4 @@ -import { DIFFS_TAG_NAME, FileDiff, VirtualizedFileDiff } from "@pierre/diffs" +import { DIFFS_TAG_NAME, FileDiff } from "@pierre/diffs" import { type PreloadFileDiffResult, type PreloadMultiFileDiffResult } from "@pierre/diffs/ssr" import { createEffect, onCleanup, onMount, Show, splitProps } from "solid-js" import { Dynamic, isServer } from "solid-js/web" @@ -13,7 +13,6 @@ import { notifyShadowReady, observeViewerScheme, } from "../pierre/file-runtime" -import { acquireVirtualizer, virtualMetrics } from "../pierre/virtualizer" import { File, type DiffFileProps, type FileProps } from "./file" type DiffPreload = PreloadMultiFileDiffResult | PreloadFileDiffResult @@ -26,7 +25,6 @@ function DiffSSRViewer(props: SSRDiffFileProps) { let container!: HTMLDivElement let fileDiffRef!: HTMLElement let fileDiffInstance: FileDiff | undefined - let sharedVirtualizer: NonNullable> | undefined const ready = createReadyWatcher() const workerPool = useWorkerPool(props.diffStyle) @@ -51,14 +49,6 @@ function DiffSSRViewer(props: SSRDiffFileProps) { const getRoot = () => fileDiffRef?.shadowRoot ?? undefined - const getVirtualizer = () => { - if (sharedVirtualizer) return sharedVirtualizer.virtualizer - const result = acquireVirtualizer(container) - if (!result) return - sharedVirtualizer = result - return result.virtualizer - } - const setSelectedLines = (range: DiffFileProps["selectedLines"], attempt = 0) => { const diff = fileDiffInstance if (!diff) return @@ -92,27 +82,15 @@ function DiffSSRViewer(props: SSRDiffFileProps) { onCleanup(observeViewerScheme(() => fileDiffRef)) - const virtualizer = getVirtualizer() const annotations = local.annotations ?? local.preloadedDiff.annotations ?? [] - fileDiffInstance = virtualizer - ? new VirtualizedFileDiff( - { - ...createDefaultOptions(props.diffStyle), - ...others, - ...local.preloadedDiff.options, - }, - virtualizer, - virtualMetrics, - workerPool, - ) - : new FileDiff( - { - ...createDefaultOptions(props.diffStyle), - ...others, - ...local.preloadedDiff.options, - }, - workerPool, - ) + fileDiffInstance = new FileDiff( + { + ...createDefaultOptions(props.diffStyle), + ...others, + ...(local.preloadedDiff.options ?? {}), + }, + workerPool, + ) applyViewerScheme(fileDiffRef) @@ -163,8 +141,6 @@ function DiffSSRViewer(props: SSRDiffFileProps) { onCleanup(() => { clearReadyWatcher(ready) fileDiffInstance?.cleanUp() - sharedVirtualizer?.release() - sharedVirtualizer = undefined }) return ( diff --git a/packages/ui/src/components/file.tsx b/packages/ui/src/components/file.tsx index 97d4d69f78..faf1813cae 100644 --- a/packages/ui/src/components/file.tsx +++ b/packages/ui/src/components/file.tsx @@ -1,6 +1,5 @@ import { sampledChecksum } from "@opencode-ai/core/util/encode" import { - DEFAULT_VIRTUAL_FILE_METRICS, type DiffLineAnnotation, type FileContents, type FileDiffMetadata, @@ -10,10 +9,6 @@ import { type FileOptions, type LineAnnotation, type SelectedLineRange, - type VirtualFileMetrics, - VirtualizedFile, - VirtualizedFileDiff, - Virtualizer, } from "@pierre/diffs" import { type PreloadFileDiffResult, type PreloadMultiFileDiffResult } from "@pierre/diffs/ssr" import { createMediaQuery } from "@solid-primitives/media" @@ -40,19 +35,10 @@ import { readShadowLineSelection, } from "../pierre/file-selection" import { createLineNumberSelectionBridge, restoreShadowTextSelection } from "../pierre/selection-bridge" -import { acquireVirtualizer, virtualMetrics } from "../pierre/virtualizer" import { getWorkerPool } from "../pierre/worker" import { FileMedia, type FileMediaOptions } from "./file-media" import { FileSearchBar } from "./file-search" -const VIRTUALIZE_BYTES = 500_000 - -const codeMetrics = { - ...DEFAULT_VIRTUAL_FILE_METRICS, - lineHeight: 24, - fileGap: 0, -} satisfies Partial - type SharedProps = { annotations?: LineAnnotation[] | DiffLineAnnotation[] selectedLines?: SelectedLineRange | null @@ -386,11 +372,6 @@ type AnnotationTarget = { rerender: () => void } -type VirtualStrategy = { - get: () => Virtualizer | undefined - cleanup: () => void -} - function useModeViewer(config: ModeConfig, adapter: ModeAdapter) { return useFileViewer({ enableLineSelection: config.enableLineSelection, @@ -532,64 +513,6 @@ function scrollParent(el: HTMLElement): HTMLElement | undefined { } } -function createLocalVirtualStrategy(host: () => HTMLDivElement | undefined, enabled: () => boolean): VirtualStrategy { - let virtualizer: Virtualizer | undefined - let root: Document | HTMLElement | undefined - - const release = () => { - virtualizer?.cleanUp() - virtualizer = undefined - root = undefined - } - - return { - get: () => { - if (!enabled()) { - release() - return - } - if (typeof document === "undefined") return - - const wrapper = host() - if (!wrapper) return - - const next = scrollParent(wrapper) ?? document - if (virtualizer && root === next) return virtualizer - - release() - virtualizer = new Virtualizer() - root = next - virtualizer.setup(next, next instanceof Document ? undefined : wrapper) - return virtualizer - }, - cleanup: release, - } -} - -function createSharedVirtualStrategy(host: () => HTMLDivElement | undefined): VirtualStrategy { - let shared: NonNullable> | undefined - - const release = () => { - shared?.release() - shared = undefined - } - - return { - get: () => { - if (shared) return shared.virtualizer - - const container = host() - if (!container) return - - const result = acquireVirtualizer(container) - if (!result) return - shared = result - return result.virtualizer - }, - cleanup: release, - } -} - function parseLine(node: HTMLElement) { if (!node.dataset.line) return const value = parseInt(node.dataset.line, 10) @@ -688,7 +611,7 @@ function ViewerShell(props: { // --------------------------------------------------------------------------- function TextViewer(props: TextFileProps) { - let instance: PierreFile | VirtualizedFile | undefined + let instance: PierreFile | undefined let viewer!: Viewer const [local, others] = splitProps(props, textKeys) @@ -708,36 +631,12 @@ function TextViewer(props: TextFileProps) { return Math.max(1, total) } - const bytes = createMemo(() => { - const value = local.file.contents as unknown - if (typeof value === "string") return value.length - if (Array.isArray(value)) { - return value.reduce( - // oxlint-disable-next-line no-base-to-string -- array parts coerced intentionally - (sum, part) => sum + (typeof part === "string" ? part.length + 1 : String(part).length + 1), - 0, - ) - } - if (value == null) return 0 - // oxlint-disable-next-line no-base-to-string -- file contents cast to unknown, coercion is intentional - return String(value).length - }) - - const virtual = createMemo(() => bytes() > VIRTUALIZE_BYTES) - - const virtuals = createLocalVirtualStrategy(() => viewer.wrapper, virtual) - const lineFromMouseEvent = (event: MouseEvent): MouseHit => mouseHit(event, parseLine) const applySelection = (range: SelectedLineRange | null) => { const current = instance if (!current) return false - if (virtual()) { - current.setSelectedLines(range) - return true - } - const root = viewer.getRoot() if (!root) return false @@ -836,10 +735,7 @@ function TextViewer(props: TextFileProps) { const notify = () => { notifyRendered({ viewer, - isReady: (root) => { - if (virtual()) return root.querySelector("[data-line]") != null - return root.querySelectorAll("[data-line]").length >= lineCount() - }, + isReady: (root) => root.querySelectorAll("[data-line]").length >= lineCount(), onReady: () => { applySelection(viewer.lastSelection) viewer.find.refresh({ reset: true }) @@ -858,17 +754,11 @@ function TextViewer(props: TextFileProps) { createEffect(() => { const opts = options() const workerPool = getWorkerPool("unified") - const isVirtual = virtual() - - const virtualizer = virtuals.get() renderViewer({ viewer, current: instance, - create: () => - isVirtual && virtualizer - ? new VirtualizedFile(opts, virtualizer, codeMetrics, workerPool) - : new PierreFile(opts, workerPool), + create: () => new PierreFile(opts, workerPool), assign: (value) => { instance = value }, @@ -895,7 +785,6 @@ function TextViewer(props: TextFileProps) { onCleanup(() => { instance?.cleanUp() instance = undefined - virtuals.cleanup() }) return @@ -991,8 +880,6 @@ function DiffViewer(props: DiffFileProps) { adapter, ) - const virtuals = createSharedVirtualStrategy(() => viewer.container) - const large = createMemo(() => { if (local.fileDiff) { const before = local.fileDiff.deletionLines.join("") @@ -1055,7 +942,6 @@ function DiffViewer(props: DiffFileProps) { createEffect(() => { const opts = options() const workerPool = large() ? getWorkerPool("unified") : getWorkerPool(props.diffStyle) - const virtualizer = virtuals.get() const beforeContents = typeof local.before?.contents === "string" ? local.before.contents : "" const afterContents = typeof local.after?.contents === "string" ? local.after.contents : "" const done = preserve(viewer) @@ -1070,10 +956,7 @@ function DiffViewer(props: DiffFileProps) { renderViewer({ viewer, current: instance, - create: () => - virtualizer - ? new VirtualizedFileDiff(opts, virtualizer, virtualMetrics, workerPool) - : new FileDiff(opts, workerPool), + create: () => new FileDiff(opts, workerPool), assign: (value) => { instance = value }, @@ -1111,7 +994,6 @@ function DiffViewer(props: DiffFileProps) { onCleanup(() => { instance?.cleanUp() instance = undefined - virtuals.cleanup() dragSide = undefined dragEndSide = undefined }) diff --git a/packages/ui/src/components/session-review.tsx b/packages/ui/src/components/session-review.tsx index 949402f439..c9e5fe29a8 100644 --- a/packages/ui/src/components/session-review.tsx +++ b/packages/ui/src/components/session-review.tsx @@ -26,7 +26,6 @@ import type { LineCommentEditorProps } from "./line-comment" import { normalize, text, type ViewDiff } from "./session-diff" const MAX_DIFF_CHANGED_LINES = 500 -const REVIEW_MOUNT_MARGIN = 300 export type SessionReviewDiffStyle = "unified" | "split" @@ -159,14 +158,11 @@ type SessionReviewSelection = { export const SessionReview = (props: SessionReviewProps) => { let scroll: HTMLDivElement | undefined let focusToken = 0 - let frame: number | undefined const i18n = useI18n() const fileComponent = useFileComponent() const anchors = new Map() - const nodes = new Map() const [store, setStore] = createStore({ open: [] as string[], - visible: {} as Record, force: {} as Record, selection: null as SessionReviewSelection | null, commenting: null as SessionReviewSelection | null, @@ -196,44 +192,7 @@ export const SessionReview = (props: SessionReviewProps) => { const diffStyle = () => props.diffStyle ?? (props.split ? "split" : "unified") const hasDiffs = () => files().length > 0 - const syncVisible = () => { - frame = undefined - if (!scroll) return - - const root = scroll.getBoundingClientRect() - const top = root.top - REVIEW_MOUNT_MARGIN - const bottom = root.bottom + REVIEW_MOUNT_MARGIN - const openSet = new Set(open()) - const next: Record = {} - - for (const [file, el] of nodes) { - if (!openSet.has(file)) continue - const rect = el.getBoundingClientRect() - if (rect.bottom < top || rect.top > bottom) continue - next[file] = true - } - - const prev = untrack(() => store.visible) - const prevKeys = Object.keys(prev) - const nextKeys = Object.keys(next) - if (prevKeys.length === nextKeys.length && nextKeys.every((file) => prev[file])) return - setStore("visible", next) - } - - const queue = () => { - if (frame !== undefined) return - frame = requestAnimationFrame(syncVisible) - } - - const pinned = (file: string) => - props.focusedComment?.file === file || - props.focusedFile === file || - selection()?.file === file || - commenting()?.file === file || - opened()?.file === file - const handleScroll: JSX.EventHandler = (event) => { - queue() const next = props.onScroll if (!next) return if (Array.isArray(next)) { @@ -244,21 +203,9 @@ export const SessionReview = (props: SessionReviewProps) => { ;(next as JSX.EventHandler)(event) } - onCleanup(() => { - if (frame === undefined) return - cancelAnimationFrame(frame) - }) - - createEffect(() => { - props.open - files() - queue() - }) - const handleChange = (next: string[]) => { props.onOpenChange?.(next) if (props.open === undefined) setStore("open", next) - queue() } const handleExpandOrCollapseAll = () => { @@ -372,7 +319,6 @@ export const SessionReview = (props: SessionReviewProps) => { viewportRef={(el) => { scroll = el props.scrollRef?.(el) - queue() }} onScroll={handleScroll} classList={{ @@ -391,7 +337,6 @@ export const SessionReview = (props: SessionReviewProps) => { const diffCanRender = () => diff.additions !== 0 || diff.deletions !== 0 const expanded = createMemo(() => open().includes(file)) - const mounted = createMemo(() => expanded() && (!!store.visible[file] || pinned(file))) const force = () => !!store.force[file] const comments = createMemo(() => grouped().get(file) ?? []) @@ -482,8 +427,6 @@ export const SessionReview = (props: SessionReviewProps) => { onCleanup(() => { anchors.delete(file) - nodes.delete(file) - queue() }) const handleLineSelected = (range: SelectedLineRange | null) => { @@ -569,19 +512,10 @@ export const SessionReview = (props: SessionReviewProps) => { data-slot="session-review-diff-wrapper" ref={(el) => { anchors.set(file, el) - nodes.set(file, el) - queue() }} > - -
-
diff --git a/packages/ui/src/pierre/virtualizer.ts b/packages/ui/src/pierre/virtualizer.ts deleted file mode 100644 index 31862cc493..0000000000 --- a/packages/ui/src/pierre/virtualizer.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { type VirtualFileMetrics, Virtualizer } from "@pierre/diffs" - -type Target = { - key: Document | HTMLElement - root: Document | HTMLElement - content: HTMLElement | undefined -} - -type Entry = { - virtualizer: Virtualizer - refs: number -} - -const cache = new WeakMap() - -export const virtualMetrics: Partial = { - lineHeight: 24, - hunkSeparatorHeight: 24, - fileGap: 0, -} - -function scrollable(value: string) { - return value === "auto" || value === "scroll" || value === "overlay" -} - -function scrollRoot(container: HTMLElement) { - let node = container.parentElement - while (node) { - const style = getComputedStyle(node) - if (scrollable(style.overflowY)) return node - node = node.parentElement - } -} - -function target(container: HTMLElement): Target | undefined { - if (typeof document === "undefined") return - - const review = container.closest("[data-component='session-review']") - if (review instanceof HTMLElement) { - const root = scrollRoot(container) ?? review - const content = review.querySelector("[data-slot='session-review-container']") - return { - key: review, - root, - content: content instanceof HTMLElement ? content : undefined, - } - } - - const root = scrollRoot(container) - if (root) { - const content = root.querySelector("[role='log']") - return { - key: root, - root, - content: content instanceof HTMLElement ? content : undefined, - } - } - - return { - key: document, - root: document, - content: undefined, - } -} - -export function acquireVirtualizer(container: HTMLElement) { - const resolved = target(container) - if (!resolved) return - - let entry = cache.get(resolved.key) - if (!entry) { - const virtualizer = new Virtualizer() - virtualizer.setup(resolved.root, resolved.content) - entry = { - virtualizer, - refs: 0, - } - cache.set(resolved.key, entry) - } - - entry.refs += 1 - let done = false - - return { - virtualizer: entry.virtualizer, - release() { - if (done) return - done = true - - const current = cache.get(resolved.key) - if (!current) return - - current.refs -= 1 - if (current.refs > 0) return - - current.virtualizer.cleanUp() - cache.delete(resolved.key) - }, - } -}