From d8ec826c170b561fa744d9fe4e00b83c6b6fa92a Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Sun, 8 Feb 2026 06:15:37 -0600 Subject: [PATCH] wip(ui): update pierre diffs --- bun.lock | 4 +- package.json | 2 +- packages/ui/src/components/diff-ssr.tsx | 45 ++++++++++++--- packages/ui/src/components/diff.tsx | 21 ++++++- packages/ui/src/pierre/index.ts | 1 + packages/ui/src/pierre/virtualizer.ts | 76 +++++++++++++++++++++++++ 6 files changed, 135 insertions(+), 14 deletions(-) create mode 100644 packages/ui/src/pierre/virtualizer.ts diff --git a/bun.lock b/bun.lock index 13aedc3d1d..8c8c638e96 100644 --- a/bun.lock +++ b/bun.lock @@ -513,7 +513,7 @@ "@kobalte/core": "0.13.11", "@octokit/rest": "22.0.0", "@openauthjs/openauth": "0.0.0-20250322224806", - "@pierre/diffs": "1.1.0-beta.9", + "@pierre/diffs": "1.1.0-beta.10", "@playwright/test": "1.51.0", "@solid-primitives/storage": "4.3.3", "@solidjs/meta": "0.29.4", @@ -1409,7 +1409,7 @@ "@petamoriken/float16": ["@petamoriken/float16@3.9.3", "", {}, "sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g=="], - "@pierre/diffs": ["@pierre/diffs@1.1.0-beta.9", "", { "dependencies": { "@shikijs/core": "^3.0.0", "@shikijs/engine-javascript": "^3.0.0", "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-yoDzRnK8L6ylLsLMilOq5MsIIWinGg60yVzOHqVHW0NJkrfCT4vpCqN172linI73Yva57VgpxJkIIKV7xyQPEg=="], + "@pierre/diffs": ["@pierre/diffs@1.1.0-beta.10", "", { "dependencies": { "@shikijs/core": "^3.0.0", "@shikijs/engine-javascript": "^3.0.0", "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-VJz9Nl0mf9dxieH6VaXXV25Fq1wwT/xOWXqkarc9HYywCuw5ZoxHb9Ptw8+58XnTw0ZwwvYmsdfmkrKRZR3q9Q=="], "@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="], diff --git a/package.json b/package.json index f8f1951641..749063a0b9 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "@tsconfig/bun": "1.0.9", "@cloudflare/workers-types": "4.20251008.0", "@openauthjs/openauth": "0.0.0-20250322224806", - "@pierre/diffs": "1.1.0-beta.9", + "@pierre/diffs": "1.1.0-beta.10", "@solid-primitives/storage": "4.3.3", "@tailwindcss/vite": "4.1.11", "diff": "8.0.2", diff --git a/packages/ui/src/components/diff-ssr.tsx b/packages/ui/src/components/diff-ssr.tsx index 9cb132698e..e739afc16d 100644 --- a/packages/ui/src/components/diff-ssr.tsx +++ b/packages/ui/src/components/diff-ssr.tsx @@ -1,8 +1,9 @@ -import { DIFFS_TAG_NAME, FileDiff, type SelectedLineRange } from "@pierre/diffs" +import { DIFFS_TAG_NAME, FileDiff, type SelectedLineRange, VirtualizedFileDiff } from "@pierre/diffs" import { PreloadMultiFileDiffResult } from "@pierre/diffs/ssr" import { createEffect, onCleanup, onMount, Show, splitProps } from "solid-js" import { Dynamic, isServer } from "solid-js/web" import { createDefaultOptions, styleVariables, type DiffProps } from "../pierre" +import { acquireVirtualizer, virtualMetrics } from "../pierre/virtualizer" import { useWorkerPool } from "../context/worker-pool" export type SSRDiffProps = DiffProps & { @@ -24,10 +25,21 @@ export function Diff(props: SSRDiffProps) { const workerPool = useWorkerPool(props.diffStyle) let fileDiffInstance: FileDiff | undefined + let sharedVirtualizer: NonNullable> | undefined const cleanupFunctions: Array<() => void> = [] 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 applyScheme = () => { const scheme = document.documentElement.dataset.colorScheme if (scheme === "dark" || scheme === "light") { @@ -215,14 +227,27 @@ export function Diff(props: SSRDiffProps) { onCleanup(() => monitor.disconnect()) } - fileDiffInstance = new FileDiff( - { - ...createDefaultOptions(props.diffStyle), - ...others, - ...props.preloadedDiff, - }, - workerPool, - ) + const virtualizer = getVirtualizer() + + fileDiffInstance = virtualizer + ? new VirtualizedFileDiff( + { + ...createDefaultOptions(props.diffStyle), + ...others, + ...props.preloadedDiff, + }, + virtualizer, + virtualMetrics, + workerPool, + ) + : new FileDiff( + { + ...createDefaultOptions(props.diffStyle), + ...others, + ...props.preloadedDiff, + }, + workerPool, + ) // @ts-expect-error - fileContainer is private but needed for SSR hydration fileDiffInstance.fileContainer = fileDiffRef fileDiffInstance.hydrate({ @@ -276,6 +301,8 @@ export function Diff(props: SSRDiffProps) { // Clean up FileDiff event handlers and dispose SolidJS components fileDiffInstance?.cleanUp() cleanupFunctions.forEach((dispose) => dispose()) + sharedVirtualizer?.release() + sharedVirtualizer = undefined }) return ( diff --git a/packages/ui/src/components/diff.tsx b/packages/ui/src/components/diff.tsx index 8e4fd64fbe..0966db75e0 100644 --- a/packages/ui/src/components/diff.tsx +++ b/packages/ui/src/components/diff.tsx @@ -1,8 +1,9 @@ import { checksum } from "@opencode-ai/util/encode" -import { FileDiff, type SelectedLineRange } from "@pierre/diffs" +import { FileDiff, type SelectedLineRange, VirtualizedFileDiff } from "@pierre/diffs" import { createMediaQuery } from "@solid-primitives/media" import { createEffect, createMemo, createSignal, onCleanup, splitProps } from "solid-js" import { createDefaultOptions, type DiffProps, styleVariables } from "../pierre" +import { acquireVirtualizer, virtualMetrics } from "../pierre/virtualizer" import { getWorkerPool } from "../pierre/worker" type SelectionSide = "additions" | "deletions" @@ -52,6 +53,7 @@ function findSide(node: Node | null): SelectionSide | undefined { export function Diff(props: DiffProps) { let container!: HTMLDivElement let observer: MutationObserver | undefined + let sharedVirtualizer: NonNullable> | undefined let renderToken = 0 let selectionFrame: number | undefined let dragFrame: number | undefined @@ -92,6 +94,16 @@ export function Diff(props: DiffProps) { const [current, setCurrent] = createSignal | undefined>(undefined) const [rendered, setRendered] = createSignal(0) + const getVirtualizer = () => { + if (sharedVirtualizer) return sharedVirtualizer.virtualizer + + const result = acquireVirtualizer(container) + if (!result) return + + sharedVirtualizer = result + return result.virtualizer + } + const getRoot = () => { const host = container.querySelector("diffs-container") if (!(host instanceof HTMLElement)) return @@ -517,12 +529,15 @@ export function Diff(props: DiffProps) { createEffect(() => { const opts = options() const workerPool = getWorkerPool(props.diffStyle) + const virtualizer = getVirtualizer() const annotations = local.annotations const beforeContents = typeof local.before?.contents === "string" ? local.before.contents : "" const afterContents = typeof local.after?.contents === "string" ? local.after.contents : "" instance?.cleanUp() - instance = new FileDiff(opts, workerPool) + instance = virtualizer + ? new VirtualizedFileDiff(opts, virtualizer, virtualMetrics, workerPool) + : new FileDiff(opts, workerPool) setCurrent(instance) container.innerHTML = "" @@ -609,6 +624,8 @@ export function Diff(props: DiffProps) { instance?.cleanUp() setCurrent(undefined) + sharedVirtualizer?.release() + sharedVirtualizer = undefined }) return
diff --git a/packages/ui/src/pierre/index.ts b/packages/ui/src/pierre/index.ts index 137e3c3286..a7caa615fa 100644 --- a/packages/ui/src/pierre/index.ts +++ b/packages/ui/src/pierre/index.ts @@ -153,6 +153,7 @@ export function createDefaultOptions(style: FileDiffOptions["diffStyle"]) lineHoverHighlight: "both", disableBackground: false, expansionLineCount: 20, + hunkSeparators: "line-info-basic", lineDiffType: style === "split" ? "word-alt" : "none", maxLineDiffLength: 1000, maxLineLengthForHighlighting: 1000, diff --git a/packages/ui/src/pierre/virtualizer.ts b/packages/ui/src/pierre/virtualizer.ts new file mode 100644 index 0000000000..4957afc125 --- /dev/null +++ b/packages/ui/src/pierre/virtualizer.ts @@ -0,0 +1,76 @@ +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 target(container: HTMLElement): Target | undefined { + if (typeof document === "undefined") return + + const root = container.closest("[data-component='session-review']") + if (root instanceof HTMLElement) { + const content = root.querySelector("[data-slot='session-review-container']") + 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) + }, + } +}