1use clap::Args;
23use serde::{Deserialize, Serialize};
24
25#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
30pub enum ContextMode {
31 Symmetric(usize),
33 Asymmetric {
35 before: usize,
37 after: usize,
39 },
40 All,
42}
43
44impl ContextMode {
45 #[must_use]
49 pub const fn lines(&self) -> Option<(usize, usize)> {
50 match self {
51 Self::Symmetric(n) => Some((*n, *n)),
52 Self::Asymmetric { before, after } => Some((*before, *after)),
53 Self::All => None,
54 }
55 }
56
57 #[must_use]
59 pub const fn is_all(&self) -> bool {
60 matches!(self, Self::All)
61 }
62
63 #[must_use]
65 pub fn merge(self, other: Self) -> Self {
66 match (self, other) {
67 (Self::All, _) | (_, Self::All) => Self::All,
69 (a, b) => {
71 let (a_before, a_after) = a.lines().unwrap_or((0, 0));
72 let (b_before, b_after) = b.lines().unwrap_or((0, 0));
73 let before = a_before.max(b_before);
74 let after = a_after.max(b_after);
75 if before == after {
76 Self::Symmetric(before)
77 } else {
78 Self::Asymmetric { before, after }
79 }
80 },
81 }
82 }
83}
84
85impl std::str::FromStr for ContextMode {
86 type Err = String;
87
88 fn from_str(s: &str) -> Result<Self, Self::Err> {
89 if s.eq_ignore_ascii_case("all") {
90 Ok(Self::All)
91 } else {
92 s.parse::<usize>()
93 .map(Self::Symmetric)
94 .map_err(|_| format!("Invalid context value: '{s}'. Expected a number or 'all'"))
95 }
96 }
97}
98
99impl std::fmt::Display for ContextMode {
100 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101 match self {
102 Self::Symmetric(n) => write!(f, "{n}"),
103 Self::Asymmetric { before, after } => write!(f, "B{before}:A{after}"),
104 Self::All => write!(f, "all"),
105 }
106 }
107}
108
109#[derive(Args, Clone, Debug, Default, PartialEq, Eq)]
133pub struct ContextArgs {
134 #[arg(
143 short = 'C',
144 long = "context",
145 value_name = "LINES",
146 display_order = 30
147 )]
148 pub context: Option<ContextMode>,
149
150 #[arg(
158 short = 'A',
159 long = "after-context",
160 value_name = "LINES",
161 display_order = 31
162 )]
163 pub after_context: Option<usize>,
164
165 #[arg(
173 short = 'B',
174 long = "before-context",
175 value_name = "LINES",
176 display_order = 32
177 )]
178 pub before_context: Option<usize>,
179}
180
181impl ContextArgs {
182 #[must_use]
184 pub const fn symmetric(lines: usize) -> Self {
185 Self {
186 context: Some(ContextMode::Symmetric(lines)),
187 after_context: None,
188 before_context: None,
189 }
190 }
191
192 #[must_use]
194 pub const fn all() -> Self {
195 Self {
196 context: Some(ContextMode::All),
197 after_context: None,
198 before_context: None,
199 }
200 }
201
202 #[must_use]
209 pub fn resolve(&self) -> Option<ContextMode> {
210 let mut result = self.context.clone();
212
213 if let Some(after) = self.after_context {
215 let new_mode = self
216 .before_context
217 .map_or(ContextMode::Asymmetric { before: 0, after }, |before| {
218 ContextMode::Asymmetric { before, after }
219 });
220
221 result = Some(match result.take() {
222 Some(existing) => existing.merge(new_mode),
223 None => new_mode,
224 });
225 } else if let Some(before) = self.before_context {
226 let new_mode = ContextMode::Asymmetric { before, after: 0 };
228 result = Some(match result.take() {
229 Some(existing) => existing.merge(new_mode),
230 None => new_mode,
231 });
232 }
233
234 result
235 }
236
237 #[must_use]
239 pub const fn has_context(&self) -> bool {
240 self.context.is_some() || self.after_context.is_some() || self.before_context.is_some()
241 }
242}
243
244#[cfg(test)]
245#[allow(clippy::unwrap_used)]
246mod tests {
247 use super::*;
248
249 mod context_mode {
250 use super::*;
251
252 #[test]
253 fn test_symmetric_lines() {
254 let mode = ContextMode::Symmetric(5);
255 assert_eq!(mode.lines(), Some((5, 5)));
256 }
257
258 #[test]
259 fn test_asymmetric_lines() {
260 let mode = ContextMode::Asymmetric {
261 before: 3,
262 after: 7,
263 };
264 assert_eq!(mode.lines(), Some((3, 7)));
265 }
266
267 #[test]
268 fn test_all_lines() {
269 let mode = ContextMode::All;
270 assert_eq!(mode.lines(), None);
271 }
272
273 #[test]
274 fn test_is_all() {
275 assert!(ContextMode::All.is_all());
276 assert!(!ContextMode::Symmetric(5).is_all());
277 assert!(
278 !ContextMode::Asymmetric {
279 before: 1,
280 after: 1
281 }
282 .is_all()
283 );
284 }
285
286 #[test]
287 fn test_merge_all_precedence() {
288 let sym = ContextMode::Symmetric(5);
289 let all = ContextMode::All;
290
291 assert!(matches!(sym.clone().merge(all.clone()), ContextMode::All));
292 assert!(matches!(all.merge(sym), ContextMode::All));
293 }
294
295 #[test]
296 fn test_merge_symmetric() {
297 let a = ContextMode::Symmetric(3);
298 let b = ContextMode::Symmetric(5);
299
300 assert_eq!(a.merge(b), ContextMode::Symmetric(5));
301 }
302
303 #[test]
304 fn test_merge_asymmetric() {
305 let a = ContextMode::Asymmetric {
306 before: 3,
307 after: 2,
308 };
309 let b = ContextMode::Asymmetric {
310 before: 1,
311 after: 5,
312 };
313
314 assert_eq!(
315 a.merge(b),
316 ContextMode::Asymmetric {
317 before: 3,
318 after: 5
319 }
320 );
321 }
322
323 #[test]
324 fn test_parse_number() {
325 assert_eq!(
326 "5".parse::<ContextMode>().unwrap(),
327 ContextMode::Symmetric(5)
328 );
329 assert_eq!(
330 "0".parse::<ContextMode>().unwrap(),
331 ContextMode::Symmetric(0)
332 );
333 }
334
335 #[test]
336 fn test_parse_all() {
337 assert_eq!("all".parse::<ContextMode>().unwrap(), ContextMode::All);
338 assert_eq!("ALL".parse::<ContextMode>().unwrap(), ContextMode::All);
339 assert_eq!("All".parse::<ContextMode>().unwrap(), ContextMode::All);
340 }
341
342 #[test]
343 fn test_parse_invalid() {
344 assert!("abc".parse::<ContextMode>().is_err());
345 assert!("-1".parse::<ContextMode>().is_err());
346 }
347
348 #[test]
349 fn test_display() {
350 assert_eq!(ContextMode::Symmetric(5).to_string(), "5");
351 assert_eq!(
352 ContextMode::Asymmetric {
353 before: 2,
354 after: 3
355 }
356 .to_string(),
357 "B2:A3"
358 );
359 assert_eq!(ContextMode::All.to_string(), "all");
360 }
361 }
362
363 mod context_args {
364 use super::*;
365
366 #[test]
367 fn test_default() {
368 let args = ContextArgs::default();
369 assert_eq!(args.context, None);
370 assert_eq!(args.after_context, None);
371 assert_eq!(args.before_context, None);
372 assert!(!args.has_context());
373 }
374
375 #[test]
376 fn test_symmetric() {
377 let args = ContextArgs::symmetric(5);
378 assert_eq!(args.resolve(), Some(ContextMode::Symmetric(5)));
379 assert!(args.has_context());
380 }
381
382 #[test]
383 fn test_all() {
384 let args = ContextArgs::all();
385 assert_eq!(args.resolve(), Some(ContextMode::All));
386 assert!(args.has_context());
387 }
388
389 #[test]
390 fn test_resolve_only_after() {
391 let args = ContextArgs {
392 context: None,
393 after_context: Some(5),
394 before_context: None,
395 };
396 assert_eq!(
397 args.resolve(),
398 Some(ContextMode::Asymmetric {
399 before: 0,
400 after: 5
401 })
402 );
403 }
404
405 #[test]
406 fn test_resolve_only_before() {
407 let args = ContextArgs {
408 context: None,
409 after_context: None,
410 before_context: Some(5),
411 };
412 assert_eq!(
413 args.resolve(),
414 Some(ContextMode::Asymmetric {
415 before: 5,
416 after: 0
417 })
418 );
419 }
420
421 #[test]
422 fn test_resolve_both_before_after() {
423 let args = ContextArgs {
424 context: None,
425 after_context: Some(3),
426 before_context: Some(5),
427 };
428 assert_eq!(
429 args.resolve(),
430 Some(ContextMode::Asymmetric {
431 before: 5,
432 after: 3
433 })
434 );
435 }
436
437 #[test]
438 fn test_resolve_merge_with_context() {
439 let args = ContextArgs {
440 context: Some(ContextMode::Symmetric(2)),
441 after_context: Some(5),
442 before_context: None,
443 };
444 assert_eq!(
446 args.resolve(),
447 Some(ContextMode::Asymmetric {
448 before: 2,
449 after: 5
450 })
451 );
452 }
453 }
454}