import { type DiffLineClass, type DisplayDiffLine, diffLineClass, diffLineClassFromText, displayDiffLineText } from "./unified"; const MAX_INLINE_DIFF_CHARS = 4000; const MAX_INLINE_DIFF_TOKENS = 500; interface InlineDiffPart { text: string; changed: boolean; } interface InlineDiff { added: InlineDiffPart[]; removed: InlineDiffPart[]; } interface RenderDiffLine { text: string; className: DiffLineClass; } type ChangeLineClass = "added" | "removed"; export interface RenderDiffLinesOptions { className?: string; } export function renderDisplayDiffLines(parent: HTMLElement, lines: DisplayDiffLine[], options: RenderDiffLinesOptions = {}): HTMLElement { return renderDiffLines( parent, lines.map((line) => ({ text: line.text, className: diffLineClass(line) })), options, ); } export function renderRawDiffLines(parent: HTMLElement, diff: string, options: RenderDiffLinesOptions = {}): HTMLElement { return renderDiffLines( parent, diff.split("\n").map((line) => ({ text: line, className: diffLineClassFromText(line) })), options, ); } function renderDiffLines(parent: HTMLElement, lines: RenderDiffLine[], options: RenderDiffLinesOptions): HTMLElement { const className = ["codex-panel-diff", options.className].filter(Boolean).join(" "); const pre = parent.createEl("pre", { cls: className }); for (let index = 0; index < lines.length; index += 1) { const line = lines[index]; if (!line) continue; const changeClass = changeLineClass(line.className); if (!changeClass) { renderLine(pre, line); continue; } const firstRun = collectChangeRun(lines, index, changeClass); const secondRun = collectChangeRun(lines, firstRun.endIndex, oppositeChangeLineClass(changeClass)); if (secondRun.lines.length > 0) { renderChangeRuns(pre, firstRun.lines, secondRun.lines); index = secondRun.endIndex - 1; continue; } for (const runLine of firstRun.lines) { renderLine(pre, runLine); } index = firstRun.endIndex - 1; } return pre; } function collectChangeRun( lines: RenderDiffLine[], startIndex: number, className: ChangeLineClass, ): { lines: RenderDiffLine[]; endIndex: number } { const run: RenderDiffLine[] = []; let index = startIndex; while (index < lines.length && lines[index]?.className === className) { const line = lines[index]; if (line) run.push(line); index += 1; } return { lines: run, endIndex: index }; } function renderChangeRuns(parent: HTMLElement, firstRun: RenderDiffLine[], secondRun: RenderDiffLine[]): void { const inlineDiffs = pairedInlineDiffs(firstRun, secondRun); for (let index = 0; index < firstRun.length; index += 1) { const line = firstRun[index]; if (!line) continue; renderLine(parent, line, inlinePartsForLine(line.className, inlineDiffs[index] ?? null)); } for (let index = 0; index < secondRun.length; index += 1) { const line = secondRun[index]; if (!line) continue; renderLine(parent, line, inlinePartsForLine(line.className, inlineDiffs[index] ?? null)); } } function pairedInlineDiffs(firstRun: RenderDiffLine[], secondRun: RenderDiffLine[]): (InlineDiff | null)[] { const pairCount = Math.min(firstRun.length, secondRun.length); const inlineDiffs: (InlineDiff | null)[] = []; for (let index = 0; index < pairCount; index += 1) { const firstLine = firstRun[index]; const secondLine = secondRun[index]; if (!firstLine || !secondLine) { inlineDiffs.push(null); } else { inlineDiffs.push(inlineDiffForLines(firstLine, secondLine)); } } return inlineDiffs; } function inlineDiffForLines(firstLine: RenderDiffLine, secondLine: RenderDiffLine): InlineDiff | null { const removedLine = firstLine.className === "removed" ? firstLine : secondLine; const addedLine = firstLine.className === "added" ? firstLine : secondLine; return inlineDiff(displayDiffLineText(removedLine.text, removedLine.className), displayDiffLineText(addedLine.text, addedLine.className)); } function inlinePartsForLine(className: DiffLineClass, inlineDiff: InlineDiff | null): InlineDiffPart[] | null { if (!inlineDiff) return null; if (className === "removed") return inlineDiff.removed; if (className === "added") return inlineDiff.added; return null; } function changeLineClass(className: DiffLineClass): ChangeLineClass | null { return className === "added" || className === "removed" ? className : null; } function oppositeChangeLineClass(className: ChangeLineClass): ChangeLineClass { return className === "added" ? "removed" : "added"; } function renderLine(parent: HTMLElement, line: RenderDiffLine, inlineParts: InlineDiffPart[] | null = null): void { const lineEl = parent.createEl("span", { cls: diffLineClassName(line.className) }); if (!inlineParts) { lineEl.textContent = displayDiffLineText(line.text, line.className); return; } for (const part of inlineParts) { if (part.changed) { lineEl.createSpan({ cls: diffWordClassName(line.className), text: part.text }); } else { lineEl.createSpan({ text: part.text }); } } } function diffLineClassName(className: DiffLineClass): string { if (className === "added") return "codex-panel-diff__line codex-panel-diff__line--added"; if (className === "context") return "codex-panel-diff__line codex-panel-diff__line--context"; if (className === "file") return "codex-panel-diff__line codex-panel-diff__line--file"; if (className === "hunk") return "codex-panel-diff__line codex-panel-diff__line--hunk"; return "codex-panel-diff__line codex-panel-diff__line--removed"; } function diffWordClassName(className: DiffLineClass): string { if (className === "added") return "codex-panel-diff__word codex-panel-diff__word--added"; if (className === "removed") return "codex-panel-diff__word codex-panel-diff__word--removed"; return "codex-panel-diff__word"; } function inlineDiff(removedText: string, addedText: string): InlineDiff | null { if (removedText.length + addedText.length > MAX_INLINE_DIFF_CHARS) return null; const removedTokens = segmentWords(removedText); const addedTokens = segmentWords(addedText); if (removedTokens.length + addedTokens.length > MAX_INLINE_DIFF_TOKENS) return null; const lengths = lcsLengths(removedTokens, addedTokens); const removed: InlineDiffPart[] = []; const added: InlineDiffPart[] = []; let removedIndex = 0; let addedIndex = 0; while (removedIndex < removedTokens.length || addedIndex < addedTokens.length) { const removedToken = removedTokens[removedIndex]; const addedToken = addedTokens[addedIndex]; if (removedToken !== undefined && addedToken !== undefined && removedToken === addedToken) { removed.push({ text: removedToken, changed: false }); added.push({ text: addedToken, changed: false }); removedIndex += 1; addedIndex += 1; } else if ( addedToken !== undefined && (removedToken === undefined || (lengths[removedIndex]?.[addedIndex + 1] ?? 0) >= (lengths[removedIndex + 1]?.[addedIndex] ?? 0)) ) { added.push({ text: addedToken, changed: true }); addedIndex += 1; } else if (removedToken !== undefined) { removed.push({ text: removedToken, changed: true }); removedIndex += 1; } } return { added: mergeInlineParts(added), removed: mergeInlineParts(removed) }; } function segmentWords(text: string): string[] { const segmenter = new Intl.Segmenter(undefined, { granularity: "word" }); return Array.from(segmenter.segment(text), (segment) => segment.segment); } function lcsLengths(left: string[], right: string[]): number[][] { const rows = Array.from({ length: left.length + 1 }, () => Array.from({ length: right.length + 1 }, () => 0)); for (let leftIndex = left.length - 1; leftIndex >= 0; leftIndex -= 1) { for (let rightIndex = right.length - 1; rightIndex >= 0; rightIndex -= 1) { const row = rows[leftIndex]; if (!row) continue; row[rightIndex] = left[leftIndex] === right[rightIndex] ? (rows[leftIndex + 1]?.[rightIndex + 1] ?? 0) + 1 : Math.max(rows[leftIndex + 1]?.[rightIndex] ?? 0, rows[leftIndex]?.[rightIndex + 1] ?? 0); } } return rows; } function mergeInlineParts(parts: InlineDiffPart[]): InlineDiffPart[] { const merged: InlineDiffPart[] = []; for (const part of parts) { const previous = merged.at(-1); if (previous?.changed === part.changed) { previous.text += part.text; } else { merged.push({ ...part }); } } return merged; }