Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 155 additions & 12 deletions src/ui/diff_overlay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,53 @@ pub fn parse_hunk_header(header: &str) -> Option<(usize, usize)> {
Some((old_start, new_start))
}

/// Build the line-number gutter string for one diff line.
///
/// Both columns are right-aligned to `w` and separated by a single space, so
/// the old and new numbers never collide regardless of magnitude. A trailing
/// space separates the gutter from the line content. Additions blank the old
/// column; deletions blank the new column; hunk headers are blank across the
/// full `2 * w + 2` gutter width.
fn format_gutter(kind: &DiffLineKind, old_line: usize, new_line: usize, w: usize) -> String {
match kind {
DiffLineKind::Addition => format!("{:>w$} {:>w$} ", "", new_line, w = w),
DiffLineKind::Deletion => format!("{:>w$} {:>w$} ", old_line, "", w = w),
DiffLineKind::Context => format!("{:>w$} {:>w$} ", old_line, new_line, w = w),
DiffLineKind::HunkHeader => " ".repeat(2 * w + 2),
}
}

/// Per-column digit width for a file's diff gutter: the digit count of the
/// largest line number that will be displayed across both the old and new
/// columns. Floored at 1 so empty/degenerate diffs still render a stable
/// gutter. Sizing per-file keeps narrow files compact while guaranteeing
/// 5+ digit line numbers never collide.
fn gutter_width(diff: &FileDiff) -> usize {
let mut max_line = 0usize;
for hunk in &diff.hunks {
let (mut old_line, mut new_line) = parse_hunk_header(&hunk.header).unwrap_or((1, 1));
for diff_line in &hunk.lines {
match diff_line.kind {
DiffLineKind::Addition => {
max_line = max_line.max(new_line);
new_line += 1;
}
DiffLineKind::Deletion => {
max_line = max_line.max(old_line);
old_line += 1;
}
DiffLineKind::Context => {
max_line = max_line.max(old_line).max(new_line);
old_line += 1;
new_line += 1;
}
DiffLineKind::HunkHeader => {}
}
}
}
max_line.to_string().len().max(1)
}

/// Render the diff overlay onto `frame`.
///
/// The overlay is drawn as a centred, bordered panel on top of whatever is
Expand Down Expand Up @@ -85,13 +132,19 @@ pub fn render_diff_overlay(
let inner = block.inner(overlay_rect);
frame.render_widget(block, overlay_rect);

// Size the line-number gutter to this file's largest line number.
let w = gutter_width(diff);

// Build diff lines with line numbers and colours
let mut lines: Vec<Line> = Vec::new();

for hunk in &diff.hunks {
// Hunk header line
lines.push(Line::from(vec![
Span::styled(" ", Style::default().fg(colors::DIFF_LINE_NUMBER)),
Span::styled(
format_gutter(&DiffLineKind::HunkHeader, 0, 0, w),
Style::default().fg(colors::DIFF_LINE_NUMBER),
),
Span::styled(
&hunk.header,
Style::default()
Expand All @@ -103,27 +156,22 @@ pub fn render_diff_overlay(
let (mut old_line, mut new_line) = parse_hunk_header(&hunk.header).unwrap_or((1, 1));

for diff_line in &hunk.lines {
let (gutter, style) = match diff_line.kind {
let gutter = format_gutter(&diff_line.kind, old_line, new_line, w);
let style = match diff_line.kind {
DiffLineKind::Addition => {
let g = format!("{:>4}{:>4} ", " ", new_line);
new_line += 1;
(g, Style::default().fg(colors::DIFF_ADD_FG))
Style::default().fg(colors::DIFF_ADD_FG)
}
DiffLineKind::Deletion => {
let g = format!("{:>4}{:>4} ", old_line, " ");
old_line += 1;
(g, Style::default().fg(colors::DIFF_DEL_FG))
Style::default().fg(colors::DIFF_DEL_FG)
}
DiffLineKind::Context => {
let g = format!("{:>4}{:>4} ", old_line, new_line);
old_line += 1;
new_line += 1;
(g, Style::default().fg(colors::DIFF_CONTEXT))
Style::default().fg(colors::DIFF_CONTEXT)
}
DiffLineKind::HunkHeader => (
" ".to_string(),
Style::default().fg(colors::DIFF_HUNK_HEADER),
),
DiffLineKind::HunkHeader => Style::default().fg(colors::DIFF_HUNK_HEADER),
};

let prefix = match diff_line.kind {
Expand Down Expand Up @@ -202,4 +250,99 @@ mod tests {
let area = Rect::new(0, 0, 100, 100);
assert_eq!(inner_height(area), 83);
}

#[test]
fn test_format_gutter_context_separates_columns() {
// width 2, both columns present
let g = format_gutter(&DiffLineKind::Context, 12, 34, 2);
assert_eq!(g, "12 34 ");
}

#[test]
fn test_format_gutter_addition_blanks_old_column() {
let g = format_gutter(&DiffLineKind::Addition, 12, 34, 2);
assert_eq!(g, " 34 ");
}

#[test]
fn test_format_gutter_deletion_blanks_new_column() {
let g = format_gutter(&DiffLineKind::Deletion, 12, 34, 2);
assert_eq!(g, "12 ");
}

#[test]
fn test_format_gutter_five_digits_stay_separated() {
// The bug: 5-digit numbers used to collide. With width 5 they must
// be separated by at least one space.
let g = format_gutter(&DiffLineKind::Context, 12345, 12346, 5);
assert_eq!(g, "12345 12346 ");
assert!(g.contains("12345 12346"));
}

#[test]
fn test_format_gutter_hunk_header_is_blank_of_matching_width() {
// Blank gutter must span 2*w + 2 columns to align with number rows.
let g = format_gutter(&DiffLineKind::HunkHeader, 0, 0, 4);
assert_eq!(g, " ".repeat(4 * 2 + 2));
assert_eq!(g.len(), 10);
}

fn diff_with(hunks: Vec<(&str, Vec<(DiffLineKind, &str)>)>) -> FileDiff {
use crate::git::{DiffHunk, DiffLine};
FileDiff {
hunks: hunks
.into_iter()
.map(|(header, lines)| DiffHunk {
header: header.to_string(),
lines: lines
.into_iter()
.map(|(kind, content)| DiffLine {
kind,
content: content.to_string(),
})
.collect(),
})
.collect(),
}
}

#[test]
fn test_gutter_width_empty_diff_is_one() {
let diff = FileDiff::default();
assert_eq!(gutter_width(&diff), 1);
}

#[test]
fn test_gutter_width_small_numbers() {
// Lines run 1..=3 on each side -> 1 digit.
let diff = diff_with(vec![(
"@@ -1,3 +1,3 @@",
vec![
(DiffLineKind::Context, "a"),
(DiffLineKind::Context, "b"),
(DiffLineKind::Context, "c"),
],
)]);
assert_eq!(gutter_width(&diff), 1);
}

#[test]
fn test_gutter_width_tracks_largest_across_hunks() {
// Second hunk starts at 9998 with 3 context lines -> reaches 10000 (5 digits).
let diff = diff_with(vec![
(
"@@ -1,2 +1,2 @@",
vec![(DiffLineKind::Context, "a"), (DiffLineKind::Context, "b")],
),
(
"@@ -9998,3 +9998,3 @@",
vec![
(DiffLineKind::Context, "x"),
(DiffLineKind::Context, "y"),
(DiffLineKind::Context, "z"),
],
),
]);
assert_eq!(gutter_width(&diff), 5);
}
}
Loading
Loading