summaryrefslogtreecommitdiffhomepage
path: root/packages/ui/src/components/diff.tsx
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-02-26 18:23:04 -0600
committerGitHub <[email protected]>2026-02-26 18:23:04 -0600
commitfc52e4b2d3a41efde772e6de8fb2e01f27821701 (patch)
treecf23af294a00a10e55f230232585344c111f0bb9 /packages/ui/src/components/diff.tsx
parent9a6bfeb782766099d4ce3a98bb9e7b4e79f8bfe6 (diff)
downloadopencode-fc52e4b2d3a41efde772e6de8fb2e01f27821701.tar.gz
opencode-fc52e4b2d3a41efde772e6de8fb2e01f27821701.zip
feat(app): better diff/code comments (#14621)
Co-authored-by: adamelmore <[email protected]> Co-authored-by: David Hill <[email protected]>
Diffstat (limited to 'packages/ui/src/components/diff.tsx')
-rw-r--r--packages/ui/src/components/diff.tsx652
1 files changed, 0 insertions, 652 deletions
diff --git a/packages/ui/src/components/diff.tsx b/packages/ui/src/components/diff.tsx
deleted file mode 100644
index 0002232b0..000000000
--- a/packages/ui/src/components/diff.tsx
+++ /dev/null
@@ -1,652 +0,0 @@
-import { sampledChecksum } from "@opencode-ai/util/encode"
-import { FileDiff, type FileDiffOptions, 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"
-
-function findElement(node: Node | null): HTMLElement | undefined {
- if (!node) return
- if (node instanceof HTMLElement) return node
- return node.parentElement ?? undefined
-}
-
-function findLineNumber(node: Node | null): number | undefined {
- const element = findElement(node)
- if (!element) return
-
- const line = element.closest("[data-line], [data-alt-line]")
- if (!(line instanceof HTMLElement)) return
-
- const value = (() => {
- const primary = parseInt(line.dataset.line ?? "", 10)
- if (!Number.isNaN(primary)) return primary
-
- const alt = parseInt(line.dataset.altLine ?? "", 10)
- if (!Number.isNaN(alt)) return alt
- })()
-
- return value
-}
-
-function findSide(node: Node | null): SelectionSide | undefined {
- const element = findElement(node)
- if (!element) return
-
- const line = element.closest("[data-line], [data-alt-line]")
- if (line instanceof HTMLElement) {
- const type = line.dataset.lineType
- if (type === "change-deletion") return "deletions"
- if (type === "change-addition" || type === "change-additions") return "additions"
- }
-
- const code = element.closest("[data-code]")
- if (!(code instanceof HTMLElement)) return
-
- if (code.hasAttribute("data-deletions")) return "deletions"
- return "additions"
-}
-
-export function Diff<T>(props: DiffProps<T>) {
- let container!: HTMLDivElement
- let observer: MutationObserver | undefined
- let sharedVirtualizer: NonNullable<ReturnType<typeof acquireVirtualizer>> | undefined
- let renderToken = 0
- let selectionFrame: number | undefined
- let dragFrame: number | undefined
- let dragStart: number | undefined
- let dragEnd: number | undefined
- let dragSide: SelectionSide | undefined
- let dragEndSide: SelectionSide | undefined
- let dragMoved = false
- let lastSelection: SelectedLineRange | null = null
- let pendingSelectionEnd = false
-
- const [local, others] = splitProps(props, [
- "before",
- "after",
- "class",
- "classList",
- "annotations",
- "selectedLines",
- "commentedLines",
- "onRendered",
- ])
-
- const mobile = createMediaQuery("(max-width: 640px)")
-
- const large = createMemo(() => {
- const before = typeof local.before?.contents === "string" ? local.before.contents : ""
- const after = typeof local.after?.contents === "string" ? local.after.contents : ""
- return Math.max(before.length, after.length) > 500_000
- })
-
- const largeOptions = {
- lineDiffType: "none",
- maxLineDiffLength: 0,
- tokenizeMaxLineLength: 1,
- } satisfies Pick<FileDiffOptions<T>, "lineDiffType" | "maxLineDiffLength" | "tokenizeMaxLineLength">
-
- const options = createMemo<FileDiffOptions<T>>(() => {
- const base = {
- ...createDefaultOptions(props.diffStyle),
- ...others,
- }
-
- const perf = large() ? { ...base, ...largeOptions } : base
- if (!mobile()) return perf
-
- return {
- ...perf,
- disableLineNumbers: true,
- }
- })
-
- let instance: FileDiff<T> | undefined
- const [current, setCurrent] = createSignal<FileDiff<T> | 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
-
- const root = host.shadowRoot
- if (!root) return
-
- return root
- }
-
- const applyScheme = () => {
- const host = container.querySelector("diffs-container")
- if (!(host instanceof HTMLElement)) return
-
- const scheme = document.documentElement.dataset.colorScheme
- if (scheme === "dark" || scheme === "light") {
- host.dataset.colorScheme = scheme
- return
- }
-
- host.removeAttribute("data-color-scheme")
- }
-
- const lineIndex = (split: boolean, element: HTMLElement) => {
- const raw = element.dataset.lineIndex
- if (!raw) return
- const values = raw
- .split(",")
- .map((value) => parseInt(value, 10))
- .filter((value) => !Number.isNaN(value))
- if (values.length === 0) return
- if (!split) return values[0]
- if (values.length === 2) return values[1]
- return values[0]
- }
-
- const rowIndex = (root: ShadowRoot, split: boolean, line: number, side: SelectionSide | undefined) => {
- const nodes = Array.from(root.querySelectorAll(`[data-line="${line}"], [data-alt-line="${line}"]`)).filter(
- (node): node is HTMLElement => node instanceof HTMLElement,
- )
- if (nodes.length === 0) return
-
- const targetSide = side ?? "additions"
-
- for (const node of nodes) {
- if (findSide(node) === targetSide) return lineIndex(split, node)
- if (parseInt(node.dataset.altLine ?? "", 10) === line) return lineIndex(split, node)
- }
- }
-
- const fixSelection = (range: SelectedLineRange | null) => {
- if (!range) return range
- const root = getRoot()
- if (!root) return
-
- const diffs = root.querySelector("[data-diff]")
- if (!(diffs instanceof HTMLElement)) return
-
- 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)
- if (start === undefined || end === undefined) {
- if (root.querySelector("[data-line], [data-alt-line]") == null) return
- return null
- }
- if (start <= end) return range
-
- const side = range.endSide ?? range.side
- const swapped: SelectedLineRange = {
- start: range.end,
- end: range.start,
- }
-
- if (side) swapped.side = side
- if (range.endSide && range.side) swapped.endSide = range.side
-
- return swapped
- }
-
- const notifyRendered = () => {
- observer?.disconnect()
- observer = undefined
- renderToken++
-
- const token = renderToken
- let settle = 0
-
- const isReady = (root: ShadowRoot) => root.querySelector("[data-line]") != null
-
- const notify = () => {
- if (token !== renderToken) return
-
- observer?.disconnect()
- observer = undefined
- requestAnimationFrame(() => {
- if (token !== renderToken) return
- setSelectedLines(lastSelection)
- local.onRendered?.()
- })
- }
-
- const schedule = () => {
- settle++
- const current = settle
-
- requestAnimationFrame(() => {
- if (token !== renderToken) return
- if (current !== settle) return
-
- requestAnimationFrame(() => {
- if (token !== renderToken) return
- if (current !== settle) return
-
- notify()
- })
- })
- }
-
- const observeRoot = (root: ShadowRoot) => {
- observer?.disconnect()
- observer = new MutationObserver(() => {
- if (token !== renderToken) return
- if (!isReady(root)) return
-
- schedule()
- })
-
- observer.observe(root, { childList: true, subtree: true })
-
- if (!isReady(root)) return
- schedule()
- }
-
- const root = getRoot()
- if (typeof MutationObserver === "undefined") {
- if (!root || !isReady(root)) return
- setSelectedLines(lastSelection)
- local.onRendered?.()
- return
- }
-
- if (root) {
- observeRoot(root)
- return
- }
-
- observer = new MutationObserver(() => {
- if (token !== renderToken) return
-
- const root = getRoot()
- if (!root) return
-
- observeRoot(root)
- })
-
- observer.observe(container, { childList: true, subtree: true })
- }
-
- const applyCommentedLines = (ranges: SelectedLineRange[]) => {
- const root = getRoot()
- if (!root) return
-
- const existing = Array.from(root.querySelectorAll("[data-comment-selected]"))
- for (const node of existing) {
- if (!(node instanceof HTMLElement)) continue
- node.removeAttribute("data-comment-selected")
- }
-
- const diffs = root.querySelector("[data-diff]")
- if (!(diffs instanceof HTMLElement)) return
-
- 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 annotations = Array.from(diffs.querySelectorAll("[data-line-annotation]")).filter(
- (node): node is HTMLElement => node instanceof HTMLElement,
- )
-
- for (const range of ranges) {
- const start = rowIndex(root, split, range.start, range.side)
- if (start === undefined) continue
-
- const end = (() => {
- const same = range.end === range.start && (range.endSide == null || range.endSide === range.side)
- if (same) return start
- return rowIndex(root, split, range.end, range.endSide ?? range.side)
- })()
- if (end === undefined) continue
-
- const first = Math.min(start, end)
- const last = Math.max(start, end)
-
- 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", "")
- }
- }
- }
-
- const setSelectedLines = (range: SelectedLineRange | null) => {
- const active = current()
- if (!active) return
-
- const fixed = fixSelection(range)
- if (fixed === undefined) {
- lastSelection = range
- return
- }
-
- lastSelection = fixed
- active.setSelectedLines(fixed)
- }
-
- const updateSelection = () => {
- const root = getRoot()
- if (!root) return
-
- const selection =
- (root as unknown as { getSelection?: () => Selection | null }).getSelection?.() ?? window.getSelection()
- if (!selection || selection.isCollapsed) return
-
- const domRange =
- (
- selection as unknown as {
- getComposedRanges?: (options?: { shadowRoots?: ShadowRoot[] }) => Range[]
- }
- ).getComposedRanges?.({ shadowRoots: [root] })?.[0] ??
- (selection.rangeCount > 0 ? selection.getRangeAt(0) : undefined)
-
- const startNode = domRange?.startContainer ?? selection.anchorNode
- const endNode = domRange?.endContainer ?? selection.focusNode
- if (!startNode || !endNode) return
-
- if (!root.contains(startNode) || !root.contains(endNode)) return
-
- const start = findLineNumber(startNode)
- const end = findLineNumber(endNode)
- if (start === undefined || end === undefined) return
-
- const startSide = findSide(startNode)
- const endSide = findSide(endNode)
- const side = startSide ?? endSide
-
- const selected: SelectedLineRange = {
- start,
- end,
- }
-
- if (side) selected.side = side
- if (endSide && side && endSide !== side) selected.endSide = endSide
-
- setSelectedLines(selected)
- }
-
- const scheduleSelectionUpdate = () => {
- if (selectionFrame !== undefined) return
-
- selectionFrame = requestAnimationFrame(() => {
- selectionFrame = undefined
- updateSelection()
-
- if (!pendingSelectionEnd) return
- pendingSelectionEnd = false
- props.onLineSelectionEnd?.(lastSelection)
- })
- }
-
- const updateDragSelection = () => {
- if (dragStart === undefined || dragEnd === undefined) return
-
- const selected: SelectedLineRange = {
- start: dragStart,
- end: dragEnd,
- }
-
- if (dragSide) selected.side = dragSide
- if (dragEndSide && dragSide && dragEndSide !== dragSide) selected.endSide = dragEndSide
-
- setSelectedLines(selected)
- }
-
- const scheduleDragUpdate = () => {
- if (dragFrame !== undefined) return
-
- dragFrame = requestAnimationFrame(() => {
- dragFrame = undefined
- updateDragSelection()
- })
- }
-
- const lineFromMouseEvent = (event: MouseEvent) => {
- const path = event.composedPath()
-
- let numberColumn = false
- let line: number | undefined
- let side: SelectionSide | undefined
-
- for (const item of path) {
- if (!(item instanceof HTMLElement)) continue
-
- numberColumn = numberColumn || item.dataset.columnNumber != null
-
- if (side === undefined) {
- const type = item.dataset.lineType
- if (type === "change-deletion") side = "deletions"
- if (type === "change-addition" || type === "change-additions") side = "additions"
- }
-
- if (side === undefined && item.dataset.code != null) {
- side = item.hasAttribute("data-deletions") ? "deletions" : "additions"
- }
-
- if (line === undefined) {
- const primary = item.dataset.line ? parseInt(item.dataset.line, 10) : Number.NaN
- if (!Number.isNaN(primary)) {
- line = primary
- } else {
- const alt = item.dataset.altLine ? parseInt(item.dataset.altLine, 10) : Number.NaN
- if (!Number.isNaN(alt)) line = alt
- }
- }
-
- if (numberColumn && line !== undefined && side !== undefined) break
- }
-
- return { line, numberColumn, side }
- }
-
- const handleMouseDown = (event: MouseEvent) => {
- if (props.enableLineSelection !== true) return
- if (event.button !== 0) return
-
- const { line, numberColumn, side } = lineFromMouseEvent(event)
- if (numberColumn) return
- if (line === undefined) return
-
- dragStart = line
- dragEnd = line
- dragSide = side
- dragEndSide = side
- dragMoved = false
- }
-
- const handleMouseMove = (event: MouseEvent) => {
- if (props.enableLineSelection !== true) return
- if (dragStart === undefined) return
-
- if ((event.buttons & 1) === 0) {
- dragStart = undefined
- dragEnd = undefined
- dragSide = undefined
- dragEndSide = undefined
- dragMoved = false
- return
- }
-
- const { line, side } = lineFromMouseEvent(event)
- if (line === undefined) return
-
- dragEnd = line
- dragEndSide = side
- dragMoved = true
- scheduleDragUpdate()
- }
-
- const handleMouseUp = () => {
- if (props.enableLineSelection !== true) return
- if (dragStart === undefined) return
-
- if (!dragMoved) {
- pendingSelectionEnd = false
- const line = dragStart
- const selected: SelectedLineRange = {
- start: line,
- end: line,
- }
- if (dragSide) selected.side = dragSide
- setSelectedLines(selected)
- props.onLineSelectionEnd?.(lastSelection)
- dragStart = undefined
- dragEnd = undefined
- dragSide = undefined
- dragEndSide = undefined
- dragMoved = false
- return
- }
-
- pendingSelectionEnd = true
- scheduleDragUpdate()
- scheduleSelectionUpdate()
-
- dragStart = undefined
- dragEnd = undefined
- dragSide = undefined
- dragEndSide = undefined
- dragMoved = false
- }
-
- const handleSelectionChange = () => {
- if (props.enableLineSelection !== true) return
- if (dragStart === undefined) return
-
- const selection = window.getSelection()
- if (!selection || selection.isCollapsed) return
-
- scheduleSelectionUpdate()
- }
-
- createEffect(() => {
- const opts = options()
- const workerPool = large() ? getWorkerPool("unified") : 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 : ""
-
- const cacheKey = (contents: string) => {
- if (!large()) return sampledChecksum(contents, contents.length)
- return sampledChecksum(contents)
- }
-
- instance?.cleanUp()
- instance = virtualizer
- ? new VirtualizedFileDiff<T>(opts, virtualizer, virtualMetrics, workerPool)
- : new FileDiff<T>(opts, workerPool)
- setCurrent(instance)
-
- container.innerHTML = ""
- instance.render({
- oldFile: {
- ...local.before,
- contents: beforeContents,
- cacheKey: cacheKey(beforeContents),
- },
- newFile: {
- ...local.after,
- contents: afterContents,
- cacheKey: cacheKey(afterContents),
- },
- lineAnnotations: annotations,
- containerWrapper: container,
- })
-
- applyScheme()
-
- setRendered((value) => value + 1)
- notifyRendered()
- })
-
- createEffect(() => {
- if (typeof document === "undefined") return
- if (typeof MutationObserver === "undefined") return
-
- const root = document.documentElement
- const monitor = new MutationObserver(() => applyScheme())
- monitor.observe(root, { attributes: true, attributeFilter: ["data-color-scheme"] })
- applyScheme()
-
- onCleanup(() => monitor.disconnect())
- })
-
- createEffect(() => {
- rendered()
- const ranges = local.commentedLines ?? []
- requestAnimationFrame(() => applyCommentedLines(ranges))
- })
-
- createEffect(() => {
- const selected = local.selectedLines ?? null
- setSelectedLines(selected)
- })
-
- createEffect(() => {
- if (props.enableLineSelection !== true) return
-
- container.addEventListener("mousedown", handleMouseDown)
- container.addEventListener("mousemove", handleMouseMove)
- window.addEventListener("mouseup", handleMouseUp)
- document.addEventListener("selectionchange", handleSelectionChange)
-
- onCleanup(() => {
- container.removeEventListener("mousedown", handleMouseDown)
- container.removeEventListener("mousemove", handleMouseMove)
- window.removeEventListener("mouseup", handleMouseUp)
- document.removeEventListener("selectionchange", handleSelectionChange)
- })
- })
-
- onCleanup(() => {
- observer?.disconnect()
-
- if (selectionFrame !== undefined) {
- cancelAnimationFrame(selectionFrame)
- selectionFrame = undefined
- }
-
- if (dragFrame !== undefined) {
- cancelAnimationFrame(dragFrame)
- dragFrame = undefined
- }
-
- dragStart = undefined
- dragEnd = undefined
- dragSide = undefined
- dragEndSide = undefined
- dragMoved = false
- lastSelection = null
- pendingSelectionEnd = false
-
- instance?.cleanUp()
- setCurrent(undefined)
- sharedVirtualizer?.release()
- sharedVirtualizer = undefined
- })
-
- return <div data-component="diff" style={styleVariables} ref={container} />
-}