grit_lib/diff/
whitespace_mode.rs1use std::borrow::Cow;
4
5use crate::ws::is_space_git;
6
7#[non_exhaustive]
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
10pub struct WhitespaceMode {
11 pub ignore_all_space: bool,
13 pub ignore_space_change: bool,
15 pub ignore_space_at_eol: bool,
17 pub ignore_blank_lines: bool,
19}
20
21impl WhitespaceMode {
22 #[must_use]
24 pub fn ignore_all_space() -> Self {
25 Self {
26 ignore_all_space: true,
27 ..Self::default()
28 }
29 }
30
31 #[must_use]
33 pub fn ignore_space_change() -> Self {
34 Self {
35 ignore_space_change: true,
36 ..Self::default()
37 }
38 }
39
40 #[must_use]
42 pub fn ignore_blank_lines() -> Self {
43 Self {
44 ignore_blank_lines: true,
45 ..Self::default()
46 }
47 }
48
49 #[must_use]
51 pub fn is_active(self) -> bool {
52 self.ignore_all_space
53 || self.ignore_space_change
54 || self.ignore_space_at_eol
55 || self.ignore_blank_lines
56 }
57
58 #[must_use]
60 pub fn normalizes_line_compare(self) -> bool {
61 self.ignore_all_space || self.ignore_space_change || self.ignore_space_at_eol
62 }
63
64 #[must_use]
66 pub fn normalize_line_for_compare<'a>(&self, line: &'a str) -> Cow<'a, str> {
67 if !self.normalizes_line_compare() {
68 return Cow::Borrowed(line);
69 }
70 if self.ignore_all_space {
71 return Cow::Owned(strip_git_whitespace_bytes(line));
72 }
73 let mut work = line.to_string();
74 if self.ignore_space_at_eol {
75 work = trim_trailing_git_whitespace(&work);
76 }
77 if self.ignore_space_change {
78 work = normalize_ignore_space_change_line(&work);
79 }
80 Cow::Owned(work)
81 }
82}
83
84#[must_use]
86pub fn normalize_ignore_space_change_line(line: &str) -> String {
87 let bytes = line.as_bytes();
88 let mut result = String::with_capacity(line.len());
89 let mut in_space = false;
90 for &ch in bytes {
91 if is_space_git(ch) {
92 if !in_space {
93 result.push(' ');
94 in_space = true;
95 }
96 } else {
97 result.push(ch as char);
98 in_space = false;
99 }
100 }
101 while result.ends_with(' ') {
102 result.pop();
103 }
104 result
105}
106
107#[must_use]
109pub fn strip_git_whitespace_bytes(line: &str) -> String {
110 let filtered: Vec<u8> = line.bytes().filter(|b| !is_space_git(*b)).collect();
111 String::from_utf8(filtered).unwrap_or_else(|_| line.to_string())
112}
113
114fn trim_trailing_git_whitespace(line: &str) -> String {
115 let bytes = line.as_bytes();
116 let mut end = bytes.len();
117 while end > 0 && is_space_git(bytes[end - 1]) {
118 end -= 1;
119 }
120 if end == bytes.len() {
121 return line.to_string();
122 }
123 String::from_utf8(bytes[..end].to_vec()).unwrap_or_else(|_| line.to_string())
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 #[test]
131 fn ignore_space_change_collapses_runs() {
132 assert_eq!(normalize_ignore_space_change_line("f o o"), "f o o");
133 assert_eq!(normalize_ignore_space_change_line("a b\tc "), "a b c");
134 }
135
136 #[test]
137 fn ignore_all_space_strips_git_whitespace_only() {
138 let ws = WhitespaceMode::ignore_all_space();
139 assert_eq!(ws.normalize_line_for_compare("f o o").as_ref(), "foo");
140 let nbsp_line = "a\u{00a0}b";
141 assert_eq!(
142 ws.normalize_line_for_compare(nbsp_line).as_ref(),
143 nbsp_line,
144 "NBSP is not Git whitespace and must not be stripped by -w"
145 );
146 }
147
148 #[test]
149 fn nbsp_change_not_ignored_by_ignore_all_space_compare_key() {
150 let ws = WhitespaceMode::ignore_all_space();
151 let old_key = ws.normalize_line_for_compare("a\u{00a0}b");
152 let new_key = ws.normalize_line_for_compare("ab");
153 assert_ne!(old_key, new_key);
154 }
155}