1use crate::documentation::{CheckableChunk, ContentOrigin};
15
16use std::cmp;
17use std::convert::TryFrom;
18
19use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator};
20
21use crate::{Range, Span};
22
23#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
25pub enum Detector {
26 Hunspell,
28 ZSpell,
30 Spellbook,
32 NlpRules,
34 Reflow,
36 #[cfg(test)]
38 Dummy,
39}
40
41impl Detector {
42 pub const fn as_str(&self) -> &'static str {
44 match self {
45 Self::Hunspell => "Hunspell",
46 Self::ZSpell => "ZSpell",
47 Self::Spellbook => "Spellbook",
48 Self::NlpRules => "NlpRules",
49 Self::Reflow => "Reflow",
50 #[cfg(test)]
51 Self::Dummy => "Dummy",
52 }
53 }
54}
55
56pub fn get_terminal_size() -> usize {
61 const DEFAULT_TERMINAL_SIZE: usize = 80;
62 #[cfg(not(test))]
63 match crossterm::terminal::size() {
64 Ok((terminal_size, _)) => terminal_size as usize,
65 Err(_) => {
66 use std::sync::Once;
67
68 static WARN_ONCE: Once = Once::new();
69 WARN_ONCE.call_once(|| {
70 log::warn!("Unable to get terminal size. Using default: {DEFAULT_TERMINAL_SIZE}");
71 });
72
73 DEFAULT_TERMINAL_SIZE
74 }
75 }
76 #[cfg(test)]
77 DEFAULT_TERMINAL_SIZE
78}
79
80use std::fmt;
90
91impl fmt::Display for Detector {
92 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
93 formatter.write_str(self.as_str())
94 }
95}
96
97pub fn condition_display_content(
100 terminal_size: usize,
101 _indent: usize,
102 stripped_line: &str,
103 mistake_range: Range,
104 terminal_print_offset_left: usize,
105 marker_size: usize,
106) -> (String, usize, usize) {
107 if stripped_line.chars().count() + terminal_print_offset_left <= terminal_size {
109 return (stripped_line.to_owned(), mistake_range.start, marker_size);
110 }
111
112 const MAX_MISTAKE_LEN: usize = 20;
126
127 const HEAD_DISPLAY_LEN: usize = 4;
128 const TAIL_DISPLAY_LEN: usize = 4;
129
130 const CENTER_DOTS: &str = "...";
131 const LEFT_DOTS: &str = "..";
132 const RIGHT_DOTS: &str = LEFT_DOTS;
133 const NO_DOTS: &str = "";
134
135 assert!(HEAD_DISPLAY_LEN + CENTER_DOTS.len() + TAIL_DISPLAY_LEN <= MAX_MISTAKE_LEN);
137
138 const TOTAL_CONTEXT_CHAR_COUNT: usize = 6;
140
141 let (marker_size, shortened) = if mistake_range.len() > MAX_MISTAKE_LEN {
151 let head_sub_range = Range {
152 start: mistake_range.start,
153 end: mistake_range.start + HEAD_DISPLAY_LEN,
154 };
155 let tail_sub_range = Range {
156 start: mistake_range
157 .end .saturating_sub(TAIL_DISPLAY_LEN),
159 end: mistake_range.end,
160 };
161
162 let head_sub = stripped_line
170 .chars()
171 .skip(head_sub_range.start)
172 .take(HEAD_DISPLAY_LEN)
173 .collect::<String>();
174 let tail_sub = stripped_line
175 .chars()
176 .skip(tail_sub_range.start)
177 .take(TAIL_DISPLAY_LEN)
178 .collect::<String>();
179
180 let shortened = format!("{head_sub}...{tail_sub}");
181 let marker_size = head_sub_range.len() + CENTER_DOTS.len() + tail_sub_range.len();
184
185 (marker_size, shortened)
186 } else {
187 let full: String = stripped_line
188 .chars()
189 .skip(mistake_range.start)
190 .take(mistake_range.len())
191 .collect();
192 (marker_size, full)
193 };
194
195 let stripped_line_len = stripped_line.chars().count();
196
197 let left_context = Range {
199 start: 0,
200 end: mistake_range.start,
201 };
202 let right_context = Range {
203 start: mistake_range.end,
204 end: stripped_line_len,
205 };
206
207 let avail_space = terminal_size
208 .saturating_sub(terminal_print_offset_left + marker_size + TOTAL_CONTEXT_CHAR_COUNT);
209
210 let avail_space_half = avail_space / 2usize;
212
213 let (left_context, right_context, left_dots, right_dots) = match (
215 avail_space_half > left_context.len(),
216 avail_space_half > right_context.len(),
217 ) {
218 (true, false) => {
219 let right_avail_space = avail_space - left_context.len();
222 let rdots = if mistake_range.end + right_avail_space < stripped_line_len {
223 NO_DOTS
224 } else {
225 RIGHT_DOTS
226 };
227 (
228 left_context,
229 Range {
230 start: right_context.end,
231 end: cmp::min(mistake_range.end + right_avail_space, stripped_line_len),
232 },
233 NO_DOTS,
234 rdots,
235 )
236 }
237 (false, true) => {
238 let left_avail_space = avail_space - right_context.len();
241 let ldots = if left_avail_space > left_context.end {
242 NO_DOTS
243 } else {
244 LEFT_DOTS
245 };
246 (
247 Range {
248 start: left_context.end.saturating_sub(left_avail_space),
249 end: left_context.end,
250 },
251 right_context,
252 ldots,
253 NO_DOTS,
254 )
255 }
256 (false, false) => {
257 (
259 Range {
260 start: left_context.end.saturating_sub(avail_space_half),
261 end: left_context.end,
262 },
263 Range {
264 start: right_context.start,
265 end: right_context.start + avail_space_half,
266 },
267 LEFT_DOTS,
268 RIGHT_DOTS,
269 )
270 }
271 _ => {
272 (left_context, right_context, NO_DOTS, NO_DOTS)
274 }
275 };
276
277 assert!(left_context.end == mistake_range.start);
278 assert!(right_context.end <= stripped_line_len);
279 assert!(left_context.len() + mistake_range.len() + right_context.len() <= stripped_line_len);
280
281 let offset = left_context.len();
282 let conditioned_line = format!(
283 "{}{}{}{}{}",
284 left_dots,
285 stripped_line
286 .chars()
287 .skip(left_context.start + left_dots.len())
288 .take(left_context.len() - left_dots.len())
289 .collect::<String>(),
290 shortened,
291 stripped_line
292 .chars()
293 .skip(right_context.start)
294 .take(right_context.len() - right_dots.len())
295 .collect::<String>(),
296 right_dots,
297 );
298 (conditioned_line, offset, marker_size)
299}
300
301#[derive(Clone, Hash, PartialEq, Eq)]
303pub struct Suggestion<'s> {
304 pub detector: Detector,
306 pub origin: ContentOrigin,
308 pub chunk: &'s CheckableChunk,
310 pub span: Span,
312 pub range: Range,
314 pub replacements: Vec<String>,
317 pub description: Option<String>,
319}
320
321impl<'s> Suggestion<'s> {
322 pub fn is_overlapped(&self, other: &Self) -> bool {
324 if self.origin != other.origin {
325 return false;
326 }
327
328 if self.span.end.line < other.span.start.line || other.span.end.line < self.span.start.line
329 {
330 return false;
331 }
332
333 if self.span.start.line < other.span.start.line
334 || (self.span.start.line == other.span.start.line
335 && self.span.start.column < other.span.start.column)
336 {
337 self.span.end.column > other.span.start.column
338 } else {
339 self.span.start.column < other.span.end.column
340 }
341 }
342}
343
344impl<'s> fmt::Display for Suggestion<'s> {
345 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
346 use console::Style;
347
348 let highlight = Style::new().bold().white();
349 let error = Style::new().bold().red();
350 let arrow_marker = Style::new().blue();
351 let context_marker = Style::new().bold().blue();
352 let fix = Style::new().green();
353 let help = Style::new().yellow().bold();
354
355 let line_number_digit_count = self.span.start.line.to_string().len();
356 let indent = 3 + line_number_digit_count;
357
358 error.apply_to("error").fmt(formatter)?;
359 highlight
360 .apply_to(format!(": spellcheck({})", self.detector))
361 .fmt(formatter)?;
362 formatter.write_str("\n")?;
363
364 arrow_marker
365 .apply_to(format!("{:>width$}", "-->", width = indent + 1))
366 .fmt(formatter)?;
367
368 let x = self.span.start.line;
369 let (path, line) = match self.origin {
370 ContentOrigin::RustDocTest(ref path, ref span) => {
371 (path.display().to_string(), x + span.start.line)
372 }
373 ref origin => (origin.as_path().display().to_string(), x),
374 };
375 writeln!(formatter, " {path}:{line}", path = path, line = line)?;
376 context_marker
377 .apply_to(format!("{:>width$}", "|", width = indent))
378 .fmt(formatter)?;
379 formatter.write_str("\n")?;
380 context_marker
381 .apply_to(format!(
382 "{:>width$} |",
383 self.span.start.line,
384 width = indent - 2,
385 ))
386 .fmt(formatter)?;
387
388 let marker_size = self.span.one_line_len().unwrap_or_else(|| {
392 self.chunk
393 .len_in_chars()
394 .saturating_sub(self.span.start.column)
395 });
396
397 let mistake_lines = self.chunk.find_covered_lines(self.range.clone());
400 let (line_range, start_of_line_offset) = mistake_lines
401 .first()
402 .map(|line_range| {
403 (
404 line_range,
405 self.range.start.saturating_sub(line_range.start),
406 )
407 })
408 .expect("Lines covered must exist");
409
410 let intra_line_mistake_range = Range {
411 start: start_of_line_offset,
412 end: cmp::min(start_of_line_offset + self.range.len(), line_range.len()),
413 };
414 let relevant_line = self
415 .chunk
416 .as_str()
417 .chars()
418 .enumerate()
419 .skip_while(|(idx, _)| line_range.start > *idx)
420 .take(line_range.len())
421 .map(|(_, c)| c)
422 .collect::<String>();
423
424 let terminal_size = get_terminal_size();
425
426 let padding_till_excerpt_start = indent + 2;
429
430 let (formatted, offset, marker_size) = condition_display_content(
431 terminal_size,
432 indent,
433 relevant_line.as_str(),
434 intra_line_mistake_range,
435 padding_till_excerpt_start,
436 marker_size,
437 );
438
439 writeln!(formatter, " {formatted}")?;
440
441 if marker_size > 0 {
442 context_marker
443 .apply_to(format!("{:>width$}", "|", width = indent))
444 .fmt(formatter)?;
445 help.apply_to(format!(" {:>offset$}", "", offset = offset))
446 .fmt(formatter)?;
447 help.apply_to(format!("{:^>size$}", "", size = marker_size))
448 .fmt(formatter)?;
449 formatter.write_str("\n")?;
450 log::trace!(
451 "marker_size={} span {{ {:?} .. {:?} }} >> {:?} <<",
452 marker_size,
453 self.span.start,
454 self.span.end,
455 self,
456 );
457 } else {
458 log::warn!(
459 "marker_size={} span {{ {:?} .. {:?} }} >> {:?} <<",
460 marker_size,
461 self.span.start,
462 self.span.end,
463 self,
464 );
465 }
466
467 context_marker
468 .apply_to(format!("{:>width$}", "|", width = indent))
469 .fmt(formatter)?;
470
471 let replacement = match self.replacements.len() {
472 0 => String::new(),
473 1 => format!(" - {}", fix.apply_to(&self.replacements[0])),
474 2 => format!(
475 " - {} or {}",
476 fix.apply_to(&self.replacements[0]),
477 fix.apply_to(&self.replacements[1])
478 ),
479 n if (n < 7) => {
480 let last = fix.apply_to(&self.replacements[n - 1]).to_string();
481 let joined = self.replacements[..n - 1]
482 .iter()
483 .map(|x| fix.apply_to(x.to_owned()).to_string())
484 .collect::<Vec<String>>()
485 .as_slice()
486 .join(", ");
487 format!(" - {joined}, or {last}")
488 }
489 _n => {
490 let joined = self.replacements[..=6]
491 .iter()
492 .map(|x| fix.apply_to(x.to_owned()).to_string())
493 .collect::<Vec<String>>()
494 .as_slice()
495 .join(", ");
496
497 let remaining = self.replacements.len() - 6;
498 let remaining = fix.apply_to(format!("{remaining}")).to_string();
499 format!(" - {joined}, or one of {remaining} others")
500 }
501 };
502
503 error.apply_to(replacement).fmt(formatter)?;
504
505 if !self.replacements.is_empty() {
506 formatter.write_str("\n")?;
507 context_marker
508 .apply_to(format!("{:>width$}", "|\n", width = indent + 1))
509 .fmt(formatter)?;
510 context_marker
511 .apply_to(format!("{:>width$}", "|", width = indent))
512 .fmt(formatter)?;
513 }
514
515 if let Some(ref description) = self.description {
516 writeln!(formatter, " {description}")?;
517 }
518 Ok(())
519 }
520}
521
522impl<'s> fmt::Debug for Suggestion<'s> {
523 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
524 match crate::documentation::ChunkDisplay::try_from((self.chunk, self.span)) {
525 Ok(printable) => write!(
526 formatter,
527 "({}, {:?}, {:?})",
528 printable,
529 printable.1,
530 self.replacements.as_slice()
531 ),
532 Err(e) => {
533 writeln!(formatter, "> span={:?}", self.span)?;
534 writeln!(
535 formatter,
536 "> Failed to create chunk display from chunk={:?} with {}",
537 self.chunk, e
538 )
539 }
540 }
541 }
542}
543
544impl<'s> Ord for Suggestion<'s> {
545 fn cmp(&self, other: &Self) -> cmp::Ordering {
546 let cmp = self.span.start.cmp(&other.span.start);
547 if cmp != std::cmp::Ordering::Equal {
548 return cmp;
549 }
550
551 self.span.end.cmp(&other.span.end)
552 }
553}
554
555impl<'s> PartialOrd for Suggestion<'s> {
556 fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
557 Some(self.cmp(other))
558 }
559}
560
561#[derive(Debug, Clone)]
563pub struct SuggestionSet<'s> {
564 per_file: indexmap::IndexMap<ContentOrigin, Vec<Suggestion<'s>>>,
565}
566
567impl<'s> SuggestionSet<'s> {
568 pub fn new() -> Self {
570 Self {
571 per_file: indexmap::IndexMap::with_capacity(64),
572 }
573 }
574
575 pub fn iter<'a>(
578 &'a self,
579 ) -> impl DoubleEndedIterator<Item = (&'a ContentOrigin, &'a Vec<Suggestion<'s>>)> {
580 self.per_file.iter()
581 }
582
583 pub fn add(&mut self, origin: ContentOrigin, suggestion: Suggestion<'s>) {
585 self.per_file
586 .entry(origin)
587 .or_insert_with(|| Vec::with_capacity(1))
588 .push(suggestion);
589 }
590
591 pub fn append(&mut self, origin: ContentOrigin, suggestions: &[Suggestion<'s>]) {
593 self.per_file
594 .entry(origin)
595 .or_insert_with(|| Vec::with_capacity(32))
596 .extend_from_slice(suggestions);
597 }
598
599 pub fn extend<I>(&mut self, origin: ContentOrigin, suggestions: I)
601 where
602 I: IntoIterator<Item = Suggestion<'s>>,
603 {
604 let v: &mut Vec<Suggestion<'s>> = self
605 .per_file
606 .entry(origin)
607 .or_insert_with(|| Vec::with_capacity(32));
608 v.extend(suggestions);
609 }
610
611 pub fn entry(
613 &mut self,
614 origin: ContentOrigin,
615 ) -> indexmap::map::Entry<'_, ContentOrigin, Vec<Suggestion<'s>>> {
616 self.per_file.entry(origin)
617 }
618
619 pub fn files<'i, 'a>(&'a mut self) -> impl DoubleEndedIterator<Item = &'i ContentOrigin>
621 where
622 's: 'i,
623 'a: 'i,
624 {
625 self.per_file.keys()
626 }
627
628 pub fn suggestions<'a>(
632 &'a self,
633 origin: &ContentOrigin,
634 ) -> impl DoubleEndedIterator<Item = &'a Suggestion<'s>>
635 where
636 'a: 's,
637 {
638 if let Some(suggestions) = self.per_file.get(origin) {
639 suggestions.iter()
640 } else {
641 panic!("origin must exist")
642 }
643 }
648
649 pub fn join<I>(&mut self, other: I)
653 where
654 I: IntoIterator<Item = (ContentOrigin, Vec<Suggestion<'s>>)>,
655 {
656 other.into_iter().for_each(|(origin, suggestions)| {
657 self.entry(origin)
658 .or_insert_with(|| Vec::with_capacity(suggestions.len()))
659 .extend_from_slice(suggestions.as_slice())
660 })
661 }
662
663 #[inline]
665 pub fn len(&self) -> usize {
666 self.per_file.len()
667 }
668
669 #[inline]
671 pub fn is_empty(&self) -> bool {
672 self.per_file.is_empty()
673 }
674
675 pub fn sort(&mut self) {
678 self.per_file
679 .par_iter_mut()
680 .for_each(|(_origin, suggestions)| {
681 suggestions.sort_by(|a, b| {
682 let cmp = a.span.start.cmp(&b.span.start);
683 if cmp != std::cmp::Ordering::Equal {
684 return cmp;
685 }
686
687 a.span.end.cmp(&b.span.end)
688 });
689 });
690 self.per_file
691 .sort_by(|origin_a, _a, origin_b, _b| -> std::cmp::Ordering {
692 origin_a.as_path().cmp(origin_b.as_path())
693 });
694 }
695
696 pub fn total_count(&self) -> usize {
698 self.per_file.iter().map(|(_origin, vec)| vec.len()).sum()
699 }
700}
701
702impl<'s> IntoIterator for SuggestionSet<'s> {
703 type Item = (ContentOrigin, Vec<Suggestion<'s>>);
704 type IntoIter = indexmap::map::IntoIter<ContentOrigin, Vec<Suggestion<'s>>>;
705 fn into_iter(self) -> Self::IntoIter {
706 self.per_file.into_iter()
707 }
708}
709
710impl<'s> IntoIterator for &'s SuggestionSet<'s> {
711 type Item = (&'s ContentOrigin, &'s Vec<Suggestion<'s>>);
712 type IntoIter = indexmap::map::Iter<'s, ContentOrigin, Vec<Suggestion<'s>>>;
713 fn into_iter(self) -> Self::IntoIter {
714 self.per_file.iter()
715 }
716}
717
718#[cfg(test)]
719mod tests {
720 use super::*;
721 use crate::{CommentVariant, LineColumn};
722 use console;
723 use std::fmt;
724
725 fn assert_display_eq<D: fmt::Display, S: AsRef<str>>(display: D, s: S) {
730 let expected = s.as_ref();
731 let expected = console::strip_ansi_codes(expected);
732
733 let reality = display.to_string();
735 let reality = console::strip_ansi_codes(reality.as_str());
736 assert_eq!(reality, expected);
737 }
738
739 #[test]
740 fn fmt_0_single() {
741 const CONTENT: &str = " Is it dyrck again?";
742 let chunk = CheckableChunk::from_str(
743 CONTENT,
744 indexmap::indexmap! { 0..18 => Span {
745 start: LineColumn {
746 line: 1,
747 column: 0,
748 },
749 end: LineColumn {
750 line: 1,
751 column: 17,
752 }
753 }
754 },
755 CommentVariant::TripleSlash,
756 );
757
758 let suggestion = Suggestion {
759 detector: Detector::Dummy,
760 origin: ContentOrigin::TestEntityRust,
761 chunk: &chunk,
762 range: 7..12,
763 span: Span {
764 start: LineColumn { line: 1, column: 6 },
765 end: LineColumn {
766 line: 1,
767 column: 10,
768 },
769 },
770 replacements: vec![
771 "replacement_0".to_owned(),
772 "replacement_1".to_owned(),
773 "replacement_2".to_owned(),
774 ],
775 description: Some("Possible spelling mistake found.".to_owned()),
776 };
777
778 const EXPECTED: &str = r#"error: spellcheck(Dummy)
779 --> /tmp/test/entity.rs:1
780 |
781 1 | Is it dyrck again?
782 | ^^^^^
783 | - replacement_0, replacement_1, or replacement_2
784 |
785 | Possible spelling mistake found.
786"#;
787 assert_display_eq(suggestion, EXPECTED);
788 }
789
790 #[test]
791 fn fmt_0_no_suggestion() {
792 const CONTENT: &str = " Is it dyrck again?";
793 let chunk = CheckableChunk::from_str(
794 CONTENT,
795 indexmap::indexmap! { 0..18 => Span {
796 start: LineColumn {
797 line: 1,
798 column: 0,
799 },
800 end: LineColumn {
801 line: 1,
802 column: 17,
803 }
804 }
805 },
806 CommentVariant::TripleSlash,
807 );
808
809 let suggestion = Suggestion {
810 detector: Detector::Dummy,
811 origin: ContentOrigin::TestEntityRust,
812 chunk: &chunk,
813 range: 7..12,
814 span: Span {
815 start: LineColumn { line: 1, column: 6 },
816 end: LineColumn {
817 line: 1,
818 column: 10,
819 },
820 },
821 replacements: vec![],
822 description: Some("Possible spelling mistake found.".to_owned()),
823 };
824
825 const EXPECTED: &str = r#"error: spellcheck(Dummy)
826 --> /tmp/test/entity.rs:1
827 |
828 1 | Is it dyrck again?
829 | ^^^^^
830 | Possible spelling mistake found.
831"#;
832 assert_display_eq(suggestion, EXPECTED);
833 }
834
835 #[test]
836 fn fmt_1_multi() {
837 const CONTENT: &str = r#" Line mitake 1
838 Anowher 2
839 Last"#;
840
841 let chunk = CheckableChunk::from_str(
842 CONTENT,
843 indexmap::indexmap! {
844 0..13 => Span {
845 start: LineColumn {
846 line: 1,
847 column: 4,
848 },
849 end: LineColumn {
850 line: 1,
851 column: 16,
852 }
853 },
854 14..24 => Span {
855 start: LineColumn {
856 line: 2,
857 column: 4,
858 },
859 end: LineColumn {
860 line: 2,
861 column: 12,
862 }
863 },
864 25..29 => Span {
865 start: LineColumn {
866 line: 3,
867 column: 4,
868 },
869 end: LineColumn {
870 line: 3,
871 column: 7,
872 }
873 }
874 },
875 CommentVariant::TripleSlash,
876 );
877
878 let suggestion = Suggestion {
879 detector: Detector::Dummy,
880 origin: ContentOrigin::TestEntityRust,
881 chunk: &chunk,
882 range: 6..12,
883 span: Span {
884 start: LineColumn {
885 line: 1,
886 column: 10,
887 },
888 end: LineColumn {
889 line: 1,
890 column: 15,
891 },
892 },
893 replacements: vec![
894 "replacement_0".to_owned(),
895 "replacement_1".to_owned(),
896 "replacement_2".to_owned(),
897 ],
898 description: Some("Possible spelling mistake found.".to_owned()),
899 };
900
901 const EXPECTED: &str = r#"error: spellcheck(Dummy)
902 --> /tmp/test/entity.rs:1
903 |
904 1 | Line mitake 1
905 | ^^^^^^
906 | - replacement_0, replacement_1, or replacement_2
907 |
908 | Possible spelling mistake found.
909"#;
910
911 assert_display_eq(suggestion, EXPECTED);
912 }
913
914 #[test]
915 fn fmt_2_multi_80_plus() {
916 const CONTENT: &str = r#" Line mitake 1
917 Suuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuper duuuuuuuuuuuuuuuuuuuuuuuuper too long
918 "#;
919
920 let chunk = CheckableChunk::from_str(
921 CONTENT,
922 indexmap::indexmap! {
923 0..13 => Span {
924 start: LineColumn {
925 line: 1,
926 column: 4,
927 },
928 end: LineColumn {
929 line: 1,
930 column: 16,
931 }
932 },
933 14..101 => Span {
934 start: LineColumn {
935 line: 2,
936 column: 4,
937 },
938 end: LineColumn {
939 line: 2,
940 column: 90,
941 }
942 }
943 },
944 CommentVariant::TripleSlash,
945 );
946
947 let suggestion = Suggestion {
948 detector: Detector::Dummy,
949 origin: ContentOrigin::TestEntityRust,
950 chunk: &chunk,
951 range: 66..94,
952 span: Span {
953 start: LineColumn { line: 2, column: 5 },
954 end: LineColumn {
955 line: 2,
956 column: 92,
957 },
958 },
959 replacements: vec![
960 "replacement_0".to_owned(),
961 "replacement_1".to_owned(),
962 "replacement_2".to_owned(),
963 ],
964 description: Some("Possible spelling mistake found.".to_owned()),
965 };
966
967 const EXPECTED: &str = r#"error: spellcheck(Dummy)
968 --> /tmp/test/entity.rs:2
969 |
970 2 | ..uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuper duuu...uper too long
971 | ^^^^^^^^^^^
972 | - replacement_0, replacement_1, or replacement_2
973 |
974 | Possible spelling mistake found.
975"#;
976
977 assert_display_eq(suggestion, EXPECTED);
978 }
979
980 #[test]
981 fn multiline_is_dbg_printable() {
982 let _ = env_logger::builder()
983 .is_test(true)
984 .filter(None, log::LevelFilter::Trace)
985 .try_init();
986
987 use crate::documentation::CheckableChunk;
988 let chunk = CheckableChunk::from_str(
989 r#"0
9902345
9917@n"#,
992 indexmap::indexmap! { 0..10 => Span {
993 start : LineColumn {
994 line: 7usize,
995 column: 8usize,
996 },
997 end : LineColumn {
998 line: 9usize,
999 column: 4usize,
1000 }
1001 } },
1002 CommentVariant::TripleSlash,
1003 );
1004
1005 let suggestion = Suggestion {
1006 detector: Detector::Dummy,
1007 origin: ContentOrigin::TestEntityRust,
1008 chunk: &chunk,
1009 span: Span {
1010 start: LineColumn {
1011 line: 8usize,
1012 column: 0,
1013 },
1014 end: LineColumn {
1015 line: 8usize,
1016 column: 3,
1017 },
1018 },
1019 range: 2..6,
1020 replacements: vec!["whocares".to_owned()],
1021 description: None,
1022 };
1023
1024 let suggestion = dbg!(suggestion);
1025
1026 log::info!("fmt debug=\n{suggestion:?}\n<");
1027 log::info!("fmt display=\n{suggestion}\n<");
1028 }
1029
1030 #[test]
1031 fn overlapped() {
1032 let chunk = CheckableChunk::from_str(
1033 r#"0
10342345
10357@n"#,
1036 indexmap::indexmap! { 0..10 => Span {
1037 start : LineColumn {
1038 line: 7usize,
1039 column: 8usize,
1040 },
1041 end : LineColumn {
1042 line: 9usize,
1043 column: 4usize,
1044 }
1045 } },
1046 CommentVariant::TripleSlash,
1047 );
1048 let suggestion = Suggestion {
1049 detector: Detector::Dummy,
1050 origin: ContentOrigin::TestEntityRust,
1051 chunk: &chunk,
1052 span: Span {
1053 start: LineColumn {
1054 line: 8usize,
1055 column: 1,
1056 },
1057 end: LineColumn {
1058 line: 8usize,
1059 column: 3,
1060 },
1061 },
1062 range: 2..6,
1063 replacements: vec!["whocares".to_owned()],
1064 description: None,
1065 };
1066 let overlapped_smaller_suggestion = Suggestion {
1067 detector: Detector::Dummy,
1068 origin: ContentOrigin::TestEntityRust,
1069 chunk: &chunk,
1070 span: Span {
1071 start: LineColumn {
1072 line: 8usize,
1073 column: 0,
1074 },
1075 end: LineColumn {
1076 line: 8usize,
1077 column: 2,
1078 },
1079 },
1080 range: 2..6,
1081 replacements: vec!["whocares".to_owned()],
1082 description: None,
1083 };
1084
1085 let overlapped_larger_suggestion = Suggestion {
1086 detector: Detector::Dummy,
1087 origin: ContentOrigin::TestEntityRust,
1088 chunk: &chunk,
1089 span: Span {
1090 start: LineColumn {
1091 line: 8usize,
1092 column: 2,
1093 },
1094 end: LineColumn {
1095 line: 8usize,
1096 column: 3,
1097 },
1098 },
1099 range: 2..6,
1100 replacements: vec!["whocares".to_owned()],
1101 description: None,
1102 };
1103
1104 assert!(suggestion.is_overlapped(&overlapped_smaller_suggestion));
1105 assert!(suggestion.is_overlapped(&overlapped_larger_suggestion));
1106 }
1107}