217 lines
6.2 KiB
TypeScript
217 lines
6.2 KiB
TypeScript
import type { FileDiffData, DiffComment, AnnotationSide, CommentAnnotation } from './types';
|
|
|
|
/**
|
|
* Format line range for display (e.g., "L10" or "L10-15")
|
|
*/
|
|
export function formatLineRange(startLine: number, endLine: number): string {
|
|
return startLine === endLine ? `L${startLine}` : `L${startLine}-${endLine}`;
|
|
}
|
|
|
|
/**
|
|
* Frontend types that mirror backend FileMutation
|
|
*/
|
|
export interface FileMutation {
|
|
type: 'create' | 'replace' | 'patch' | 'delete' | 'rename';
|
|
content?: string;
|
|
old_content?: string;
|
|
new_content?: string;
|
|
diff?: string;
|
|
new_path?: string;
|
|
start_line?: number;
|
|
end_line?: number;
|
|
}
|
|
|
|
export interface ModifyFilePayload {
|
|
file_path: string;
|
|
mutations: FileMutation[];
|
|
}
|
|
|
|
/**
|
|
* Normalize a diff string by ensuring it has proper headers.
|
|
* Required because backend diffs may not include full git headers.
|
|
*/
|
|
export function normalizeDiffString(diff: string, filePath: string): string {
|
|
if (!diff) return '';
|
|
|
|
const trimmed = diff.trim();
|
|
|
|
// Check if the diff already has headers
|
|
if (trimmed.startsWith('diff --git')) {
|
|
return trimmed;
|
|
}
|
|
|
|
// Check if it has file headers but not diff header
|
|
const hasFileHeaders = trimmed.includes('---') && trimmed.includes('+++');
|
|
|
|
if (hasFileHeaders) {
|
|
// Add just the diff header
|
|
return `diff --git a/${filePath} b/${filePath}\n${trimmed}`;
|
|
}
|
|
|
|
// Add minimal headers
|
|
const headers = [
|
|
`diff --git a/${filePath} b/${filePath}`,
|
|
`--- a/${filePath}`,
|
|
`+++ b/${filePath}`,
|
|
];
|
|
return headers.join('\n') + '\n' + trimmed;
|
|
}
|
|
|
|
/**
|
|
* Transform a backend FileMutation to FileDiffData for @pierre/diffs.
|
|
* The DiffViewer component handles diff generation from content using the library.
|
|
*/
|
|
export function transformFileMutation(
|
|
filePath: string,
|
|
mutation: FileMutation
|
|
): FileDiffData {
|
|
return {
|
|
filePath: mutation.new_path || filePath,
|
|
oldContent: mutation.old_content || '',
|
|
newContent: mutation.new_content || mutation.content || '',
|
|
diff: mutation.diff ? normalizeDiffString(mutation.diff, filePath) : undefined,
|
|
additions: 0,
|
|
deletions: 0,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Transform a git status diff string to FileDiffData.
|
|
* Language detection is handled automatically by the library.
|
|
*/
|
|
export function transformGitDiff(
|
|
filePath: string,
|
|
diff: string,
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
_status: 'A' | 'M' | 'D' | '??' | string
|
|
): FileDiffData {
|
|
return {
|
|
filePath,
|
|
oldContent: '',
|
|
newContent: '',
|
|
diff: normalizeDiffString(diff, filePath),
|
|
additions: 0,
|
|
deletions: 0,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Convert DiffComment[] to @pierre/diffs DiffLineAnnotation[]
|
|
*/
|
|
export function commentsToAnnotations(
|
|
comments: DiffComment[]
|
|
): CommentAnnotation[] {
|
|
return comments.map((comment) => ({
|
|
side: comment.side,
|
|
lineNumber: comment.endLine, // Anchor at the end of the range
|
|
metadata: {
|
|
comment,
|
|
isEditing: false,
|
|
},
|
|
}));
|
|
}
|
|
|
|
function isDiffHeader(line: string): boolean {
|
|
return line.startsWith('diff --git') || line.startsWith('---') || line.startsWith('+++');
|
|
}
|
|
|
|
function isInRange(lineNum: number, startLine: number, endLine: number): boolean {
|
|
return lineNum >= startLine && lineNum <= endLine;
|
|
}
|
|
|
|
type DiffLineCounters = { currentOldLine: number; currentNewLine: number };
|
|
|
|
function processHunkHeader(line: string, counters: DiffLineCounters): boolean {
|
|
const hunkMatch = line.match(/^@@\s*-(\d+)(?:,\d+)?\s*\+(\d+)(?:,\d+)?\s*@@/);
|
|
if (!hunkMatch) return false;
|
|
counters.currentOldLine = parseInt(hunkMatch[1], 10);
|
|
counters.currentNewLine = parseInt(hunkMatch[2], 10);
|
|
return true;
|
|
}
|
|
|
|
type ProcessDiffLineParams = {
|
|
line: string;
|
|
counters: DiffLineCounters;
|
|
startLine: number;
|
|
endLine: number;
|
|
side: AnnotationSide;
|
|
resultLines: string[];
|
|
};
|
|
|
|
function processDiffLine({ line, counters, startLine, endLine, side, resultLines }: ProcessDiffLineParams): void {
|
|
if (line.startsWith('+')) {
|
|
if (side === 'additions' && isInRange(counters.currentNewLine, startLine, endLine)) {
|
|
resultLines.push(line.substring(1));
|
|
}
|
|
counters.currentNewLine++;
|
|
} else if (line.startsWith('-')) {
|
|
if (side === 'deletions' && isInRange(counters.currentOldLine, startLine, endLine)) {
|
|
resultLines.push(line.substring(1));
|
|
}
|
|
counters.currentOldLine++;
|
|
} else if (line.startsWith(' ') || (!line.startsWith('@') && line !== '')) {
|
|
const content = line.startsWith(' ') ? line.substring(1) : line;
|
|
const lineNum = side === 'additions' ? counters.currentNewLine : counters.currentOldLine;
|
|
if (isInRange(lineNum, startLine, endLine)) resultLines.push(content);
|
|
counters.currentOldLine++;
|
|
counters.currentNewLine++;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extract code content from diff for a line range
|
|
*/
|
|
export function extractCodeFromDiff(
|
|
diff: string,
|
|
startLine: number,
|
|
endLine: number,
|
|
side: AnnotationSide
|
|
): string {
|
|
const lines = diff.split('\n');
|
|
const resultLines: string[] = [];
|
|
const counters: DiffLineCounters = { currentOldLine: 0, currentNewLine: 0 };
|
|
|
|
for (const line of lines) {
|
|
if (processHunkHeader(line, counters)) continue;
|
|
if (isDiffHeader(line)) continue;
|
|
processDiffLine({ line, counters, startLine, endLine, side, resultLines });
|
|
}
|
|
|
|
return resultLines.join('\n');
|
|
}
|
|
|
|
/**
|
|
* Extract code content from full file content for a line range
|
|
*/
|
|
export function extractCodeFromContent(
|
|
content: string,
|
|
startLine: number,
|
|
endLine: number
|
|
): string {
|
|
const lines = content.split('\n');
|
|
return lines.slice(startLine - 1, endLine).join('\n');
|
|
}
|
|
|
|
/**
|
|
* Compute simple line-level diff stats between two strings.
|
|
* Returns the number of added and deleted lines.
|
|
*/
|
|
export function computeLineDiffStats(
|
|
original: string,
|
|
current: string,
|
|
): { additions: number; deletions: number } {
|
|
const originalLines = original.split('\n');
|
|
const currentLines = current.split('\n');
|
|
let additions = 0;
|
|
let deletions = 0;
|
|
const maxLen = Math.max(originalLines.length, currentLines.length);
|
|
for (let i = 0; i < maxLen; i++) {
|
|
const origLine = originalLines[i];
|
|
const currLine = currentLines[i];
|
|
if (origLine === undefined && currLine !== undefined) additions++;
|
|
else if (origLine !== undefined && currLine === undefined) deletions++;
|
|
else if (origLine !== currLine) { additions++; deletions++; }
|
|
}
|
|
return { additions, deletions };
|
|
}
|