| import * as Diff from "diff"; |
| import { theme } from "../theme/theme.ts"; |
|
|
| |
| |
| |
| |
| function parseDiffLine(line: string): { prefix: string; lineNum: string; content: string } | null { |
| const match = line.match(/^([+-\s])(\s*\d*)\s(.*)$/); |
| if (!match) return null; |
| return { prefix: match[1], lineNum: match[2], content: match[3] }; |
| } |
|
|
| |
| |
| |
| function replaceTabs(text: string): string { |
| return text.replace(/\t/g, " "); |
| } |
|
|
| |
| |
| |
| |
| |
| function renderIntraLineDiff(oldContent: string, newContent: string): { removedLine: string; addedLine: string } { |
| const wordDiff = Diff.diffWords(oldContent, newContent); |
|
|
| let removedLine = ""; |
| let addedLine = ""; |
| let isFirstRemoved = true; |
| let isFirstAdded = true; |
|
|
| for (const part of wordDiff) { |
| if (part.removed) { |
| let value = part.value; |
| |
| if (isFirstRemoved) { |
| const leadingWs = value.match(/^(\s*)/)?.[1] || ""; |
| value = value.slice(leadingWs.length); |
| removedLine += leadingWs; |
| isFirstRemoved = false; |
| } |
| if (value) { |
| removedLine += theme.inverse(value); |
| } |
| } else if (part.added) { |
| let value = part.value; |
| |
| if (isFirstAdded) { |
| const leadingWs = value.match(/^(\s*)/)?.[1] || ""; |
| value = value.slice(leadingWs.length); |
| addedLine += leadingWs; |
| isFirstAdded = false; |
| } |
| if (value) { |
| addedLine += theme.inverse(value); |
| } |
| } else { |
| removedLine += part.value; |
| addedLine += part.value; |
| } |
| } |
|
|
| return { removedLine, addedLine }; |
| } |
|
|
| export interface RenderDiffOptions { |
| |
| filePath?: string; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function renderDiff(diffText: string, _options: RenderDiffOptions = {}): string { |
| const lines = diffText.split("\n"); |
| const result: string[] = []; |
|
|
| let i = 0; |
| while (i < lines.length) { |
| const line = lines[i]; |
| const parsed = parseDiffLine(line); |
|
|
| if (!parsed) { |
| result.push(theme.fg("toolDiffContext", line)); |
| i++; |
| continue; |
| } |
|
|
| if (parsed.prefix === "-") { |
| |
| const removedLines: { lineNum: string; content: string }[] = []; |
| while (i < lines.length) { |
| const p = parseDiffLine(lines[i]); |
| if (!p || p.prefix !== "-") break; |
| removedLines.push({ lineNum: p.lineNum, content: p.content }); |
| i++; |
| } |
|
|
| |
| const addedLines: { lineNum: string; content: string }[] = []; |
| while (i < lines.length) { |
| const p = parseDiffLine(lines[i]); |
| if (!p || p.prefix !== "+") break; |
| addedLines.push({ lineNum: p.lineNum, content: p.content }); |
| i++; |
| } |
|
|
| |
| |
| if (removedLines.length === 1 && addedLines.length === 1) { |
| const removed = removedLines[0]; |
| const added = addedLines[0]; |
|
|
| const { removedLine, addedLine } = renderIntraLineDiff( |
| replaceTabs(removed.content), |
| replaceTabs(added.content), |
| ); |
|
|
| result.push(theme.fg("toolDiffRemoved", `-${removed.lineNum} ${removedLine}`)); |
| result.push(theme.fg("toolDiffAdded", `+${added.lineNum} ${addedLine}`)); |
| } else { |
| |
| for (const removed of removedLines) { |
| result.push(theme.fg("toolDiffRemoved", `-${removed.lineNum} ${replaceTabs(removed.content)}`)); |
| } |
| for (const added of addedLines) { |
| result.push(theme.fg("toolDiffAdded", `+${added.lineNum} ${replaceTabs(added.content)}`)); |
| } |
| } |
| } else if (parsed.prefix === "+") { |
| |
| result.push(theme.fg("toolDiffAdded", `+${parsed.lineNum} ${replaceTabs(parsed.content)}`)); |
| i++; |
| } else { |
| |
| result.push(theme.fg("toolDiffContext", ` ${parsed.lineNum} ${replaceTabs(parsed.content)}`)); |
| i++; |
| } |
| } |
|
|
| return result.join("\n"); |
| } |
|
|