diff --git a/src/features/pr-review/FileDiff.svelte b/src/features/pr-review/FileDiff.svelte index 38ac786..5b47b1a 100644 --- a/src/features/pr-review/FileDiff.svelte +++ b/src/features/pr-review/FileDiff.svelte @@ -125,7 +125,86 @@ return result; } + interface SideBySideRow { + type: 'header' | 'context' | 'deletion' | 'addition' | 'modified'; + header?: string; + left?: { lineNumber: number; content: string }; + right?: { lineNumber: number; content: string }; + } + + // Pair consecutive deletions with additions so modified lines appear side-by-side + function buildSideBySideRows( + lines: Array<{ type: 'context' | 'addition' | 'deletion' | 'header'; content: string; lineNumber?: { old: number | null; new: number | null } }> + ): SideBySideRow[] { + const rows: SideBySideRow[] = []; + let i = 0; + + while (i < lines.length) { + const line = lines[i]; + + if (line.type === 'header') { + rows.push({ type: 'header', header: line.content }); + i++; + } else if (line.type === 'context') { + rows.push({ + type: 'context', + left: { lineNumber: line.lineNumber!.old!, content: line.content }, + right: { lineNumber: line.lineNumber!.new!, content: line.content }, + }); + i++; + } else if (line.type === 'deletion') { + // Collect consecutive deletions + const deletions: typeof lines = []; + while (i < lines.length && lines[i].type === 'deletion') { + deletions.push(lines[i]); + i++; + } + // Collect consecutive additions that follow + const additions: typeof lines = []; + while (i < lines.length && lines[i].type === 'addition') { + additions.push(lines[i]); + i++; + } + // Pair deletions with additions + const maxLen = Math.max(deletions.length, additions.length); + for (let j = 0; j < maxLen; j++) { + const del = j < deletions.length ? deletions[j] : null; + const add = j < additions.length ? additions[j] : null; + if (del && add) { + rows.push({ + type: 'modified', + left: { lineNumber: del.lineNumber!.old!, content: del.content }, + right: { lineNumber: add.lineNumber!.new!, content: add.content }, + }); + } else if (del) { + rows.push({ + type: 'deletion', + left: { lineNumber: del.lineNumber!.old!, content: del.content }, + }); + } else if (add) { + rows.push({ + type: 'addition', + right: { lineNumber: add.lineNumber!.new!, content: add.content }, + }); + } + } + } else if (line.type === 'addition') { + // Standalone addition (not preceded by deletions) + rows.push({ + type: 'addition', + right: { lineNumber: line.lineNumber!.new!, content: line.content }, + }); + i++; + } else { + i++; + } + } + + return rows; + } + const parsedPatch = $derived(file.patch ? parsePatch(file.patch) : []); + const sideBySideRows = $derived(buildSideBySideRows(parsedPatch)); const githubFileUrl = $derived( getGitHubFileUrl({ repoHtmlUrl, @@ -305,65 +384,65 @@
| - {line.content} + {row.header} | |||
| line.lineNumber?.old && handleLineMouseDown(line.lineNumber.old, 'left', line.content, e)} - onmouseenter={() => line.lineNumber?.old && handleLineMouseEnter(line.lineNumber.old, 'left', line.content)} + class={`px-2 py-1 text-[#8b949e] text-xs text-right border-r border-[#30363d] w-12 select-none cursor-pointer hover:bg-white/5 ${checkLineSelected(row.left!.lineNumber, 'left') ? 'bg-[#1f6feb]/25' : ''} ${isDragging ? 'user-select-none' : ''}`} + onmousedown={(e) => handleLineMouseDown(row.left!.lineNumber, 'left', row.left!.content, e)} + onmouseenter={() => handleLineMouseEnter(row.left!.lineNumber, 'left', row.left!.content)} onmouseup={handleLineMouseUp} > - {line.lineNumber?.old || ''} + {row.left?.lineNumber} | line.lineNumber?.old && handleLineMouseDown(line.lineNumber.old, 'left', line.content, e)} - onmouseenter={() => line.lineNumber?.old && handleLineMouseEnter(line.lineNumber.old, 'left', line.content)} + class={`px-4 py-1 whitespace-pre-wrap break-all w-1/2 border-r border-[#30363d] cursor-pointer ${checkLineSelected(row.left!.lineNumber, 'left') ? 'bg-[#1f6feb]/10' : ''} ${isDragging ? 'user-select-none' : ''}`} + onmousedown={(e) => handleLineMouseDown(row.left!.lineNumber, 'left', row.left!.content, e)} + onmouseenter={() => handleLineMouseEnter(row.left!.lineNumber, 'left', row.left!.content)} onmouseup={handleLineMouseUp} > - {@html highlightCode(line.content, file.filename)} + {@html highlightCode(row.left!.content, file.filename)} | line.lineNumber?.new && handleLineMouseDown(line.lineNumber.new, 'right', line.content, e)} - onmouseenter={() => line.lineNumber?.new && handleLineMouseEnter(line.lineNumber.new, 'right', line.content)} + class={`px-2 py-1 text-[#8b949e] text-xs text-right border-r border-[#30363d] w-12 select-none cursor-pointer hover:bg-white/5 ${checkLineSelected(row.right!.lineNumber, 'right') ? 'bg-[#1f6feb]/25' : ''} ${isDragging ? 'user-select-none' : ''}`} + onmousedown={(e) => handleLineMouseDown(row.right!.lineNumber, 'right', row.right!.content, e)} + onmouseenter={() => handleLineMouseEnter(row.right!.lineNumber, 'right', row.right!.content)} onmouseup={handleLineMouseUp} > - {line.lineNumber?.new || ''} + {row.right?.lineNumber} | line.lineNumber?.new && handleLineMouseDown(line.lineNumber.new, 'right', line.content, e)} - onmouseenter={() => line.lineNumber?.new && handleLineMouseEnter(line.lineNumber.new, 'right', line.content)} + class={`px-4 py-1 whitespace-pre-wrap break-all w-1/2 cursor-pointer ${checkLineSelected(row.right!.lineNumber, 'right') ? 'bg-[#1f6feb]/10' : ''} ${isDragging ? 'user-select-none' : ''}`} + onmousedown={(e) => handleLineMouseDown(row.right!.lineNumber, 'right', row.right!.content, e)} + onmouseenter={() => handleLineMouseEnter(row.right!.lineNumber, 'right', row.right!.content)} onmouseup={handleLineMouseUp} > - {@html highlightCode(line.content, file.filename)} + {@html highlightCode(row.right!.content, file.filename)} |
| line.lineNumber?.old && handleLineMouseDown(line.lineNumber.old, 'left', line.content, e)} - onmouseenter={() => line.lineNumber?.old && handleLineMouseEnter(line.lineNumber.old, 'left', line.content)} + class={`px-2 py-1 text-[#8b949e] text-xs text-right border-r border-[#30363d] w-12 select-none cursor-pointer bg-red-900/15 hover:bg-red-900/30 ${checkLineSelected(row.left!.lineNumber, 'left') ? 'bg-red-900/40' : ''} ${isDragging ? 'user-select-none' : ''}`} + onmousedown={(e) => handleLineMouseDown(row.left!.lineNumber, 'left', row.left!.content, e)} + onmouseenter={() => handleLineMouseEnter(row.left!.lineNumber, 'left', row.left!.content)} onmouseup={handleLineMouseUp} > - {line.lineNumber?.old || ''} + {row.left?.lineNumber} | line.lineNumber?.old && handleLineMouseDown(line.lineNumber.old, 'left', line.content, e)} - onmouseenter={() => line.lineNumber?.old && handleLineMouseEnter(line.lineNumber.old, 'left', line.content)} + class={`px-4 py-1 whitespace-pre-wrap break-all w-1/2 border-r border-[#30363d] bg-red-900/15 cursor-pointer ${checkLineSelected(row.left!.lineNumber, 'left') ? 'bg-red-900/20' : ''} ${isDragging ? 'user-select-none' : ''}`} + onmousedown={(e) => handleLineMouseDown(row.left!.lineNumber, 'left', row.left!.content, e)} + onmouseenter={() => handleLineMouseEnter(row.left!.lineNumber, 'left', row.left!.content)} onmouseup={handleLineMouseUp} > - - - - {@html highlightCode(line.content, file.filename)} - + {@html highlightCode(row.left!.content, file.filename)} + | + +handleLineMouseDown(row.right!.lineNumber, 'right', row.right!.content, e)} + onmouseenter={() => handleLineMouseEnter(row.right!.lineNumber, 'right', row.right!.content)} + onmouseup={handleLineMouseUp} + > + {row.right?.lineNumber} + | + +handleLineMouseDown(row.right!.lineNumber, 'right', row.right!.content, e)} + onmouseenter={() => handleLineMouseEnter(row.right!.lineNumber, 'right', row.right!.content)} + onmouseup={handleLineMouseUp} + > + {@html highlightCode(row.right!.content, file.filename)} + | +
|
+ |
+ |||
| handleLineMouseDown(row.left!.lineNumber, 'left', row.left!.content, e)} + onmouseenter={() => handleLineMouseEnter(row.left!.lineNumber, 'left', row.left!.content)} + onmouseup={handleLineMouseUp} + > + {row.left?.lineNumber} + | + +handleLineMouseDown(row.left!.lineNumber, 'left', row.left!.content, e)} + onmouseenter={() => handleLineMouseEnter(row.left!.lineNumber, 'left', row.left!.content)} + onmouseup={handleLineMouseUp} + > + {@html highlightCode(row.left!.content, file.filename)} | ||
| @@ -433,26 +580,53 @@ | line.lineNumber?.new && handleLineMouseDown(line.lineNumber.new, 'right', line.content, e)} - onmouseenter={() => line.lineNumber?.new && handleLineMouseEnter(line.lineNumber.new, 'right', line.content)} + class={`px-2 py-1 text-[#8b949e] text-xs text-right border-r border-[#30363d] w-12 select-none cursor-pointer hover:bg-green-900/30 ${checkLineSelected(row.right!.lineNumber, 'right') ? 'bg-green-900/40' : ''} ${isDragging ? 'user-select-none' : ''}`} + onmousedown={(e) => handleLineMouseDown(row.right!.lineNumber, 'right', row.right!.content, e)} + onmouseenter={() => handleLineMouseEnter(row.right!.lineNumber, 'right', row.right!.content)} onmouseup={handleLineMouseUp} > - {line.lineNumber?.new || ''} + {row.right?.lineNumber} | line.lineNumber?.new && handleLineMouseDown(line.lineNumber.new, 'right', line.content, e)} - onmouseenter={() => line.lineNumber?.new && handleLineMouseEnter(line.lineNumber.new, 'right', line.content)} + class={`px-4 py-1 whitespace-pre-wrap break-all w-1/2 cursor-pointer ${checkLineSelected(row.right!.lineNumber, 'right') ? 'bg-green-900/20' : ''} ${isDragging ? 'user-select-none' : ''}`} + onmousedown={(e) => handleLineMouseDown(row.right!.lineNumber, 'right', row.right!.content, e)} + onmouseenter={() => handleLineMouseEnter(row.right!.lineNumber, 'right', row.right!.content)} onmouseup={handleLineMouseUp} > - - + - {@html highlightCode(line.content, file.filename)} - + {@html highlightCode(row.right!.content, file.filename)} | |
|
+ |
+ |||