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
65 changes: 30 additions & 35 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ impl AppState {
}

fn jump_to_hunk(&mut self, files: &[DiffFileView], rows: u16, forward: bool) {
let current_file = &files[self.file_index];
let current_anchor = self
.focused_hunk_lines
.as_ref()
Expand All @@ -167,12 +168,15 @@ impl AppState {
.copied()
.or(self.hunk_anchor_by_file[self.file_index])
.unwrap_or(self.scroll_offset);
let hunk_starts = build_hunk_start_lines(&files[self.file_index]);

let target = if forward {
hunk_starts.iter().find(|&&line| line > current_anchor)
current_file
.hunk_start_lines
.iter()
.find(|&&line| line > current_anchor)
} else {
hunk_starts
current_file
.hunk_start_lines
.iter()
.rev()
.find(|&&line| line < current_anchor)
Expand All @@ -181,7 +185,7 @@ impl AppState {
if let Some(&line) = target {
let max_scroll = max_scroll_for_current_file(files, self, rows);
self.scroll_offset = line.min(max_scroll);
self.focused_hunk_lines = Some(build_hunk_line_range(&files[self.file_index], line));
self.focused_hunk_lines = Some(build_hunk_line_range(current_file, line));
self.hunk_anchor_by_file[self.file_index] = Some(line);
return;
}
Expand All @@ -198,7 +202,7 @@ impl AppState {
} else {
(self.file_index + file_count - step) % file_count
};
let next_hunk_starts = build_hunk_start_lines(&files[next_index]);
let next_hunk_starts = &files[next_index].hunk_start_lines;
let wrap_target = if forward {
next_hunk_starts.first()
} else {
Expand Down Expand Up @@ -330,37 +334,12 @@ fn move_horizontal(
}
}

fn build_hunk_start_lines(file: &DiffFileView) -> Vec<usize> {
let mut changed: Vec<usize> = file
.left_deleted_line_indexes
.iter()
.chain(file.right_added_line_indexes.iter())
.copied()
.collect();
changed.sort_unstable();
changed.dedup();

let changed_set: std::collections::HashSet<usize> = changed.iter().copied().collect();
changed
.into_iter()
.filter(|&line| line == 0 || !changed_set.contains(&(line - 1)))
.collect()
}

fn build_hunk_line_range(file: &DiffFileView, hunk_start: usize) -> HashSet<usize> {
let mut range = HashSet::new();
let max_lines = file.left_lines.len().max(file.right_lines.len());
let mut line = hunk_start;
while line < max_lines {
let is_changed = file.left_deleted_line_indexes.contains(&line)
|| file.right_added_line_indexes.contains(&line);
if !is_changed {
break;
}
range.insert(line);
line += 1;
}
range
file.hunk_line_ranges
.iter()
.find(|range| *range.start() == hunk_start)
.map(|range| range.clone().collect())
.unwrap_or_default()
}

fn build_search_match_line_indexes(file: &DiffFileView, query: &str) -> Vec<usize> {
Expand Down Expand Up @@ -648,10 +627,14 @@ mod tests {
review_key: "key".to_string(),
left_lines: left_lines.iter().map(|line| line.to_string()).collect(),
right_lines: right_lines.iter().map(|line| line.to_string()).collect(),
normalized_left_lines: left_lines.iter().map(|line| line.to_string()).collect(),
normalized_right_lines: right_lines.iter().map(|line| line.to_string()).collect(),
left_language: Some("rust".to_string()),
right_language: Some("rust".to_string()),
left_deleted_line_indexes: HashSet::new(),
right_added_line_indexes: HashSet::new(),
hunk_start_lines: Vec::new(),
hunk_line_ranges: Vec::new(),
left_max_content_length: 0,
right_max_content_length: 0,
}
Expand All @@ -666,6 +649,18 @@ mod tests {
let mut file = create_test_file(left_lines, right_lines);
file.left_deleted_line_indexes = left_deleted.iter().copied().collect();
file.right_added_line_indexes = right_added.iter().copied().collect();
file.hunk_start_lines = left_deleted
.iter()
.chain(right_added.iter())
.copied()
.collect::<std::collections::BTreeSet<_>>()
.into_iter()
.collect();
file.hunk_line_ranges = file
.hunk_start_lines
.iter()
.map(|line| *line..=*line)
.collect();
file
}

Expand Down
71 changes: 66 additions & 5 deletions src/diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use crate::{
},
review::compute_review_key,
syntax::syntax_set,
text::get_max_normalized_line_length,
text::{get_max_line_length, normalize_content},
};

const MISSING_LEFT: &str = "<file does not exist in base revision>";
Expand Down Expand Up @@ -439,6 +439,38 @@ fn detect_syntax_name(file_path: Option<&str>, lines: &[String]) -> Option<Strin
.map(|syntax| syntax.name.clone())
}

fn build_hunk_metadata(
left_deleted_line_indexes: &HashSet<usize>,
right_added_line_indexes: &HashSet<usize>,
) -> (Vec<usize>, Vec<std::ops::RangeInclusive<usize>>) {
let mut changed: Vec<usize> = left_deleted_line_indexes
.iter()
.chain(right_added_line_indexes.iter())
.copied()
.collect();
changed.sort_unstable();
changed.dedup();

let mut starts = Vec::new();
let mut ranges = Vec::new();
let mut index = 0;
while index < changed.len() {
let start = changed[index];
let mut end = start;
index += 1;

while index < changed.len() && changed[index] == end + 1 {
end = changed[index];
index += 1;
}

starts.push(start);
ranges.push(start..=end);
}

(starts, ranges)
}

pub(crate) fn build_file_views(
repo_root: &Path,
comparison: &ResolvedComparison,
Expand Down Expand Up @@ -483,15 +515,34 @@ pub(crate) fn build_file_views(
right_lines.len(),
);

let normalized_left_lines: Vec<String> = left_lines
.iter()
.map(|line| normalize_content(line))
.collect();
let normalized_right_lines: Vec<String> = right_lines
.iter()
.map(|line| normalize_content(line))
.collect();
let left_max_content_length = get_max_line_length(&normalized_left_lines);
let right_max_content_length = get_max_line_length(&normalized_right_lines);
let (hunk_start_lines, hunk_line_ranges) = build_hunk_metadata(
&line_highlights.left_deleted_line_indexes,
&line_highlights.right_added_line_indexes,
);

views.push(DiffFileView {
descriptor: descriptor.clone(),
review_key: compute_review_key(descriptor, &left_lines, &right_lines),
left_language: detect_syntax_name(descriptor.base_path.as_deref(), &left_lines),
right_language: detect_syntax_name(descriptor.head_path.as_deref(), &right_lines),
left_deleted_line_indexes: line_highlights.left_deleted_line_indexes,
right_added_line_indexes: line_highlights.right_added_line_indexes,
left_max_content_length: get_max_normalized_line_length(&left_lines),
right_max_content_length: get_max_normalized_line_length(&right_lines),
hunk_start_lines,
hunk_line_ranges,
left_max_content_length,
right_max_content_length,
normalized_left_lines,
normalized_right_lines,
left_lines,
right_lines,
});
Expand All @@ -505,8 +556,8 @@ mod tests {
use crate::model::FileContentSource;

use super::{
detect_syntax_name, parse_diff_name_status_output, parse_line_highlights_from_patch,
split_into_lines,
build_hunk_metadata, detect_syntax_name, parse_diff_name_status_output,
parse_line_highlights_from_patch, split_into_lines,
};

#[test]
Expand Down Expand Up @@ -539,6 +590,16 @@ mod tests {
assert_eq!(lines, vec!["a".to_string(), "b".to_string()]);
}

#[test]
fn build_hunk_metadata_groups_contiguous_changed_lines() {
let left_deleted = [1, 2, 5].into_iter().collect();
let right_added = [2, 6].into_iter().collect();
let (starts, ranges) = build_hunk_metadata(&left_deleted, &right_added);

assert_eq!(starts, vec![1, 5]);
assert_eq!(ranges, vec![1..=2, 5..=6]);
}

#[test]
fn detect_syntax_uses_filename_token_when_no_extension() {
let lines = vec!["echo hello".to_string()];
Expand Down
5 changes: 5 additions & 0 deletions src/model.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::{
collections::HashSet,
fmt::{self, Display},
ops::RangeInclusive,
};

use clap::ValueEnum;
Expand Down Expand Up @@ -98,10 +99,14 @@ pub(crate) struct DiffFileView {
pub(crate) review_key: String,
pub(crate) left_lines: Vec<String>,
pub(crate) right_lines: Vec<String>,
pub(crate) normalized_left_lines: Vec<String>,
pub(crate) normalized_right_lines: Vec<String>,
pub(crate) left_language: Option<String>,
pub(crate) right_language: Option<String>,
pub(crate) left_deleted_line_indexes: HashSet<usize>,
pub(crate) right_added_line_indexes: HashSet<usize>,
pub(crate) hunk_start_lines: Vec<usize>,
pub(crate) hunk_line_ranges: Vec<RangeInclusive<usize>>,
pub(crate) left_max_content_length: usize,
pub(crate) right_max_content_length: usize,
}
Expand Down
11 changes: 7 additions & 4 deletions src/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use crate::{
DiffFileView, LineHighlightKind, PaneOffsets, PaneSide, ResolvedComparison, ThemeMode,
},
syntax::syntax_set,
text::{fit_line, normalize_content, normalized_char_count, pad_to_width, slice_chars},
text::{fit_line, normalized_char_count, pad_to_width, slice_chars},
};

const HEADER_LINE_COUNT: usize = 4;
Expand Down Expand Up @@ -272,7 +272,7 @@ fn format_pane_line(
}

let content_width = pane_width - prefix_width;
let content_text = line_value.map(normalize_content).unwrap_or_default();
let content_text = line_value.unwrap_or_default();
let visible_content = slice_chars(&content_text, horizontal_offset, content_width);
let padded_visible_content = pad_to_width(visible_content, content_width);

Expand Down Expand Up @@ -388,9 +388,12 @@ pub(crate) fn render_frame(
let mut body_lines: Vec<Line<'static>> = Vec::with_capacity(layout.body_line_count);
for row in 0..layout.body_line_count {
let line_number = clamped_scroll_offset + row;
let left_line = current_file.left_lines.get(line_number).map(String::as_str);
let left_line = current_file
.normalized_left_lines
.get(line_number)
.map(String::as_str);
let right_line = current_file
.right_lines
.normalized_right_lines
.get(line_number)
.map(String::as_str);
let left_highlight_kind = if current_file
Expand Down
3 changes: 1 addition & 2 deletions src/terminal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use ratatui::{
Terminal,
backend::{Backend, CrosstermBackend},
text::Text,
widgets::{Clear, Paragraph},
widgets::Paragraph,
};

use crate::{
Expand Down Expand Up @@ -51,7 +51,6 @@ fn draw_app<B: Backend>(
let text = Text::from(render_output.lines);
terminal.draw(move |frame| {
let area = frame.area();
frame.render_widget(Clear, area);
frame.render_widget(Paragraph::new(text), area);
})?;

Expand Down
4 changes: 2 additions & 2 deletions src/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,10 @@ pub(crate) fn normalize_content(value: &str) -> String {
value.replace('\t', " ").replace('\r', "")
}

pub(crate) fn get_max_normalized_line_length(lines: &[String]) -> usize {
pub(crate) fn get_max_line_length(lines: &[String]) -> usize {
lines
.iter()
.map(|line| normalized_char_count(&normalize_content(line)))
.map(|line| normalized_char_count(line))
.max()
.unwrap_or(0)
}
Expand Down
Loading