From 8163e9ab6ebeaeca9c9f9eaaf87f3e6d62c01178 Mon Sep 17 00:00:00 2001 From: flamestro Date: Sat, 4 Jul 2026 10:25:12 +0200 Subject: [PATCH] improve render performance --- src/app.rs | 65 +++++++++++++++++++++----------------------- src/diff.rs | 71 +++++++++++++++++++++++++++++++++++++++++++++---- src/model.rs | 5 ++++ src/render.rs | 11 +++++--- src/terminal.rs | 3 +-- src/text.rs | 4 +-- 6 files changed, 111 insertions(+), 48 deletions(-) diff --git a/src/app.rs b/src/app.rs index 6d83c7f..9546d3c 100644 --- a/src/app.rs +++ b/src/app.rs @@ -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() @@ -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) @@ -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; } @@ -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 { @@ -330,37 +334,12 @@ fn move_horizontal( } } -fn build_hunk_start_lines(file: &DiffFileView) -> Vec { - let mut changed: Vec = 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 = 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 { - 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 { @@ -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, } @@ -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::>() + .into_iter() + .collect(); + file.hunk_line_ranges = file + .hunk_start_lines + .iter() + .map(|line| *line..=*line) + .collect(); file } diff --git a/src/diff.rs b/src/diff.rs index cf76309..2389e5f 100644 --- a/src/diff.rs +++ b/src/diff.rs @@ -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 = ""; @@ -439,6 +439,38 @@ fn detect_syntax_name(file_path: Option<&str>, lines: &[String]) -> Option, + right_added_line_indexes: &HashSet, +) -> (Vec, Vec>) { + let mut changed: Vec = 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, @@ -483,6 +515,21 @@ pub(crate) fn build_file_views( right_lines.len(), ); + let normalized_left_lines: Vec = left_lines + .iter() + .map(|line| normalize_content(line)) + .collect(); + let normalized_right_lines: Vec = 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), @@ -490,8 +537,12 @@ pub(crate) fn build_file_views( 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, }); @@ -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] @@ -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()]; diff --git a/src/model.rs b/src/model.rs index c40b8b1..edd3de4 100644 --- a/src/model.rs +++ b/src/model.rs @@ -1,6 +1,7 @@ use std::{ collections::HashSet, fmt::{self, Display}, + ops::RangeInclusive, }; use clap::ValueEnum; @@ -98,10 +99,14 @@ pub(crate) struct DiffFileView { pub(crate) review_key: String, pub(crate) left_lines: Vec, pub(crate) right_lines: Vec, + pub(crate) normalized_left_lines: Vec, + pub(crate) normalized_right_lines: Vec, pub(crate) left_language: Option, pub(crate) right_language: Option, pub(crate) left_deleted_line_indexes: HashSet, pub(crate) right_added_line_indexes: HashSet, + pub(crate) hunk_start_lines: Vec, + pub(crate) hunk_line_ranges: Vec>, pub(crate) left_max_content_length: usize, pub(crate) right_max_content_length: usize, } diff --git a/src/render.rs b/src/render.rs index c2957c7..4249c0c 100644 --- a/src/render.rs +++ b/src/render.rs @@ -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; @@ -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); @@ -388,9 +388,12 @@ pub(crate) fn render_frame( let mut body_lines: Vec> = 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 diff --git a/src/terminal.rs b/src/terminal.rs index 042946c..4b24816 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -11,7 +11,7 @@ use ratatui::{ Terminal, backend::{Backend, CrosstermBackend}, text::Text, - widgets::{Clear, Paragraph}, + widgets::Paragraph, }; use crate::{ @@ -51,7 +51,6 @@ fn draw_app( 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); })?; diff --git a/src/text.rs b/src/text.rs index 39003ab..f330f13 100644 --- a/src/text.rs +++ b/src/text.rs @@ -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) }