diff --git a/bun.lock b/bun.lock index 6f399805b828..0131a8e0c09b 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.0.2", + "@pierre/diffs": "1.1.0-beta.13", "@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.0.2", "", { "dependencies": { "@shikijs/core": "^3.0.0", "@shikijs/engine-javascript": "3.19.0", "@shikijs/transformers": "3.19.0", "diff": "8.0.2", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "3.19.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-RkFSDD5X/U+8QjyilPViYGJfmJNWXR17zTL8zw48+DcVC1Ujbh6I1edyuRnFfgRzpft05x2DSCkz2cjoIAxPvQ=="], + "@pierre/diffs": ["@pierre/diffs@1.1.0-beta.13", "", { "dependencies": { "@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-D35rxDu5V7XHX5aVGU6PF12GhscL+I+9QYgxK/i3h0d2XSirAxDdVNm49aYwlOhgmdvL0NbS1IHxPswVB5yJvw=="], "@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="], @@ -4387,13 +4387,9 @@ "@oslojs/jwt/@oslojs/encoding": ["@oslojs/encoding@0.4.1", "", {}, "sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q=="], - "@pierre/diffs/@shikijs/core": ["@shikijs/core@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-f2ED7HYV4JEk827mtMDwe/yQ25pRiXZmtHjWF8uzZKuKiEsJR7Ce1nuQ+HhV9FzDcbIo4ObBCD9GPTzNuy9S1g=="], + "@pierre/diffs/@shikijs/transformers": ["@shikijs/transformers@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/types": "3.20.0" } }, "sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g=="], - "@pierre/diffs/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.19.0", "", { "dependencies": { "@shikijs/types": "3.19.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-ZfWJNm2VMhKkQIKT9qXbs76RRcT0SF/CAvEz0+RkpUDAoDaCx0uFdCGzSRiD9gSlhm6AHkjdieOBJMaO2eC1rQ=="], - - "@pierre/diffs/@shikijs/transformers": ["@shikijs/transformers@3.19.0", "", { "dependencies": { "@shikijs/core": "3.19.0", "@shikijs/types": "3.19.0" } }, "sha512-e6vwrsyw+wx4OkcrDbL+FVCxwx8jgKiCoXzakVur++mIWVcgpzIi8vxf4/b4dVTYrV/nUx5RjinMf4tq8YV8Fw=="], - - "@pierre/diffs/shiki": ["shiki@3.19.0", "", { "dependencies": { "@shikijs/core": "3.19.0", "@shikijs/engine-javascript": "3.19.0", "@shikijs/engine-oniguruma": "3.19.0", "@shikijs/langs": "3.19.0", "@shikijs/themes": "3.19.0", "@shikijs/types": "3.19.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-77VJr3OR/VUZzPiStyRhADmO2jApMM0V2b1qf0RpfWya8Zr1PeZev5AEpPGAAKWdiYUtcZGBE4F5QvJml1PvWA=="], + "@pierre/diffs/diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="], "@poppinss/dumper/supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], @@ -4973,23 +4969,9 @@ "@opentui/solid/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@pierre/diffs/@shikijs/core/@shikijs/types": ["@shikijs/types@3.20.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw=="], - - "@pierre/diffs/@shikijs/engine-javascript/@shikijs/types": ["@shikijs/types@3.19.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-Z2hdeEQlzuntf/BZpFG8a+Fsw9UVXdML7w0o3TgSXV3yNESGon+bs9ITkQb3Ki7zxoXOOu5oJWqZ2uto06V9iQ=="], - - "@pierre/diffs/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.19.0", "", { "dependencies": { "@shikijs/types": "3.19.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-L7SrRibU7ZoYi1/TrZsJOFAnnHyLTE1SwHG1yNWjZIVCqjOEmCSuK2ZO9thnRbJG6TOkPp+Z963JmpCNw5nzvA=="], - - "@pierre/diffs/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.19.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-Z2hdeEQlzuntf/BZpFG8a+Fsw9UVXdML7w0o3TgSXV3yNESGon+bs9ITkQb3Ki7zxoXOOu5oJWqZ2uto06V9iQ=="], - - "@pierre/diffs/shiki/@shikijs/core": ["@shikijs/core@3.19.0", "", { "dependencies": { "@shikijs/types": "3.19.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-L7SrRibU7ZoYi1/TrZsJOFAnnHyLTE1SwHG1yNWjZIVCqjOEmCSuK2ZO9thnRbJG6TOkPp+Z963JmpCNw5nzvA=="], - - "@pierre/diffs/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.19.0", "", { "dependencies": { "@shikijs/types": "3.19.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-1hRxtYIJfJSZeM5ivbUXv9hcJP3PWRo5prG/V2sWwiubUKTa+7P62d2qxCW8jiVFX4pgRHhnHNp+qeR7Xl+6kg=="], - - "@pierre/diffs/shiki/@shikijs/langs": ["@shikijs/langs@3.19.0", "", { "dependencies": { "@shikijs/types": "3.19.0" } }, "sha512-dBMFzzg1QiXqCVQ5ONc0z2ebyoi5BKz+MtfByLm0o5/nbUu3Iz8uaTCa5uzGiscQKm7lVShfZHU1+OG3t5hgwg=="], - - "@pierre/diffs/shiki/@shikijs/themes": ["@shikijs/themes@3.19.0", "", { "dependencies": { "@shikijs/types": "3.19.0" } }, "sha512-H36qw+oh91Y0s6OlFfdSuQ0Ld+5CgB/VE6gNPK+Hk4VRbVG/XQgkjnt4KzfnnoO6tZPtKJKHPjwebOCfjd6F8A=="], + "@pierre/diffs/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-f2ED7HYV4JEk827mtMDwe/yQ25pRiXZmtHjWF8uzZKuKiEsJR7Ce1nuQ+HhV9FzDcbIo4ObBCD9GPTzNuy9S1g=="], - "@pierre/diffs/shiki/@shikijs/types": ["@shikijs/types@3.19.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-Z2hdeEQlzuntf/BZpFG8a+Fsw9UVXdML7w0o3TgSXV3yNESGon+bs9ITkQb3Ki7zxoXOOu5oJWqZ2uto06V9iQ=="], + "@pierre/diffs/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.20.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw=="], "@slack/web-api/form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], diff --git a/package.json b/package.json index ae790e0a5e15..c396905d458f 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.0.2", + "@pierre/diffs": "1.1.0-beta.13", "@solid-primitives/storage": "4.3.3", "@tailwindcss/vite": "4.1.11", "diff": "8.0.2", diff --git a/packages/app/src/pages/session/review-tab.tsx b/packages/app/src/pages/session/review-tab.tsx index a4232dd74e39..72518c68e407 100644 --- a/packages/app/src/pages/session/review-tab.tsx +++ b/packages/app/src/pages/session/review-tab.tsx @@ -139,7 +139,7 @@ export function SessionReviewTab(props: SessionReviewTabProps) { open={props.view().review.open()} onOpenChange={props.view().review.setOpen} classes={{ - root: props.classes?.root ?? "pb-40", + root: props.classes?.root ?? "pb-6", header: props.classes?.header ?? "px-6", container: props.classes?.container ?? "px-6", }} diff --git a/packages/ui/src/components/code.tsx b/packages/ui/src/components/code.tsx index 4e7c82d78837..2fe0e035279d 100644 --- a/packages/ui/src/components/code.tsx +++ b/packages/ui/src/components/code.tsx @@ -318,7 +318,7 @@ export function Code(props: CodeProps) { const needle = query.toLowerCase() const out: Range[] = [] - const cols = Array.from(root.querySelectorAll("[data-column-content]")).filter( + const cols = Array.from(root.querySelectorAll("[data-content] [data-line], [data-column-content]")).filter( (node): node is HTMLElement => node instanceof HTMLElement, ) @@ -537,17 +537,28 @@ export function Code(props: CodeProps) { node.removeAttribute("data-comment-selected") } + const annotations = Array.from(root.querySelectorAll("[data-line-annotation]")).filter( + (node): node is HTMLElement => node instanceof HTMLElement, + ) + for (const range of ranges) { const start = Math.max(1, Math.min(range.start, range.end)) const end = Math.max(range.start, range.end) for (let line = start; line <= end; line++) { - const nodes = Array.from(root.querySelectorAll(`[data-line="${line}"]`)) + const nodes = Array.from(root.querySelectorAll(`[data-line="${line}"], [data-column-number="${line}"]`)) for (const node of nodes) { if (!(node instanceof HTMLElement)) continue node.setAttribute("data-comment-selected", "") } } + + for (const annotation of annotations) { + const line = parseInt(annotation.dataset.lineAnnotation?.split(",")[1] ?? "", 10) + if (Number.isNaN(line)) continue + if (line < start || line > end) continue + annotation.setAttribute("data-comment-selected", "") + } } } diff --git a/packages/ui/src/components/diff-ssr.tsx b/packages/ui/src/components/diff-ssr.tsx index 602e59a2f57b..e739afc16d83 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") { @@ -70,10 +82,10 @@ export function Diff(props: SSRDiffProps) { const root = getRoot() if (!root) return - const diffs = root.querySelector("[data-diffs]") + const diffs = root.querySelector("[data-diff]") if (!(diffs instanceof HTMLElement)) return - const split = diffs.dataset.type === "split" + const split = diffs.dataset.diffType === "split" const start = rowIndex(root, split, range.start, range.side) const end = rowIndex(root, split, range.end, range.endSide ?? range.side) @@ -132,15 +144,19 @@ export function Diff(props: SSRDiffProps) { node.removeAttribute("data-comment-selected") } - const diffs = root.querySelector("[data-diffs]") + const diffs = root.querySelector("[data-diff]") if (!(diffs instanceof HTMLElement)) return - const split = diffs.dataset.type === "split" + const split = diffs.dataset.diffType === "split" + + const rows = Array.from(diffs.querySelectorAll("[data-line-index]")).filter( + (node): node is HTMLElement => node instanceof HTMLElement, + ) + if (rows.length === 0) return - const code = Array.from(diffs.querySelectorAll("[data-code]")).filter( + const annotations = Array.from(diffs.querySelectorAll("[data-line-annotation]")).filter( (node): node is HTMLElement => node instanceof HTMLElement, ) - if (code.length === 0) return const lineIndex = (element: HTMLElement) => { const raw = element.dataset.lineIndex @@ -183,19 +199,18 @@ export function Diff(props: SSRDiffProps) { const first = Math.min(start, end) const last = Math.max(start, end) - for (const block of code) { - for (const element of Array.from(block.children)) { - if (!(element instanceof HTMLElement)) continue - const idx = lineIndex(element) - if (idx === undefined) continue - if (idx > last) break - if (idx < first) continue - element.setAttribute("data-comment-selected", "") - const next = element.nextSibling - if (next instanceof HTMLElement && next.hasAttribute("data-line-annotation")) { - next.setAttribute("data-comment-selected", "") - } - } + for (const row of rows) { + const idx = lineIndex(row) + if (idx === undefined) continue + if (idx < first || idx > last) continue + row.setAttribute("data-comment-selected", "") + } + + for (const annotation of annotations) { + const idx = parseInt(annotation.dataset.lineAnnotation?.split(",")[1] ?? "", 10) + if (Number.isNaN(idx)) continue + if (idx < first || idx > last) continue + annotation.setAttribute("data-comment-selected", "") } } } @@ -212,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({ @@ -273,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 21dada535034..0966db75e036 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 @@ -147,10 +159,10 @@ export function Diff(props: DiffProps) { const root = getRoot() if (!root) return - const diffs = root.querySelector("[data-diffs]") + const diffs = root.querySelector("[data-diff]") if (!(diffs instanceof HTMLElement)) return - const split = diffs.dataset.type === "split" + const split = diffs.dataset.diffType === "split" const start = rowIndex(root, split, range.start, range.side) const end = rowIndex(root, split, range.end, range.endSide ?? range.side) @@ -261,15 +273,19 @@ export function Diff(props: DiffProps) { node.removeAttribute("data-comment-selected") } - const diffs = root.querySelector("[data-diffs]") + const diffs = root.querySelector("[data-diff]") if (!(diffs instanceof HTMLElement)) return - const split = diffs.dataset.type === "split" + const split = diffs.dataset.diffType === "split" + + const rows = Array.from(diffs.querySelectorAll("[data-line-index]")).filter( + (node): node is HTMLElement => node instanceof HTMLElement, + ) + if (rows.length === 0) return - const code = Array.from(diffs.querySelectorAll("[data-code]")).filter( + const annotations = Array.from(diffs.querySelectorAll("[data-line-annotation]")).filter( (node): node is HTMLElement => node instanceof HTMLElement, ) - if (code.length === 0) return for (const range of ranges) { const start = rowIndex(root, split, range.start, range.side) @@ -285,19 +301,18 @@ export function Diff(props: DiffProps) { const first = Math.min(start, end) const last = Math.max(start, end) - for (const block of code) { - for (const element of Array.from(block.children)) { - if (!(element instanceof HTMLElement)) continue - const idx = lineIndex(split, element) - if (idx === undefined) continue - if (idx > last) break - if (idx < first) continue - element.setAttribute("data-comment-selected", "") - const next = element.nextSibling - if (next instanceof HTMLElement && next.hasAttribute("data-line-annotation")) { - next.setAttribute("data-comment-selected", "") - } - } + for (const row of rows) { + const idx = lineIndex(split, row) + if (idx === undefined) continue + if (idx < first || idx > last) continue + row.setAttribute("data-comment-selected", "") + } + + for (const annotation of annotations) { + const idx = parseInt(annotation.dataset.lineAnnotation?.split(",")[1] ?? "", 10) + if (Number.isNaN(idx)) continue + if (idx < first || idx > last) continue + annotation.setAttribute("data-comment-selected", "") } } } @@ -514,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 = "" @@ -606,6 +624,8 @@ export function Diff(props: DiffProps) { instance?.cleanUp() setCurrent(undefined) + sharedVirtualizer?.release() + sharedVirtualizer = undefined }) return
diff --git a/packages/ui/src/context/marked.tsx b/packages/ui/src/context/marked.tsx index 0c6d58b93532..c5ff3c767f6a 100644 --- a/packages/ui/src/context/marked.tsx +++ b/packages/ui/src/context/marked.tsx @@ -10,7 +10,7 @@ registerCustomTheme("OpenCode", () => { return Promise.resolve({ name: "OpenCode", colors: { - "editor.background": "transparent", + "editor.background": "var(--color-background-stronger)", "editor.foreground": "var(--text-base)", "gitDecoration.addedResourceForeground": "var(--syntax-diff-add)", "gitDecoration.deletedResourceForeground": "var(--syntax-diff-delete)", diff --git a/packages/ui/src/pierre/index.ts b/packages/ui/src/pierre/index.ts index f6446f3cc859..dc9d857bf871 100644 --- a/packages/ui/src/pierre/index.ts +++ b/packages/ui/src/pierre/index.ts @@ -13,7 +13,7 @@ export type DiffProps = FileDiffOptions & { } const unsafeCSS = ` -[data-diffs] { +[data-diff] { --diffs-bg: light-dark(var(--diffs-light-bg), var(--diffs-dark-bg)); --diffs-bg-buffer: var(--diffs-bg-buffer-override, light-dark( color-mix(in lab, var(--diffs-bg) 92%, var(--diffs-mixer)), color-mix(in lab, var(--diffs-bg) 92%, var(--diffs-mixer)))); --diffs-bg-hover: var(--diffs-bg-hover-override, light-dark( color-mix(in lab, var(--diffs-bg) 97%, var(--diffs-mixer)), color-mix(in lab, var(--diffs-bg) 91%, var(--diffs-mixer)))); @@ -44,7 +44,7 @@ const unsafeCSS = ` --diffs-bg-selection-text: rgb(from var(--surface-warning-strong) r g b / 0.2); } -:host([data-color-scheme='dark']) [data-diffs] { +:host([data-color-scheme='dark']) [data-diff] { --diffs-selection-number-fg: #fdfbfb; --diffs-bg-selection: var(--diffs-bg-selection-override, rgb(from var(--solaris-dark-6) r g b / 0.65)); --diffs-bg-selection-number: var( @@ -53,7 +53,7 @@ const unsafeCSS = ` ); } -[data-diffs] ::selection { +[data-diff] ::selection { background-color: var(--diffs-bg-selection-text); } @@ -65,61 +65,48 @@ const unsafeCSS = ` background-color: rgb(from var(--surface-warning-strong) r g b / 0.55); } -[data-diffs] [data-comment-selected]:not([data-selected-line]) [data-column-content] { +[data-diff] [data-line][data-comment-selected]:not([data-selected-line]) { box-shadow: inset 0 0 0 9999px var(--diffs-bg-selection); } -[data-diffs] [data-comment-selected]:not([data-selected-line]) [data-column-number] { +[data-diff] [data-column-number][data-comment-selected]:not([data-selected-line]) { box-shadow: inset 0 0 0 9999px var(--diffs-bg-selection-number); color: var(--diffs-selection-number-fg); } -[data-diffs] [data-selected-line] { +[data-diff] [data-line-annotation][data-comment-selected]:not([data-selected-line]) [data-annotation-content] { + box-shadow: inset 0 0 0 9999px var(--diffs-bg-selection); +} + +[data-diff] [data-line][data-selected-line] { background-color: var(--diffs-bg-selection); box-shadow: inset 2px 0 0 var(--diffs-selection-border); } -[data-diffs] [data-selected-line] [data-column-number] { +[data-diff] [data-column-number][data-selected-line] { background-color: var(--diffs-bg-selection-number); color: var(--diffs-selection-number-fg); } -[data-diffs] [data-line-type='context'][data-selected-line] [data-column-number], -[data-diffs] [data-line-type='context-expanded'][data-selected-line] [data-column-number], -[data-diffs] [data-line-type='change-addition'][data-selected-line] [data-column-number], -[data-diffs] [data-line-type='change-deletion'][data-selected-line] [data-column-number] { +[data-diff] [data-column-number][data-line-type='context'][data-selected-line], +[data-diff] [data-column-number][data-line-type='context-expanded'][data-selected-line], +[data-diff] [data-column-number][data-line-type='change-addition'][data-selected-line], +[data-diff] [data-column-number][data-line-type='change-deletion'][data-selected-line] { color: var(--diffs-selection-number-fg); } /* The deletion word-diff emphasis is stronger than additions; soften it while selected so the selection highlight reads consistently. */ -[data-diffs] [data-line-type='change-deletion'][data-selected-line] { +[data-diff] [data-line][data-line-type='change-deletion'][data-selected-line] { --diffs-bg-deletion-emphasis: light-dark( rgb(from var(--diffs-deletion-base) r g b / 0.07), rgb(from var(--diffs-deletion-base) r g b / 0.1) ); } -[data-diffs-header], -[data-diffs] { - [data-separator-wrapper] { - margin: 0 !important; - border-radius: 0 !important; - } - [data-expand-button] { - width: 6.5ch !important; - height: 24px !important; - justify-content: end !important; - padding-left: 3ch !important; - padding-inline: 1ch !important; - } - [data-separator-multi-button] { - grid-template-rows: 10px 10px !important; - [data-expand-button] { - height: 12px !important; - } - } - [data-separator-content] { - height: 24px !important; +[data-diff-header], +[data-diff] { + [data-separator] { + height: 24px; } [data-column-number] { background-color: var(--background-stronger); @@ -146,28 +133,15 @@ export function createDefaultOptions(style: FileDiffOptions["diffStyle"]) overflow: "wrap", diffStyle: style ?? "unified", diffIndicators: "bars", + lineHoverHighlight: "both", disableBackground: false, expansionLineCount: 20, + hunkSeparators: "line-info-basic", lineDiffType: style === "split" ? "word-alt" : "none", maxLineDiffLength: 1000, maxLineLengthForHighlighting: 1000, disableFileHeader: true, unsafeCSS, - // hunkSeparators(hunkData: HunkData) { - // const fragment = document.createDocumentFragment() - // const numCol = document.createElement("div") - // numCol.innerHTML = ` ` - // numCol.dataset["slot"] = "diff-hunk-separator-line-number" - // fragment.appendChild(numCol) - // const contentCol = document.createElement("div") - // contentCol.dataset["slot"] = "diff-hunk-separator-content" - // const span = document.createElement("span") - // span.dataset["slot"] = "diff-hunk-separator-content-span" - // span.textContent = `${hunkData.lines} unmodified lines` - // contentCol.appendChild(span) - // fragment.appendChild(contentCol) - // return fragment - // }, } as const } diff --git a/packages/ui/src/pierre/virtualizer.ts b/packages/ui/src/pierre/virtualizer.ts new file mode 100644 index 000000000000..4957afc12552 --- /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) + }, + } +} diff --git a/packages/ui/src/pierre/worker.ts b/packages/ui/src/pierre/worker.ts index 0d117c3683ff..1993ad7aa6fc 100644 --- a/packages/ui/src/pierre/worker.ts +++ b/packages/ui/src/pierre/worker.ts @@ -21,6 +21,7 @@ function createPool(lineDiffType: "none" | "word-alt") { { theme: "OpenCode", lineDiffType, + preferredHighlighter: "shiki-wasm", }, )