diff.tsx 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. import { checksum } from "@opencode-ai/util/encode"
  2. import { FileDiff } from "@pierre/diffs"
  3. import { createMediaQuery } from "@solid-primitives/media"
  4. import { createEffect, createMemo, onCleanup, splitProps } from "solid-js"
  5. import { createDefaultOptions, type DiffProps, styleVariables } from "../pierre"
  6. import { getWorkerPool } from "../pierre/worker"
  7. export function Diff<T>(props: DiffProps<T>) {
  8. let container!: HTMLDivElement
  9. let observer: MutationObserver | undefined
  10. let renderToken = 0
  11. const [local, others] = splitProps(props, ["before", "after", "class", "classList", "annotations", "onRendered"])
  12. const mobile = createMediaQuery("(max-width: 640px)")
  13. const options = createMemo(() => {
  14. const opts = {
  15. ...createDefaultOptions(props.diffStyle),
  16. ...others,
  17. }
  18. if (!mobile()) return opts
  19. return {
  20. ...opts,
  21. disableLineNumbers: true,
  22. }
  23. })
  24. let instance: FileDiff<T> | undefined
  25. const getRoot = () => {
  26. const host = container.querySelector("diffs-container")
  27. if (!(host instanceof HTMLElement)) return
  28. const root = host.shadowRoot
  29. if (!root) return
  30. return root
  31. }
  32. const notifyRendered = () => {
  33. if (!local.onRendered) return
  34. observer?.disconnect()
  35. observer = undefined
  36. renderToken++
  37. const token = renderToken
  38. let settle = 0
  39. const isReady = (root: ShadowRoot) => root.querySelector("[data-line]") != null
  40. const notify = () => {
  41. if (token !== renderToken) return
  42. observer?.disconnect()
  43. observer = undefined
  44. requestAnimationFrame(() => {
  45. if (token !== renderToken) return
  46. local.onRendered?.()
  47. })
  48. }
  49. const schedule = () => {
  50. settle++
  51. const current = settle
  52. requestAnimationFrame(() => {
  53. if (token !== renderToken) return
  54. if (current !== settle) return
  55. requestAnimationFrame(() => {
  56. if (token !== renderToken) return
  57. if (current !== settle) return
  58. notify()
  59. })
  60. })
  61. }
  62. const observeRoot = (root: ShadowRoot) => {
  63. observer?.disconnect()
  64. observer = new MutationObserver(() => {
  65. if (token !== renderToken) return
  66. if (!isReady(root)) return
  67. schedule()
  68. })
  69. observer.observe(root, { childList: true, subtree: true })
  70. if (!isReady(root)) return
  71. schedule()
  72. }
  73. const root = getRoot()
  74. if (typeof MutationObserver === "undefined") {
  75. if (!root || !isReady(root)) return
  76. local.onRendered()
  77. return
  78. }
  79. if (root) {
  80. observeRoot(root)
  81. return
  82. }
  83. observer = new MutationObserver(() => {
  84. if (token !== renderToken) return
  85. const root = getRoot()
  86. if (!root) return
  87. observeRoot(root)
  88. })
  89. observer.observe(container, { childList: true, subtree: true })
  90. }
  91. createEffect(() => {
  92. const opts = options()
  93. const workerPool = getWorkerPool(props.diffStyle)
  94. const annotations = local.annotations
  95. const beforeContents = typeof local.before?.contents === "string" ? local.before.contents : ""
  96. const afterContents = typeof local.after?.contents === "string" ? local.after.contents : ""
  97. instance?.cleanUp()
  98. instance = new FileDiff<T>(opts, workerPool)
  99. container.innerHTML = ""
  100. instance.render({
  101. oldFile: {
  102. ...local.before,
  103. contents: beforeContents,
  104. cacheKey: checksum(beforeContents),
  105. },
  106. newFile: {
  107. ...local.after,
  108. contents: afterContents,
  109. cacheKey: checksum(afterContents),
  110. },
  111. lineAnnotations: annotations,
  112. containerWrapper: container,
  113. })
  114. notifyRendered()
  115. })
  116. onCleanup(() => {
  117. observer?.disconnect()
  118. instance?.cleanUp()
  119. })
  120. return <div data-component="diff" style={styleVariables} ref={container} />
  121. }