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 ; }