Skip to main content

declutter/
render.rs

1use std::fmt::Write;
2
3use crate::diff::RowKind;
4use crate::moves::detect;
5use crate::project::LayerMode;
6use crate::review::{Detection, DiffModes, FileReview, Layers, Summary};
7
8/// The decluttered diff as plain text. Line numbers in hunk headers refer to the
9/// original files, so the output is for reading, not for `git apply`.
10pub fn plain(files: &[FileReview], layers: Layers) -> String {
11    print(files, layers, false)
12}
13
14/// As [`plain`], with each row prefixed by its old and new line numbers. Hidden rows
15/// leave gaps in the numbering, so counting down from a hunk header would go wrong;
16/// these numbers are always right.
17pub fn plain_numbered(files: &[FileReview], layers: Layers) -> String {
18    print(files, layers, true)
19}
20
21fn print(files: &[FileReview], layers: Layers, numbered: bool) -> String {
22    let mut out = String::new();
23    let listed: Vec<&FileReview> = files
24        .iter()
25        .filter(|file| file.is_visible(layers.tests))
26        .collect();
27    let moves = detect(&listed, layers.into());
28    for (file_index, file) in listed.iter().enumerate() {
29        let view = file.view(layers);
30        let _ = writeln!(out, "{} [{}]", file_title(file), file.tag());
31        if file.detection == Detection::Binary {
32            let _ = writeln!(out, "  (binary file not shown)");
33        } else if view.hunks.is_empty() {
34            let _ = writeln!(out, "  ({})", empty_message(file, layers.into()));
35        }
36        for (hunk_index, hunk) in view.hunks.iter().enumerate() {
37            let _ = writeln!(out, "{}", hunk.header());
38            for (row_index, row) in hunk.rows.iter().enumerate() {
39                if let Some(moved) = moves.get(&(file_index, hunk_index, row_index))
40                    && moved.starts_block
41                {
42                    let _ = writeln!(out, "{}", moved.describe(&file.path));
43                }
44                let sign = match row.kind {
45                    RowKind::Context => ' ',
46                    RowKind::Removed => '-',
47                    RowKind::Added => '+',
48                };
49                if numbered {
50                    let number =
51                        |line: Option<usize>| line.map_or(String::new(), |n| n.to_string());
52                    let _ = write!(
53                        out,
54                        "{:>5} {:>5} ",
55                        number(row.old_line),
56                        number(row.new_line)
57                    );
58                }
59                let _ = writeln!(out, "{sign}{}", row.text);
60            }
61        }
62        let _ = writeln!(out);
63    }
64    let _ = writeln!(out, "{}", Summary::new(files, layers).status_line(layers));
65    out
66}
67
68pub fn file_title(file: &FileReview) -> String {
69    match &file.old_path {
70        Some(old) => format!("{} {old} → {}", file.status.letter(), file.path),
71        None => format!("{} {}", file.status.letter(), file.path),
72    }
73}
74
75/// Why a file with changes shows nothing under these layers.
76pub fn empty_message(file: &FileReview, modes: DiffModes) -> String {
77    let hidden = file.view(modes).hidden_hunks;
78    let adjective = modes.adjective();
79    match modes.outcome().0 {
80        _ if file.total_hunks() == 0 => "no textual changes".to_string(),
81        LayerMode::Hidden => format!(
82            "{adjective}-only changes: {hidden} hunk{} hidden",
83            if hidden == 1 { "" } else { "s" }
84        ),
85        LayerMode::Only => format!("no {adjective} changes"),
86        LayerMode::Shown => "no changes".to_string(),
87    }
88}