summaryrefslogtreecommitdiffhomepage
path: root/packages/web/src/components/share/content-diff.tsx
diff options
context:
space:
mode:
authorDax <[email protected]>2025-07-07 15:53:43 -0400
committerGitHub <[email protected]>2025-07-07 15:53:43 -0400
commitf884766445bbf1fbce11f1db4bc6174e72d9baa5 (patch)
treee7d9e7b3d8efc8bed249f9d29e2d8fb838a275f2 /packages/web/src/components/share/content-diff.tsx
parent76b2e4539cb97bae5812ed2d832ce49d02e70c64 (diff)
downloadopencode-f884766445bbf1fbce11f1db4bc6174e72d9baa5.tar.gz
opencode-f884766445bbf1fbce11f1db4bc6174e72d9baa5.zip
v2 message format and upgrade to ai sdk v5 (#743)
Co-authored-by: GitHub Action <[email protected]> Co-authored-by: Liang-Shih Lin <[email protected]> Co-authored-by: Dominik Engelhardt <[email protected]> Co-authored-by: Jay V <[email protected]> Co-authored-by: adamdottv <[email protected]>
Diffstat (limited to 'packages/web/src/components/share/content-diff.tsx')
-rw-r--r--packages/web/src/components/share/content-diff.tsx231
1 files changed, 231 insertions, 0 deletions
diff --git a/packages/web/src/components/share/content-diff.tsx b/packages/web/src/components/share/content-diff.tsx
new file mode 100644
index 000000000..894145c34
--- /dev/null
+++ b/packages/web/src/components/share/content-diff.tsx
@@ -0,0 +1,231 @@
+import { type Component, createMemo } from "solid-js"
+import { parsePatch } from "diff"
+import { ContentCode } from "./content-code"
+import styles from "./content-diff.module.css"
+
+type DiffRow = {
+ left: string
+ right: string
+ type: "added" | "removed" | "unchanged" | "modified"
+}
+
+interface Props {
+ diff: string
+ lang?: string
+}
+
+export function ContentDiff(props: 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 = k < removals.length
+ const hasRight = k < additions.length
+
+ 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 if (hasRight) {
+ // Pure addition - only create if we actually have content
+ 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
+ })
+
+ const mobileRows = createMemo(() => {
+ const mobileBlocks: { type: "removed" | "added" | "unchanged"; lines: string[] }[] = []
+ const currentRows = rows()
+
+ let i = 0
+ while (i < currentRows.length) {
+ const removedLines: string[] = []
+ const addedLines: string[] = []
+
+ // Collect consecutive modified/removed/added rows
+ while (
+ i < currentRows.length &&
+ (currentRows[i].type === "modified" || currentRows[i].type === "removed" || currentRows[i].type === "added")
+ ) {
+ const row = currentRows[i]
+ if (row.left && (row.type === "removed" || row.type === "modified")) {
+ removedLines.push(row.left)
+ }
+ if (row.right && (row.type === "added" || row.type === "modified")) {
+ addedLines.push(row.right)
+ }
+ i++
+ }
+
+ // Add grouped blocks
+ if (removedLines.length > 0) {
+ mobileBlocks.push({ type: "removed", lines: removedLines })
+ }
+ if (addedLines.length > 0) {
+ mobileBlocks.push({ type: "added", lines: addedLines })
+ }
+
+ // Add unchanged rows as-is
+ if (i < currentRows.length && currentRows[i].type === "unchanged") {
+ mobileBlocks.push({
+ type: "unchanged",
+ lines: [currentRows[i].left],
+ })
+ i++
+ }
+ }
+
+ return mobileBlocks
+ })
+
+ return (
+ <div class={styles.root}>
+ <div data-component="desktop">
+ {rows().map((r) => (
+ <div data-component="diff-row" data-type={r.type}>
+ <div data-slot="before" data-diff-type={r.type === "removed" || r.type === "modified" ? "removed" : ""}>
+ <ContentCode code={r.left} flush lang={props.lang} />
+ </div>
+ <div data-slot="after" data-diff-type={r.type === "added" || r.type === "modified" ? "added" : ""}>
+ <ContentCode code={r.right} lang={props.lang} flush />
+ </div>
+ </div>
+ ))}
+ </div>
+
+ <div data-component="mobile">
+ {mobileRows().map((block) => (
+ <div data-component="diff-block" data-type={block.type}>
+ {block.lines.map((line) => (
+ <ContentCode
+ code={line}
+ lang={props.lang}
+ data-section="cell"
+ data-diff-type={block.type === "removed" ? "removed" : block.type === "added" ? "added" : ""}
+ />
+ ))}
+ </div>
+ ))}
+ </div>
+ </div>
+ )
+}
+
+// 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`