ソースを参照

Apply PR #21537: fix(app): remove pierre diff virtualization

opencode-agent[bot] 11 時間 前
コミット
e6d622d329

+ 9 - 33
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<T> = PreloadMultiFileDiffResult<T> | PreloadFileDiffResult<T>
@@ -26,7 +25,6 @@ function DiffSSRViewer<T>(props: SSRDiffFileProps<T>) {
   let container!: HTMLDivElement
   let fileDiffRef!: HTMLElement
   let fileDiffInstance: FileDiff<T> | undefined
-  let sharedVirtualizer: NonNullable<ReturnType<typeof acquireVirtualizer>> | undefined
 
   const ready = createReadyWatcher()
   const workerPool = useWorkerPool(props.diffStyle)
@@ -51,14 +49,6 @@ function DiffSSRViewer<T>(props: SSRDiffFileProps<T>) {
 
   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<T>["selectedLines"], attempt = 0) => {
     const diff = fileDiffInstance
     if (!diff) return
@@ -92,27 +82,15 @@ function DiffSSRViewer<T>(props: SSRDiffFileProps<T>) {
 
     onCleanup(observeViewerScheme(() => fileDiffRef))
 
-    const virtualizer = getVirtualizer()
     const annotations = local.annotations ?? local.preloadedDiff.annotations ?? []
-    fileDiffInstance = virtualizer
-      ? new VirtualizedFileDiff<T>(
-          {
-            ...createDefaultOptions(props.diffStyle),
-            ...others,
-            ...local.preloadedDiff.options,
-          },
-          virtualizer,
-          virtualMetrics,
-          workerPool,
-        )
-      : new FileDiff<T>(
-          {
-            ...createDefaultOptions(props.diffStyle),
-            ...others,
-            ...local.preloadedDiff.options,
-          },
-          workerPool,
-        )
+    fileDiffInstance = new FileDiff<T>(
+      {
+        ...createDefaultOptions(props.diffStyle),
+        ...others,
+        ...(local.preloadedDiff.options ?? {}),
+      },
+      workerPool,
+    )
 
     applyViewerScheme(fileDiffRef)
 
@@ -163,8 +141,6 @@ function DiffSSRViewer<T>(props: SSRDiffFileProps<T>) {
   onCleanup(() => {
     clearReadyWatcher(ready)
     fileDiffInstance?.cleanUp()
-    sharedVirtualizer?.release()
-    sharedVirtualizer = undefined
   })
 
   return (

+ 4 - 122
packages/ui/src/components/file.tsx

@@ -1,6 +1,5 @@
 import { sampledChecksum } from "@opencode-ai/shared/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<VirtualFileMetrics>
-
 type SharedProps<T> = {
   annotations?: LineAnnotation<T>[] | DiffLineAnnotation<T>[]
   selectedLines?: SelectedLineRange | null
@@ -386,11 +372,6 @@ type AnnotationTarget<A> = {
   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<ReturnType<typeof acquireVirtualizer>> | 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<T>(props: TextFileProps<T>) {
-  let instance: PierreFile<T> | VirtualizedFile<T> | undefined
+  let instance: PierreFile<T> | undefined
   let viewer!: Viewer
 
   const [local, others] = splitProps(props, textKeys)
@@ -708,36 +631,12 @@ function TextViewer<T>(props: TextFileProps<T>) {
     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<T>(props: TextFileProps<T>) {
   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<T>(props: TextFileProps<T>) {
   createEffect(() => {
     const opts = options()
     const workerPool = getWorkerPool("unified")
-    const isVirtual = virtual()
-
-    const virtualizer = virtuals.get()
 
     renderViewer({
       viewer,
       current: instance,
-      create: () =>
-        isVirtual && virtualizer
-          ? new VirtualizedFile<T>(opts, virtualizer, codeMetrics, workerPool)
-          : new PierreFile<T>(opts, workerPool),
+      create: () => new PierreFile<T>(opts, workerPool),
       assign: (value) => {
         instance = value
       },
@@ -895,7 +785,6 @@ function TextViewer<T>(props: TextFileProps<T>) {
   onCleanup(() => {
     instance?.cleanUp()
     instance = undefined
-    virtuals.cleanup()
   })
 
   return <ViewerShell mode="text" viewer={viewer} class={local.class} classList={local.classList} />
@@ -991,8 +880,6 @@ function DiffViewer<T>(props: DiffFileProps<T>) {
     adapter,
   )
 
-  const virtuals = createSharedVirtualStrategy(() => viewer.container)
-
   const large = createMemo(() => {
     if (local.fileDiff) {
       const before = local.fileDiff.deletionLines.join("")
@@ -1055,7 +942,6 @@ function DiffViewer<T>(props: DiffFileProps<T>) {
   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<T>(props: DiffFileProps<T>) {
     renderViewer({
       viewer,
       current: instance,
-      create: () =>
-        virtualizer
-          ? new VirtualizedFileDiff<T>(opts, virtualizer, virtualMetrics, workerPool)
-          : new FileDiff<T>(opts, workerPool),
+      create: () => new FileDiff<T>(opts, workerPool),
       assign: (value) => {
         instance = value
       },
@@ -1111,7 +994,6 @@ function DiffViewer<T>(props: DiffFileProps<T>) {
   onCleanup(() => {
     instance?.cleanUp()
     instance = undefined
-    virtuals.cleanup()
     dragSide = undefined
     dragEndSide = undefined
   })

+ 0 - 66
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<string, HTMLElement>()
-  const nodes = new Map<string, HTMLDivElement>()
   const [store, setStore] = createStore({
     open: [] as string[],
-    visible: {} as Record<string, boolean>,
     force: {} as Record<string, boolean>,
     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<string, boolean> = {}
-
-    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<HTMLDivElement, Event> = (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<HTMLDivElement, Event>)(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()
                             }}
                           >
                             <Show when={expanded()}>
                               <Switch>
-                                <Match when={!mounted() && !tooLarge()}>
-                                  <div
-                                    data-slot="session-review-diff-placeholder"
-                                    class="rounded-lg border border-border-weak-base bg-background-stronger/40"
-                                    style={{ height: "160px" }}
-                                  />
-                                </Match>
                                 <Match when={tooLarge()}>
                                   <div data-slot="session-review-large-diff">
                                     <div data-slot="session-review-large-diff-title">

+ 0 - 100
packages/ui/src/pierre/virtualizer.ts

@@ -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<Document | HTMLElement, Entry>()
-
-export const virtualMetrics: Partial<VirtualFileMetrics> = {
-  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)
-    },
-  }
-}