Files
anomalyco_opencode/packages/web/src/components/DiffView.tsx
T

196 lines
5.3 KiB
TypeScript

import { type Component, createMemo } from "solid-js"
import { parsePatch } from "diff"
import CodeBlock from "./CodeBlock"
import styles from "./diffview.module.css"
type DiffRow = {
left: string
right: string
type: "added" | "removed" | "unchanged" | "modified"
}
interface DiffViewProps {
diff: string
lang?: string
class?: string
}
const DiffView: Component<DiffViewProps> = (props) => {
const rows = createMemo(() => {
const diffRows: DiffRow[] = []
try {
const patches = parsePatch(props.diff)
for (const patch of patches) {
for (const hunk of patch.hunks) {
const lines = hunk.lines
let i = 0
while (i < lines.length) {
const line = lines[i]
const content = line.slice(1)
const prefix = line[0]
if (prefix === '-') {
// Look ahead for consecutive additions to pair with removals
const removals: string[] = [content]
let j = i + 1
// Collect all consecutive removals
while (j < lines.length && lines[j][0] === '-') {
removals.push(lines[j].slice(1))
j++
}
// Collect all consecutive additions that follow
const additions: string[] = []
while (j < lines.length && lines[j][0] === '+') {
additions.push(lines[j].slice(1))
j++
}
// Pair removals with additions
const maxLength = Math.max(removals.length, additions.length)
for (let k = 0; k < maxLength; k++) {
const hasLeft = !!removals[k]
const hasRight = !!additions[k]
if (hasLeft && hasRight) {
// Replacement - left is removed, right is added
diffRows.push({
left: removals[k],
right: additions[k],
type: "modified"
})
} else if (hasLeft) {
// Pure removal
diffRows.push({
left: removals[k],
right: "",
type: "removed"
})
} else {
// Pure addition
diffRows.push({
left: "",
right: additions[k],
type: "added"
})
}
}
i = j
} else if (prefix === '+') {
// Standalone addition (not paired with removal)
diffRows.push({
left: "",
right: content,
type: "added"
})
i++
} else if (prefix === ' ') {
diffRows.push({
left: content,
right: content,
type: "unchanged"
})
i++
} else {
i++
}
}
}
}
} catch (error) {
console.error("Failed to parse patch:", error)
return []
}
return diffRows
})
return (
<div class={`${styles.diff} ${props.class ?? ""}`}>
{rows().map((r) => (
<div class={styles.row}>
<div class={styles.beforeColumn}>
<CodeBlock
code={r.left}
lang={props.lang}
data-section="cell"
data-diff-type={r.type === "removed" || r.type === "modified" ? "removed" : ""}
data-display-mobile={r.type === "added" && !r.left ? "false" : undefined}
/>
{(r.type === "added" || r.type === "modified") && r.right !== undefined && (
<CodeBlock
code={r.right}
lang={props.lang}
data-section="cell"
data-diff-type="added"
data-display-mobile="true"
/>
)}
</div>
<div class={styles.afterColumn}>
<CodeBlock
code={r.right}
lang={props.lang}
data-section="cell"
data-diff-type={r.type === "added" || r.type === "modified" ? "added" : ""}
/>
</div>
</div>
))}
</div>
)
}
export default DiffView
// String to test diff viewer with
const testDiff = `--- combined_before.txt 2025-06-24 16:38:08
+++ combined_after.txt 2025-06-24 16:38:12
@@ -1,21 +1,25 @@
unchanged line
-deleted line
-old content
+added line
+new content
-removed empty line below
+added empty line above
- tab indented
-trailing spaces
-very long line that will definitely wrap in most editors and cause potential alignment issues when displayed in a two column diff view
-unicode content: 🚀 ✨ 中文
-mixed content with tabs and spaces
+ space indented
+no trailing spaces
+short line
+very long replacement line that will also wrap and test how the diff viewer handles long line additions after short line removals
+different unicode: 🎉 💻 日本語
+normalized content with consistent spacing
+newline to content
-content to remove
-whitespace only:
-multiple
-consecutive
-deletions
-single deletion
+
+single addition
+first addition
+second addition
+third addition
line before addition
+first added line
+
+third added line
line after addition
final unchanged line`