diff.tsx 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. import { type FileContents, FileDiff, type DiffLineAnnotation, FileDiffOptions } from "@pierre/precision-diffs"
  2. import { PreloadMultiFileDiffResult } from "@pierre/precision-diffs/ssr"
  3. import { ComponentProps, createEffect, onCleanup, onMount, splitProps } from "solid-js"
  4. import { isServer } from "solid-js/web"
  5. export type DiffProps<T = {}> = FileDiffOptions<T> & {
  6. preloadedDiff?: PreloadMultiFileDiffResult<T>
  7. before: FileContents
  8. after: FileContents
  9. annotations?: DiffLineAnnotation<T>[]
  10. class?: string
  11. classList?: ComponentProps<"div">["classList"]
  12. }
  13. // interface ThreadMetadata {
  14. // threadId: string
  15. // }
  16. export function Diff<T>(props: DiffProps<T>) {
  17. let container!: HTMLDivElement
  18. let fileDiffRef!: HTMLElement
  19. const [local, others] = splitProps(props, ["before", "after", "class", "classList", "annotations"])
  20. let fileDiffInstance: FileDiff<T> | undefined
  21. const cleanupFunctions: Array<() => void> = []
  22. const defaultOptions: FileDiffOptions<T> = {
  23. theme: "OpenCode",
  24. themeType: "system",
  25. disableLineNumbers: false,
  26. overflow: "wrap",
  27. diffStyle: "unified",
  28. diffIndicators: "bars",
  29. disableBackground: false,
  30. expansionLineCount: 20,
  31. lineDiffType: props.diffStyle === "split" ? "word-alt" : "none",
  32. maxLineDiffLength: 1000,
  33. maxLineLengthForHighlighting: 1000,
  34. disableFileHeader: true,
  35. }
  36. createEffect(() => {
  37. if (props.preloadedDiff) return
  38. container.innerHTML = ""
  39. if (!fileDiffInstance) {
  40. fileDiffInstance = new FileDiff<T>({
  41. ...defaultOptions,
  42. ...others,
  43. ...(props.preloadedDiff ?? {}),
  44. })
  45. }
  46. fileDiffInstance.render({
  47. oldFile: local.before,
  48. newFile: local.after,
  49. lineAnnotations: local.annotations,
  50. containerWrapper: container,
  51. })
  52. })
  53. onMount(() => {
  54. if (isServer) return
  55. fileDiffInstance = new FileDiff<T>({
  56. ...defaultOptions,
  57. // You can optionally pass a render function for rendering out line
  58. // annotations. Just return the dom node to render
  59. // renderAnnotation(annotation: DiffLineAnnotation<T>): HTMLElement {
  60. // // Despite the diff itself being rendered in the shadow dom,
  61. // // annotations are inserted via the web components 'slots' api and you
  62. // // can use all your normal normal css and styling for them
  63. // const element = document.createElement("div")
  64. // element.innerText = annotation.metadata.threadId
  65. // return element
  66. // },
  67. ...others,
  68. ...(props.preloadedDiff ?? {}),
  69. })
  70. // @ts-expect-error - fileContainer is private but needed for SSR hydration
  71. fileDiffInstance.fileContainer = fileDiffRef
  72. // Hydrate annotation slots with interactive SolidJS components
  73. // if (props.annotations.length > 0 && props.renderAnnotation != null) {
  74. // for (const annotation of props.annotations) {
  75. // const slotName = `annotation-${annotation.side}-${annotation.lineNumber}`;
  76. // const slotElement = fileDiffRef.querySelector(
  77. // `[slot="${slotName}"]`
  78. // ) as HTMLElement;
  79. //
  80. // if (slotElement != null) {
  81. // // Clear the static server-rendered content from the slot
  82. // slotElement.innerHTML = '';
  83. //
  84. // // Mount a fresh SolidJS component into this slot using render().
  85. // // This enables full SolidJS reactivity (signals, effects, etc.)
  86. // const dispose = render(
  87. // () => props.renderAnnotation!(annotation),
  88. // slotElement
  89. // );
  90. // cleanupFunctions.push(dispose);
  91. // }
  92. // }
  93. // }
  94. })
  95. onCleanup(() => {
  96. // Clean up FileDiff event handlers and dispose SolidJS components
  97. fileDiffInstance?.cleanUp()
  98. cleanupFunctions.forEach((dispose) => dispose())
  99. })
  100. return (
  101. <div
  102. data-component="diff"
  103. style={{
  104. "--pjs-font-family": "var(--font-family-mono)",
  105. "--pjs-font-size": "var(--font-size-small)",
  106. "--pjs-line-height": "24px",
  107. "--pjs-tab-size": 2,
  108. "--pjs-font-features": "var(--font-family-mono--font-feature-settings)",
  109. "--pjs-header-font-family": "var(--font-family-sans)",
  110. "--pjs-gap-block": 0,
  111. "--pjs-min-number-column-width": "4ch",
  112. }}
  113. ref={container}
  114. >
  115. <file-diff ref={fileDiffRef} id="ssr-diff">
  116. {/* Only render on server - client hydrates the existing content */}
  117. {isServer && props.preloadedDiff && (
  118. <>
  119. {/* Declarative Shadow DOM - browsers parse this and create a shadow root */}
  120. <template shadowrootmode="open">
  121. <div innerHTML={props.preloadedDiff!.prerenderedHTML} />
  122. </template>
  123. {/* Render static annotation slots on server.
  124. Client will clear these and mount interactive components. */}
  125. {/* <For each={props.annotations}> */}
  126. {/* {(annotation) => { */}
  127. {/* const slotName = `annotation-${annotation.side}-${annotation.lineNumber}` */}
  128. {/* return <div slot={slotName}>{props.renderAnnotation?.(annotation)}</div> */}
  129. {/* }} */}
  130. {/* </For> */}
  131. </>
  132. )}
  133. </file-diff>
  134. </div>
  135. )
  136. }