File size: 3,949 Bytes
3201ca6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 | import React from "react";
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import { cn } from "#/utils/utils";
type DiffRow = { type: "add" | "del" | "ctx"; text: string };
/** Lines of unchanged context kept on each side of a change. */
const CONTEXT = 3;
/** Max rendered rows before the view is truncated. */
const MAX_ROWS = 300;
/** Above this `old x new` line product we skip the O(n*m) LCS and show a
* wholesale replacement instead, so a full-file rewrite can't blow up. */
const LCS_CELL_BUDGET = 250_000;
const lcsDiff = (a: string[], b: string[]): DiffRow[] => {
if (a.length * b.length > LCS_CELL_BUDGET) {
return [
...a.map((text): DiffRow => ({ type: "del", text })),
...b.map((text): DiffRow => ({ type: "add", text })),
];
}
const n = a.length;
const m = b.length;
const lcs: number[][] = Array.from({ length: n + 1 }, () =>
new Array<number>(m + 1).fill(0),
);
for (let i = n - 1; i >= 0; i -= 1) {
for (let j = m - 1; j >= 0; j -= 1) {
lcs[i][j] =
a[i] === b[j]
? lcs[i + 1][j + 1] + 1
: Math.max(lcs[i + 1][j], lcs[i][j + 1]);
}
}
const rows: DiffRow[] = [];
let i = 0;
let j = 0;
while (i < n && j < m) {
if (a[i] === b[j]) {
rows.push({ type: "ctx", text: a[i] });
i += 1;
j += 1;
} else if (lcs[i + 1][j] >= lcs[i][j + 1]) {
rows.push({ type: "del", text: a[i] });
i += 1;
} else {
rows.push({ type: "add", text: b[j] });
j += 1;
}
}
while (i < n) {
rows.push({ type: "del", text: a[i] });
i += 1;
}
while (j < m) {
rows.push({ type: "add", text: b[j] });
j += 1;
}
return rows;
};
/**
* Computes a unified line diff. Common leading/trailing lines are trimmed (with
* a few kept as context) so a localized edit inside a large file stays small
* and cheap to diff.
*/
const computeLineDiff = (oldText: string, newText: string): DiffRow[] => {
const a = oldText.split("\n");
const b = newText.split("\n");
let lo = 0;
while (lo < a.length && lo < b.length && a[lo] === b[lo]) lo += 1;
let hiA = a.length;
let hiB = b.length;
while (hiA > lo && hiB > lo && a[hiA - 1] === b[hiB - 1]) {
hiA -= 1;
hiB -= 1;
}
const lead = a.slice(Math.max(0, lo - CONTEXT), lo);
const trail = a.slice(hiA, Math.min(a.length, hiA + CONTEXT));
return [
...lead.map((text): DiffRow => ({ type: "ctx", text })),
...lcsDiff(a.slice(lo, hiA), b.slice(lo, hiB)),
...trail.map((text): DiffRow => ({ type: "ctx", text })),
];
};
const ROW_STYLE: Record<DiffRow["type"], string> = {
add: "bg-status-success-bg text-status-success-text",
del: "bg-status-fail-bg text-status-fail-text",
ctx: "text-muted",
};
const ROW_PREFIX: Record<DiffRow["type"], string> = {
add: "+ ",
del: "- ",
ctx: " ",
};
/**
* Unified before/after line diff for file edits.
*/
export function DiffView({
oldText,
newText,
}: {
oldText: string;
newText: string;
}) {
const { t } = useTranslation("openhands");
const rows = computeLineDiff(oldText, newText);
const truncated = rows.length > MAX_ROWS;
const shown = truncated ? rows.slice(0, MAX_ROWS) : rows;
return (
<div className="flex flex-col gap-1">
<div className="overflow-auto rounded-lg border border-surface-raised font-mono text-xs">
{shown.map((row, index) => (
<div
// Diff rows have no stable id and lines may repeat, so the index
// within this render is the only available key.
key={`${index}-${row.type}`}
className={cn("whitespace-pre-wrap px-2", ROW_STYLE[row.type])}
>
{`${ROW_PREFIX[row.type]}${row.text}`}
</div>
))}
</div>
{truncated && (
<span className="text-xs text-muted">
{t(I18nKey.COMMON$TRUNCATED)}
</span>
)}
</div>
);
}
|