1use crate::config::ConfigSet;
4
5use super::filter::DiffFilter;
6use super::{indent_heuristic_from_config, resolve_indent_heuristic, WhitespaceMode};
7
8#[non_exhaustive]
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
11pub enum DiffAlgorithm {
12 #[default]
14 Myers,
15 Minimal,
17 Patience,
19 Histogram,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
25#[error("unknown diff algorithm `{0}`")]
26pub struct UnknownDiffAlgorithm(pub String);
27
28impl std::str::FromStr for DiffAlgorithm {
29 type Err = UnknownDiffAlgorithm;
30
31 fn from_str(s: &str) -> Result<Self, Self::Err> {
32 match s.to_ascii_lowercase().as_str() {
33 "myers" | "default" => Ok(Self::Myers),
34 "minimal" => Ok(Self::Minimal),
35 "patience" => Ok(Self::Patience),
36 "histogram" => Ok(Self::Histogram),
37 other => Err(UnknownDiffAlgorithm(other.to_owned())),
38 }
39 }
40}
41
42impl DiffAlgorithm {
43 #[must_use]
45 pub fn similar_algorithm(self) -> similar::Algorithm {
46 match self {
47 Self::Myers | Self::Histogram => similar::Algorithm::Myers,
48 Self::Minimal => similar::Algorithm::Lcs,
49 Self::Patience => similar::Algorithm::Patience,
50 }
51 }
52
53 #[must_use]
55 pub fn uses_git_histogram(self) -> bool {
56 matches!(self, Self::Histogram)
57 }
58}
59
60pub const RENAME_DETECTION_DISABLED: u32 = 101;
63
64#[non_exhaustive]
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum RenameDetection {
68 Off,
70 Similarity(u8),
72}
73
74impl Default for RenameDetection {
75 fn default() -> Self {
76 Self::Similarity(40)
77 }
78}
79
80impl RenameDetection {
81 #[must_use]
83 pub fn threshold_percent(self) -> Option<u32> {
84 match self {
85 Self::Off => None,
86 Self::Similarity(p) => Some(u32::from(p)),
87 }
88 }
89}
90
91pub const WORD_LEVEL_INLINE_MAX_CHANGED_FILES: usize = 64;
93
94#[non_exhaustive]
96#[derive(Debug, Clone)]
97pub struct DiffOptions {
98 pub context_lines: usize,
100 pub inter_hunk_context: usize,
102 pub algorithm: DiffAlgorithm,
104 pub indent_heuristic: bool,
106 pub renames: RenameDetection,
108 pub filter: Option<DiffFilter>,
110 pub whitespace: WhitespaceMode,
112 pub word_level_inline: bool,
114 pub function_context: bool,
116 #[doc(hidden)]
118 pub blob_read_counter: Option<std::sync::Arc<std::sync::atomic::AtomicUsize>>,
119}
120
121impl Default for DiffOptions {
122 fn default() -> Self {
123 Self {
124 context_lines: 3,
125 inter_hunk_context: 0,
126 algorithm: DiffAlgorithm::Myers,
127 indent_heuristic: true,
128 renames: RenameDetection::default(),
129 filter: None,
130 whitespace: WhitespaceMode::default(),
131 word_level_inline: true,
132 function_context: false,
133 blob_read_counter: None,
134 }
135 }
136}
137
138impl PartialEq for DiffOptions {
139 fn eq(&self, other: &Self) -> bool {
140 self.context_lines == other.context_lines
141 && self.inter_hunk_context == other.inter_hunk_context
142 && self.algorithm == other.algorithm
143 && self.indent_heuristic == other.indent_heuristic
144 && self.renames == other.renames
145 && self.filter == other.filter
146 && self.whitespace == other.whitespace
147 && self.word_level_inline == other.word_level_inline
148 && self.function_context == other.function_context
149 }
150}
151
152impl DiffOptions {
153 #[must_use]
155 pub fn with_context_lines(mut self, context_lines: usize) -> Self {
156 self.context_lines = context_lines;
157 self
158 }
159
160 #[must_use]
162 pub fn with_inter_hunk_context(mut self, inter_hunk_context: usize) -> Self {
163 self.inter_hunk_context = inter_hunk_context;
164 self
165 }
166
167 #[must_use]
169 pub fn with_algorithm(mut self, algorithm: DiffAlgorithm) -> Self {
170 self.algorithm = algorithm;
171 self
172 }
173
174 #[must_use]
176 pub fn with_indent_heuristic(mut self, indent_heuristic: bool) -> Self {
177 self.indent_heuristic = indent_heuristic;
178 self
179 }
180
181 #[must_use]
183 pub fn with_renames(mut self, renames: RenameDetection) -> Self {
184 self.renames = renames;
185 self
186 }
187
188 #[must_use]
190 pub fn with_filter(mut self, filter: Option<DiffFilter>) -> Self {
191 self.filter = filter;
192 self
193 }
194
195 #[must_use]
197 pub fn rename_similarity_threshold(&self) -> Option<u32> {
198 match self.renames {
199 RenameDetection::Off => None,
200 RenameDetection::Similarity(p) => Some(u32::from(p)),
201 }
202 }
203
204 #[must_use]
206 pub fn rename_threshold_u32(&self) -> u32 {
207 self.rename_similarity_threshold()
208 .unwrap_or(RENAME_DETECTION_DISABLED)
209 }
210
211 #[must_use]
213 pub fn with_word_level_inline(mut self, word_level_inline: bool) -> Self {
214 self.word_level_inline = word_level_inline;
215 self
216 }
217
218 #[must_use]
220 pub fn with_whitespace(mut self, whitespace: WhitespaceMode) -> Self {
221 self.whitespace = whitespace;
222 self
223 }
224
225 #[must_use]
228 pub fn cap_word_level_inline_for_changed_files(self, changed_files: usize) -> Self {
229 let mut opts = self;
230 if opts.word_level_inline && changed_files > WORD_LEVEL_INLINE_MAX_CHANGED_FILES {
231 opts.word_level_inline = false;
232 }
233 opts
234 }
235
236 #[must_use]
239 pub fn from_config(config: &ConfigSet) -> Self {
240 let mut opts = Self::default();
241 if let Some(name) = config.get("diff.algorithm") {
242 if let Ok(parsed) = name.parse::<DiffAlgorithm>() {
243 opts.algorithm = parsed;
244 }
245 }
246 if let Some(v) = config.get("diff.context") {
247 if let Ok(n) = v.parse::<usize>() {
248 opts.context_lines = n;
249 }
250 }
251 if let Some(v) = config.get("diff.interHunkContext") {
252 if let Ok(n) = v.parse::<usize>() {
253 opts.inter_hunk_context = n;
254 }
255 }
256 opts.indent_heuristic = indent_heuristic_from_config(config);
257 opts.renames = renames_from_config(config);
258 if config
259 .get_bool("diff.ignoreSpaceChange")
260 .and_then(|r| r.ok())
261 .unwrap_or(false)
262 {
263 opts.whitespace.ignore_space_change = true;
264 }
265 opts
266 }
267
268 #[must_use]
270 pub fn resolve_indent_heuristic(
271 config: &ConfigSet,
272 cli_indent_heuristic: bool,
273 cli_no_indent_heuristic: bool,
274 ) -> bool {
275 resolve_indent_heuristic(config, cli_indent_heuristic, cli_no_indent_heuristic)
276 }
277}
278
279#[must_use]
281pub fn renames_from_config(config: &ConfigSet) -> RenameDetection {
282 match config.get("diff.renames") {
283 Some(v) => {
284 let v = v.trim();
285 if v.eq_ignore_ascii_case("false")
286 || v.eq_ignore_ascii_case("no")
287 || v.eq_ignore_ascii_case("off")
288 || v == "0"
289 {
290 RenameDetection::Off
291 } else {
292 RenameDetection::default()
293 }
294 }
295 None => RenameDetection::default(),
296 }
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302 use crate::config::{ConfigFile, ConfigScope, ConfigSet};
303
304 fn config_from(snippet: &str) -> ConfigSet {
305 let file = ConfigFile::parse(std::path::Path::new("test"), snippet, ConfigScope::Local)
306 .expect("parse");
307 let mut set = ConfigSet::new();
308 set.merge(&file);
309 set
310 }
311
312 #[test]
313 fn diff_algorithm_from_str_roundtrip_and_error() {
314 for (s, expected) in [
315 ("myers", DiffAlgorithm::Myers),
316 ("default", DiffAlgorithm::Myers),
317 ("MYERS", DiffAlgorithm::Myers),
318 ("minimal", DiffAlgorithm::Minimal),
319 ("patience", DiffAlgorithm::Patience),
320 ("histogram", DiffAlgorithm::Histogram),
321 ] {
322 assert_eq!(s.parse::<DiffAlgorithm>().expect("parse"), expected);
323 }
324 assert!(matches!(
325 "bogus".parse::<DiffAlgorithm>(),
326 Err(UnknownDiffAlgorithm(_))
327 ));
328 }
329
330 #[test]
331 fn diff_options_from_config_reads_algorithm_context_renames() {
332 let cfg = config_from(
333 "[diff]\n\
334 \talgorithm = patience\n\
335 \tcontext = 5\n\
336 \tinterHunkContext = 2\n\
337 \tindentHeuristic = false\n\
338 \trenames = false\n",
339 );
340 let opts = DiffOptions::from_config(&cfg);
341 assert_eq!(opts.algorithm, DiffAlgorithm::Patience);
342 assert_eq!(opts.context_lines, 5);
343 assert_eq!(opts.inter_hunk_context, 2);
344 assert!(!opts.indent_heuristic);
345 assert_eq!(opts.renames, RenameDetection::Off);
346
347 let cfg2 = config_from("[diff]\n\trenames = copies\n");
348 assert_eq!(
349 DiffOptions::from_config(&cfg2).renames,
350 RenameDetection::default()
351 );
352
353 for off in ["no", "0", "off", "OFF"] {
354 let cfg = config_from(&format!("[diff]\n\trenames = {off}\n"));
355 assert_eq!(
356 DiffOptions::from_config(&cfg).renames,
357 RenameDetection::Off,
358 "renames={off}"
359 );
360 }
361 }
362
363 #[test]
364 fn cap_word_level_inline_for_changed_files() {
365 let on = DiffOptions {
366 word_level_inline: true,
367 ..DiffOptions::default()
368 };
369 assert!(
370 on.clone()
371 .cap_word_level_inline_for_changed_files(WORD_LEVEL_INLINE_MAX_CHANGED_FILES)
372 .word_level_inline
373 );
374 assert!(
375 !on.cap_word_level_inline_for_changed_files(WORD_LEVEL_INLINE_MAX_CHANGED_FILES + 1)
376 .word_level_inline
377 );
378 }
379}