import React from "react";
import { diffLines } from "diff";
import {
CODE_GUTTER_COLOR,
DIFF_ADDED,
DIFF_HEADER_COLOR,
DIFF_MARKER_COLOR,
DIFF_REMOVED,
} from "../theme.ts";
import { HighlightedCode } from "../markdown/highlight-code.tsx";
import { countLines, numWidth, fileExtLanguage, extractTrim } from "../str.ts";
import { Span } from "paintcannon-react";
import { TerminalFlex } from "./terminal-flex.tsx";
export function DiffRenderer({
oldText,
newText,
fileContents,
filepath,
}: {
oldText: string;
newText: string;
fileContents: string;
filepath: string;
}) {
try {
const language = fileExtLanguage(filepath);
const diff = diffLines(oldText, newText);
const diffWithChanged: Array<
| (typeof diff)[number]
| {
added: false;
removed: false;
changed: true;
oldValue: string;
newValue: string;
}
> = [];
for (let i = 0; i < diff.length; i++) {
const curr = diff[i];
const prev =
diffWithChanged.length === 0 ? null : diffWithChanged[diffWithChanged.length - 1];
if (prev == null) {
diffWithChanged.push(curr);
continue;
}
if (prev.removed && curr.added) {
diffWithChanged.pop();
diffWithChanged.push({
added: false,
removed: false,
changed: true,
oldValue: prev.value,
newValue: curr.value,
});
continue;
}
diffWithChanged.push(curr);
}
const startLine = getStartLine(fileContents, oldText);
const oldLineCounter = buildLineCounter(startLine);
const newLineCounter = buildLineCounter(startLine);
const maxOldLines = startLine + countLines(oldText);
const maxNewLines = startLine + countLines(newText);
const lineNrWidth = Math.max(numWidth(maxOldLines), numWidth(maxNewLines));
return (
Old
New
{diffWithChanged.map((part, index) => {
if (part.added) {
return (
);
}
if (part.removed) {
return (
);
}
if ("changed" in part) {
return (
);
}
return (
);
})}
);
} catch (e) {
return null;
}
}
function getStartLine(file: string, search: string) {
const index = file.indexOf(search);
if (index < 0) throw new Error("Impossible diff rendering; search string isn't present in file");
let line = 1;
for (let i = 0; i < index; i++) {
const char = file[i];
if (char === "\n") line++;
}
return line;
}
type LineCounter = {
getLine: () => number;
incrementLine: () => number;
getStartLine: () => number;
};
function buildLineCounter(startLine: number): LineCounter {
let curr = startLine;
return {
getLine: () => curr,
incrementLine: () => curr++,
getStartLine: () => startLine,
};
}
function DiffSet({
oldValue,
newValue,
newAdded,
oldRemoved,
language,
oldLineCounter,
newLineCounter,
lineNrWidth,
oldText,
newText,
}: {
oldValue?: string;
newValue?: string;
oldRemoved?: boolean;
newAdded?: boolean;
oldLineCounter: LineCounter;
newLineCounter: LineCounter;
lineNrWidth: number;
language: string;
oldText: string;
newText: string;
}) {
const gutterWidth = 3 + lineNrWidth;
return (
{oldRemoved ? - : {" "}}
{newAdded ? + : {" "}}
);
}
function LineSegments({
value,
language,
gutterColor,
gutterWidth,
lineNrWidth,
lineCounter,
children,
originalText,
}: {
value: string | undefined;
language: string;
gutterColor: string;
gutterWidth: number;
lineNrWidth: number;
lineCounter: LineCounter;
children: React.ReactNode;
originalText: string;
}) {
// Frustratingly, the diffLines function adds newlines at the end of diffs; remove them
const valueLines = value == null ? [] : value.split("\n");
if (valueLines.length > 0 && valueLines[valueLines.length - 1] === "") {
valueLines.pop();
}
if (valueLines.length === 0) {
return (
{children}
);
}
return (
{valueLines.map((line, index) => {
const lineNumber = lineCounter.incrementLine();
return (
{lineNumber}
{children}
);
})}
);
}
// Calculate visual width of whitespace (spaces = 1, tabs = 2)
function whitespaceWidth(ws: string): number {
let width = 0;
for (const char of ws) {
if (char === "\t") width += 2;
else width += 1;
}
return width;
}
function MaybeHighlighted({
line,
language,
originalText,
currentLine,
startLine,
}: {
line: string | undefined;
language: string;
originalText: string;
currentLine: number;
startLine: number;
}) {
// Annoyingly, the diffs only include the start of the line from the first character; not the
// start of the actual line including whitespace. This means we need to find the actual, original
// line and parse out the whitespace from it rather than using the lines unchanged.
// Depending on the language, the syntax highlighter might strip some amount of whitespace (my
// life is pain) and therefore we definitely need the whitespace parsed out, rather than just
// passing the string as-is to the highlighter.
const matchedLine = (() => {
if (line == null) return line;
// Calculate relative line number within the original text
const relativeLineNum = currentLine - startLine;
// Get the original line using the relative line number to find the correct whitespace
const originalLines = originalText.split("\n");
if (relativeLineNum >= originalLines.length) {
throw new Error(
`Impossible relative line count: ${relativeLineNum} vs original ${originalLines.length}`,
);
}
const originalLine = originalLines[relativeLineNum];
return extractTrim(originalLine);
})();
if (language == "txt") {
if (matchedLine) {
return (
{matchedLine[1]}
{matchedLine[2]}
);
}
return ;
}
if (matchedLine) {
return (
);
}
return ;
}