import React from "react";
import { Box, Text } from "ink";
import { diffLines } from "diff";
import { DIFF_ADDED, DIFF_REMOVED, CODE_GUTTER_COLOR } from "../theme.ts";
import { HighlightedCode } from "../markdown/highlight-code.tsx";
import { readFileSync } from "fs";
import { countLines, numWidth, fileExtLanguage, extractTrim } from "../str.ts";
export function DiffRenderer({ oldText, newText, filepath }: {
oldText: string,
newText: string,
filepath: string,
}) {
try {
const language = fileExtLanguage(filepath);
const file = readFileSync(filepath, "utf8");
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(file, 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 {
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 }
})
}
}
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) {
console.error(originalLines);
console.error("Current overall line", currentLine);
console.error("Relative line", relativeLineNum);
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[0]}{matchedLine[1]}{matchedLine[2]}
return { " " }
}
if(matchedLine) {
return
{ matchedLine[0] }
{ matchedLine[2] }
}
return { " " }
}