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(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 = { 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 = { 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 (
{shown.map((row, index) => (
{`${ROW_PREFIX[row.type]}${row.text}`}
))}
{truncated && ( {t(I18nKey.COMMON$TRUNCATED)} )}
); }