use crate::app::App;
use crate::diff::{DiffLine, LineSource};
pub struct OutputFile {
pub path: String,
pub lines: Vec<DiffLine>,
pub additions: usize,
pub deletions: usize,
pub collapsed: bool,
}
pub struct OutputData {
pub repo_name: String,
pub to_label: String,
pub from_label: String,
pub files: Vec<OutputFile>,
pub total_additions: usize,
pub total_deletions: usize,
}
const CONTEXT_LINES: usize = 5;
fn context_visibility(lines: &[DiffLine]) -> Vec<bool> {
let interesting: Vec<bool> = lines
.iter()
.map(|line| {
line.source.is_header()
|| line.old_content.is_some()
|| !line.inline_spans.is_empty()
|| line.source.is_change()
})
.collect();
let mut show = vec![false; lines.len()];
for (i, &is_int) in interesting.iter().enumerate() {
if is_int {
let start = i.saturating_sub(CONTEXT_LINES);
let end = (i + CONTEXT_LINES + 1).min(lines.len());
for item in show.iter_mut().take(end).skip(start) {
*item = true;
}
}
}
show
}
fn filter_with_elided(lines: &[DiffLine], show: &[bool]) -> Vec<DiffLine> {
let mut result = Vec::new();
let mut last_shown: Option<usize> = None;
for (i, line) in lines.iter().enumerate() {
if show[i] {
let gap = match last_shown {
Some(last) => i - last - 1,
None => i, };
if gap > 0 {
result.push(DiffLine::elided(gap));
}
result.push(line.clone());
last_shown = Some(i);
}
}
if let Some(last) = last_shown {
let trailing_hidden = (last + 1..lines.len())
.filter(|&i| !show[i])
.count();
if trailing_hidden > 0 {
result.push(DiffLine::elided(trailing_hidden));
}
}
result
}
pub fn prepare(app: &mut App) -> OutputData {
for file in &mut app.files {
for line in &mut file.lines {
if line.old_content.is_some() {
line.ensure_inline_spans();
}
}
}
let files: Vec<OutputFile> = app
.files
.iter()
.filter_map(|file| {
let path = file
.lines
.first()
.filter(|l| l.source == LineSource::FileHeader)
.map(|l| l.content.clone())
.unwrap_or_default();
let has_content = file.lines.iter().any(|l| l.source != LineSource::FileHeader);
if !has_content {
return None;
}
let filtered = apply_view_mode(&file.lines, &app.view.view_mode);
let additions = filtered.iter().filter(|l| l.is_addition()).count();
let deletions = filtered.iter().filter(|l| l.is_deletion()).count();
let collapsed = app.view.collapsed_files.contains(&path);
Some(OutputFile {
path,
lines: filtered,
additions,
deletions,
collapsed,
})
})
.collect();
let total_additions = files.iter().map(|f| f.additions).sum();
let total_deletions = files.iter().map(|f| f.deletions).sum();
let repo_name = app
.repo_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("repo")
.to_string();
OutputData {
repo_name,
to_label: app.comparison.to_label.clone(),
from_label: app.comparison.from_label.clone(),
files,
total_additions,
total_deletions,
}
}
fn apply_view_mode(lines: &[DiffLine], view_mode: &crate::app::ViewMode) -> Vec<DiffLine> {
use crate::app::ViewMode;
match view_mode {
ViewMode::Full => lines.to_vec(),
ViewMode::Context => {
let show = context_visibility(lines);
filter_with_elided(lines, &show)
}
ViewMode::ChangesOnly => lines
.iter()
.filter(|l| l.source.is_change() || l.source.is_header())
.cloned()
.collect(),
ViewMode::CommitOnly | ViewMode::BookmarkOnly => {
let show = context_visibility(lines);
filter_with_elided(lines, &show)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::diff::DiffLine;
#[test]
fn test_context_visibility_shows_changes_and_context() {
let mut lines = Vec::new();
for i in 0..10 {
lines.push(DiffLine::new(LineSource::Base, format!("line {i}"), ' ', Some(i + 1)));
}
lines.push(DiffLine::new(LineSource::Committed, "added".into(), '+', Some(11)));
for i in 11..21 {
lines.push(DiffLine::new(LineSource::Base, format!("line {i}"), ' ', Some(i + 1)));
}
let show = context_visibility(&lines);
assert!(!show[0], "line 0 should be hidden");
assert!(!show[4], "line 4 should be hidden");
assert!(show[5], "line 5 should be visible (5 before change)");
assert!(show[9], "line 9 should be visible (1 before change)");
assert!(show[10], "change should be visible");
assert!(show[11], "line 11 should be visible");
assert!(show[15], "line 15 should be visible (5 after change)");
assert!(!show[16], "line 16 should be hidden");
}
#[test]
fn test_filter_with_elided_inserts_markers() {
let lines = vec![
DiffLine::new(LineSource::Base, "a".into(), ' ', Some(1)),
DiffLine::new(LineSource::Base, "b".into(), ' ', Some(2)),
DiffLine::new(LineSource::Base, "c".into(), ' ', Some(3)),
DiffLine::new(LineSource::Committed, "d".into(), '+', Some(4)),
DiffLine::new(LineSource::Base, "e".into(), ' ', Some(5)),
DiffLine::new(LineSource::Base, "f".into(), ' ', Some(6)),
];
let show = vec![false, false, false, true, false, false];
let filtered = filter_with_elided(&lines, &show);
assert_eq!(filtered.len(), 3); assert_eq!(filtered[0].source, LineSource::Elided);
assert_eq!(filtered[0].content, "3 lines"); assert_eq!(filtered[1].content, "d");
assert_eq!(filtered[2].source, LineSource::Elided);
assert_eq!(filtered[2].content, "2 lines"); }
#[test]
fn test_apply_view_mode_full_returns_all() {
let lines = vec![
DiffLine::file_header("test.rs"),
DiffLine::new(LineSource::Base, "a".into(), ' ', Some(1)),
DiffLine::new(LineSource::Committed, "b".into(), '+', Some(2)),
];
let result = apply_view_mode(&lines, &crate::app::ViewMode::Full);
assert_eq!(result.len(), 3);
}
#[test]
fn test_apply_view_mode_changes_only() {
let lines = vec![
DiffLine::file_header("test.rs"),
DiffLine::new(LineSource::Base, "a".into(), ' ', Some(1)),
DiffLine::new(LineSource::Committed, "b".into(), '+', Some(2)),
DiffLine::new(LineSource::DeletedBase, "c".into(), '-', Some(3)),
];
let result = apply_view_mode(&lines, &crate::app::ViewMode::ChangesOnly);
assert_eq!(result.len(), 3); assert_eq!(result[0].source, LineSource::FileHeader);
assert_eq!(result[1].source, LineSource::Committed);
assert_eq!(result[2].source, LineSource::DeletedBase);
}
}