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
54pub const RENAME_DETECTION_DISABLED: u32 = 101;
57
58#[non_exhaustive]
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum RenameDetection {
62 Off,
64 Similarity(u8),
66}
67
68impl Default for RenameDetection {
69 fn default() -> Self {
71 Self::Similarity(50)
72 }
73}
74
75impl RenameDetection {
76 #[must_use]
78 pub fn threshold_percent(self) -> Option<u32> {
79 match self {
80 Self::Off => None,
81 Self::Similarity(p) => Some(u32::from(p)),
82 }
83 }
84}
85
86pub const WORD_LEVEL_INLINE_MAX_CHANGED_FILES: usize = 64;
88
89#[non_exhaustive]
91#[derive(Debug, Clone)]
92pub struct DiffOptions {
93 pub context_lines: usize,
95 pub inter_hunk_context: usize,
97 pub algorithm: DiffAlgorithm,
99 pub indent_heuristic: bool,
101 pub renames: RenameDetection,
104 pub filter: Option<DiffFilter>,
106 pub whitespace: WhitespaceMode,
108 pub word_level_inline: bool,
110 pub function_context: bool,
112 pub(crate) no_index_stat_only: bool,
114}
115
116impl Default for DiffOptions {
117 fn default() -> Self {
118 Self {
119 context_lines: 3,
120 inter_hunk_context: 0,
121 algorithm: DiffAlgorithm::Myers,
122 indent_heuristic: true,
123 renames: RenameDetection::default(),
124 filter: None,
125 whitespace: WhitespaceMode::default(),
126 word_level_inline: true,
127 function_context: false,
128 no_index_stat_only: false,
129 }
130 }
131}
132
133impl PartialEq for DiffOptions {
134 fn eq(&self, other: &Self) -> bool {
135 self.context_lines == other.context_lines
136 && self.inter_hunk_context == other.inter_hunk_context
137 && self.algorithm == other.algorithm
138 && self.indent_heuristic == other.indent_heuristic
139 && self.renames == other.renames
140 && self.filter == other.filter
141 && self.whitespace == other.whitespace
142 && self.word_level_inline == other.word_level_inline
143 && self.function_context == other.function_context
144 && self.no_index_stat_only == other.no_index_stat_only
145 }
146}
147
148impl DiffOptions {
149 #[must_use]
151 pub fn with_context_lines(mut self, context_lines: usize) -> Self {
152 self.context_lines = context_lines;
153 self
154 }
155
156 #[must_use]
158 pub fn with_inter_hunk_context(mut self, inter_hunk_context: usize) -> Self {
159 self.inter_hunk_context = inter_hunk_context;
160 self
161 }
162
163 #[must_use]
165 pub fn with_algorithm(mut self, algorithm: DiffAlgorithm) -> Self {
166 self.algorithm = algorithm;
167 self
168 }
169
170 #[must_use]
172 pub fn with_indent_heuristic(mut self, indent_heuristic: bool) -> Self {
173 self.indent_heuristic = indent_heuristic;
174 self
175 }
176
177 #[must_use]
179 pub fn with_renames(mut self, renames: RenameDetection) -> Self {
180 self.renames = renames;
181 self
182 }
183
184 #[must_use]
186 pub fn with_filter(mut self, filter: Option<DiffFilter>) -> Self {
187 self.filter = filter;
188 self
189 }
190
191 #[must_use]
193 pub fn rename_similarity_threshold(&self) -> Option<u32> {
194 match self.renames {
195 RenameDetection::Off => None,
196 RenameDetection::Similarity(p) => Some(u32::from(p)),
197 }
198 }
199
200 #[must_use]
202 pub fn rename_threshold_u32(&self) -> u32 {
203 self.rename_similarity_threshold()
204 .unwrap_or(RENAME_DETECTION_DISABLED)
205 }
206
207 #[must_use]
209 pub fn with_word_level_inline(mut self, word_level_inline: bool) -> Self {
210 self.word_level_inline = word_level_inline;
211 self
212 }
213
214 #[must_use]
216 pub fn with_whitespace(mut self, whitespace: WhitespaceMode) -> Self {
217 self.whitespace = whitespace;
218 self
219 }
220
221 #[must_use]
223 pub fn with_no_index_stat_only(mut self, stat_only: bool) -> Self {
224 self.no_index_stat_only = stat_only;
225 self
226 }
227
228 #[must_use]
231 pub fn cap_word_level_inline_for_changed_files(self, changed_files: usize) -> Self {
232 let mut opts = self;
233 if opts.word_level_inline && changed_files > WORD_LEVEL_INLINE_MAX_CHANGED_FILES {
234 opts.word_level_inline = false;
235 }
236 opts
237 }
238
239 #[must_use]
242 pub fn from_config(config: &ConfigSet) -> Self {
243 let mut opts = Self::default();
244 if let Some(name) = config.get("diff.algorithm") {
245 if let Ok(parsed) = name.parse::<DiffAlgorithm>() {
246 opts.algorithm = parsed;
247 }
248 }
249 if let Some(v) = config.get("diff.context") {
250 if let Ok(n) = v.parse::<usize>() {
251 opts.context_lines = n;
252 }
253 }
254 if let Some(v) = config.get("diff.interHunkContext") {
255 if let Ok(n) = v.parse::<usize>() {
256 opts.inter_hunk_context = n;
257 }
258 }
259 opts.indent_heuristic = indent_heuristic_from_config(config);
260 opts.renames = renames_from_config(config);
261 if config.bool_or("diff.ignoreSpaceChange", false) {
262 opts.whitespace.ignore_space_change = true;
263 }
264 opts
265 }
266
267 #[must_use]
269 pub fn resolve_indent_heuristic(
270 config: &ConfigSet,
271 cli_indent_heuristic: bool,
272 cli_no_indent_heuristic: bool,
273 ) -> bool {
274 resolve_indent_heuristic(config, cli_indent_heuristic, cli_no_indent_heuristic)
275 }
276}
277
278#[must_use]
280pub fn renames_from_config(config: &ConfigSet) -> RenameDetection {
281 match config.get("diff.renames") {
282 Some(v) => {
283 let v = v.trim();
284 if v.eq_ignore_ascii_case("false")
285 || v.eq_ignore_ascii_case("no")
286 || v.eq_ignore_ascii_case("off")
287 || v == "0"
288 {
289 RenameDetection::Off
290 } else {
291 RenameDetection::default()
292 }
293 }
294 None => RenameDetection::default(),
295 }
296}
297
298#[cfg(test)]
299mod tests {
300 use super::*;
301 use crate::config::{ConfigFile, ConfigScope, ConfigSet};
302
303 fn config_from(snippet: &str) -> ConfigSet {
304 let file = ConfigFile::parse(std::path::Path::new("test"), snippet, ConfigScope::Local)
305 .expect("parse");
306 let mut set = ConfigSet::new();
307 set.merge(&file);
308 set
309 }
310
311 #[test]
312 fn diff_algorithm_from_str_roundtrip_and_error() {
313 for (s, expected) in [
314 ("myers", DiffAlgorithm::Myers),
315 ("default", DiffAlgorithm::Myers),
316 ("MYERS", DiffAlgorithm::Myers),
317 ("minimal", DiffAlgorithm::Minimal),
318 ("patience", DiffAlgorithm::Patience),
319 ("histogram", DiffAlgorithm::Histogram),
320 ] {
321 assert_eq!(s.parse::<DiffAlgorithm>().expect("parse"), expected);
322 }
323 assert!(matches!(
324 "bogus".parse::<DiffAlgorithm>(),
325 Err(UnknownDiffAlgorithm(_))
326 ));
327 }
328
329 #[test]
330 fn diff_options_from_config_reads_algorithm_context_renames() {
331 let cfg = config_from(
332 "[diff]\n\
333 \talgorithm = patience\n\
334 \tcontext = 5\n\
335 \tinterHunkContext = 2\n\
336 \tindentHeuristic = false\n\
337 \trenames = false\n",
338 );
339 let opts = DiffOptions::from_config(&cfg);
340 assert_eq!(opts.algorithm, DiffAlgorithm::Patience);
341 assert_eq!(opts.context_lines, 5);
342 assert_eq!(opts.inter_hunk_context, 2);
343 assert!(!opts.indent_heuristic);
344 assert_eq!(opts.renames, RenameDetection::Off);
345
346 for on in [
347 "",
348 "[diff]\n\trenames = true\n",
349 "[diff]\n\trenames = copies\n",
350 ] {
351 assert_eq!(
352 DiffOptions::from_config(&config_from(on)).renames,
353 RenameDetection::Similarity(50),
354 "config {on:?}"
355 );
356 }
357 assert_eq!(
358 DiffOptions::default().renames,
359 RenameDetection::Similarity(50)
360 );
361
362 for off in ["no", "0", "off", "OFF"] {
363 let cfg = config_from(&format!("[diff]\n\trenames = {off}\n"));
364 assert_eq!(
365 DiffOptions::from_config(&cfg).renames,
366 RenameDetection::Off,
367 "renames={off}"
368 );
369 }
370 }
371
372 #[test]
373 fn cap_word_level_inline_for_changed_files() {
374 let on = DiffOptions {
375 word_level_inline: true,
376 ..DiffOptions::default()
377 };
378 assert!(
379 on.clone()
380 .cap_word_level_inline_for_changed_files(WORD_LEVEL_INLINE_MAX_CHANGED_FILES)
381 .word_level_inline
382 );
383 assert!(
384 !on.cap_word_level_inline_for_changed_files(WORD_LEVEL_INLINE_MAX_CHANGED_FILES + 1)
385 .word_level_inline
386 );
387 }
388}