opencode/packages/ui/src/components/diff.tsx

156 lines
3.8 KiB
TypeScript
Raw Normal View History

2026-01-04 09:57:55 +08:00
import { checksum } from "@opencode-ai/util/encode"
2025-12-16 00:56:03 +08:00
import { FileDiff } from "@pierre/diffs"
import { createMediaQuery } from "@solid-primitives/media"
2025-12-08 20:24:19 +08:00
import { createEffect, createMemo, onCleanup, splitProps } from "solid-js"
import { createDefaultOptions, type DiffProps, styleVariables } from "../pierre"
2025-12-31 23:23:24 +08:00
import { getWorkerPool } from "../pierre/worker"
2025-10-24 21:26:17 +08:00
2025-10-25 00:23:32 +08:00
export function Diff<T>(props: DiffProps<T>) {
let container!: HTMLDivElement
2026-01-05 05:40:25 +08:00
let observer: MutationObserver | undefined
let renderToken = 0
const [local, others] = splitProps(props, ["before", "after", "class", "classList", "annotations", "onRendered"])
2025-10-24 21:26:17 +08:00
const mobile = createMediaQuery("(max-width: 640px)")
const options = createMemo(() => {
const opts = {
...createDefaultOptions(props.diffStyle),
...others,
}
if (!mobile()) return opts
return {
...opts,
disableLineNumbers: true,
}
})
2026-01-04 09:57:55 +08:00
let instance: FileDiff<T> | undefined
2025-12-08 20:24:19 +08:00
2026-01-05 05:40:25 +08:00
const getRoot = () => {
const host = container.querySelector("diffs-container")
if (!(host instanceof HTMLElement)) return
const root = host.shadowRoot
if (!root) return
return root
}
const notifyRendered = () => {
if (!local.onRendered) return
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
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
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 })
}
2025-12-08 20:24:19 +08:00
createEffect(() => {
2026-01-04 09:57:55 +08:00
const opts = options()
const workerPool = getWorkerPool(props.diffStyle)
const annotations = local.annotations
const beforeContents = typeof local.before?.contents === "string" ? local.before.contents : ""
const afterContents = typeof local.after?.contents === "string" ? local.after.contents : ""
2026-01-04 09:57:55 +08:00
instance?.cleanUp()
instance = new FileDiff<T>(opts, workerPool)
2025-12-08 20:24:19 +08:00
container.innerHTML = ""
2026-01-04 09:57:55 +08:00
instance.render({
oldFile: {
...local.before,
contents: beforeContents,
cacheKey: checksum(beforeContents),
2026-01-04 09:57:55 +08:00
},
newFile: {
...local.after,
contents: afterContents,
cacheKey: checksum(afterContents),
2026-01-04 09:57:55 +08:00
},
lineAnnotations: annotations,
2025-11-25 21:40:11 +08:00
containerWrapper: container,
})
2026-01-05 05:40:25 +08:00
notifyRendered()
2026-01-04 09:57:55 +08:00
})
2025-11-25 21:40:11 +08:00
2026-01-04 09:57:55 +08:00
onCleanup(() => {
2026-01-05 05:40:25 +08:00
observer?.disconnect()
2026-01-04 09:57:55 +08:00
instance?.cleanUp()
2025-10-24 21:26:17 +08:00
})
return <div data-component="diff" style={styleVariables} ref={container} />
2025-10-24 21:26:17 +08:00
}