diff-changes.tsx 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. import type { FileDiff } from "@opencode-ai/sdk"
  2. import { createMemo, For, Match, Show, Switch } from "solid-js"
  3. export function DiffChanges(props: { diff: FileDiff | FileDiff[]; variant?: "default" | "bars" }) {
  4. const variant = () => props.variant ?? "default"
  5. const additions = createMemo(() =>
  6. Array.isArray(props.diff)
  7. ? props.diff.reduce((acc, diff) => acc + (diff.additions ?? 0), 0)
  8. : props.diff.additions,
  9. )
  10. const deletions = createMemo(() =>
  11. Array.isArray(props.diff)
  12. ? props.diff.reduce((acc, diff) => acc + (diff.deletions ?? 0), 0)
  13. : props.diff.deletions,
  14. )
  15. const total = createMemo(() => (additions() ?? 0) + (deletions() ?? 0))
  16. const countLines = (text: string) => {
  17. if (!text) return 0
  18. return text.split("\n").length
  19. }
  20. const totalBeforeLines = createMemo(() => {
  21. if (!Array.isArray(props.diff)) return countLines(props.diff.before || "")
  22. return props.diff.reduce((acc, diff) => acc + countLines(diff.before || ""), 0)
  23. })
  24. const blockCounts = createMemo(() => {
  25. const TOTAL_BLOCKS = 5
  26. const adds = additions() ?? 0
  27. const dels = deletions() ?? 0
  28. if (adds === 0 && dels === 0) {
  29. return { added: 0, deleted: 0, neutral: TOTAL_BLOCKS }
  30. }
  31. const total = adds + dels
  32. if (total < 5) {
  33. const added = adds > 0 ? 1 : 0
  34. const deleted = dels > 0 ? 1 : 0
  35. const neutral = TOTAL_BLOCKS - added - deleted
  36. return { added, deleted, neutral }
  37. }
  38. const ratio = adds > dels ? adds / dels : dels / adds
  39. let BLOCKS_FOR_COLORS = TOTAL_BLOCKS
  40. if (total < 20) {
  41. BLOCKS_FOR_COLORS = TOTAL_BLOCKS - 1
  42. } else if (ratio < 4) {
  43. BLOCKS_FOR_COLORS = TOTAL_BLOCKS - 1
  44. }
  45. const percentAdded = adds / total
  46. const percentDeleted = dels / total
  47. const added_raw = percentAdded * BLOCKS_FOR_COLORS
  48. const deleted_raw = percentDeleted * BLOCKS_FOR_COLORS
  49. let added = adds > 0 ? Math.max(1, Math.round(added_raw)) : 0
  50. let deleted = dels > 0 ? Math.max(1, Math.round(deleted_raw)) : 0
  51. // Cap bars based on actual change magnitude
  52. if (adds > 0 && adds <= 5) added = Math.min(added, 1)
  53. if (adds > 5 && adds <= 10) added = Math.min(added, 2)
  54. if (dels > 0 && dels <= 5) deleted = Math.min(deleted, 1)
  55. if (dels > 5 && dels <= 10) deleted = Math.min(deleted, 2)
  56. let total_allocated = added + deleted
  57. if (total_allocated > BLOCKS_FOR_COLORS) {
  58. if (added_raw > deleted_raw) {
  59. added = BLOCKS_FOR_COLORS - deleted
  60. } else {
  61. deleted = BLOCKS_FOR_COLORS - added
  62. }
  63. total_allocated = added + deleted
  64. }
  65. const neutral = Math.max(0, TOTAL_BLOCKS - total_allocated)
  66. return { added, deleted, neutral }
  67. })
  68. const ADD_COLOR = "var(--icon-diff-add-base)"
  69. const DELETE_COLOR = "var(--icon-diff-delete-base)"
  70. const NEUTRAL_COLOR = "var(--icon-weak-base)"
  71. const visibleBlocks = createMemo(() => {
  72. const counts = blockCounts()
  73. const blocks = [
  74. ...Array(counts.added).fill(ADD_COLOR),
  75. ...Array(counts.deleted).fill(DELETE_COLOR),
  76. ...Array(counts.neutral).fill(NEUTRAL_COLOR),
  77. ]
  78. return blocks.slice(0, 5)
  79. })
  80. return (
  81. <Show when={variant() === "default" ? total() > 0 : true}>
  82. <div data-component="diff-changes" data-variant={variant()}>
  83. <Switch>
  84. <Match when={variant() === "bars"}>
  85. <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 12" fill="none">
  86. <g>
  87. <For each={visibleBlocks()}>
  88. {(color, i) => <rect x={i() * 4} width="2" height="12" rx="1" fill={color} />}
  89. </For>
  90. </g>
  91. </svg>
  92. </Match>
  93. <Match when={variant() === "default"}>
  94. <span data-slot="additions">{`+${additions()}`}</span>
  95. <span data-slot="deletions">{`-${deletions()}`}</span>
  96. </Match>
  97. </Switch>
  98. </div>
  99. </Show>
  100. )
  101. }