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 pub fn sort(&mut self) {
672 self.per_file
673 .par_iter_mut()
674 .for_each(|(_origin, suggestions)| {
675 suggestions.sort_by(|a, b| {
676 let cmp = a.span.start.cmp(&b.span.start);
677 if cmp != std::cmp::Ordering::Equal {
678 return cmp;
679 }
680
681 a.span.end.cmp(&b.span.end)
682 });
683 });
684 self.per_file
685 .sort_by(|origin_a, _a, origin_b, _b| -> std::cmp::Ordering {
686 origin_a.as_path().cmp(origin_b.as_path())
687 });
688 }
689
690 pub fn total_count(&self) -> usize {
692 self.per_file.iter().map(|(_origin, vec)| vec.len()).sum()
693 }
694}
695
696impl<'s> IntoIterator for SuggestionSet<'s> {
697 type Item = (ContentOrigin, Vec<Suggestion<'s>>);
698 type IntoIter = indexmap::map::IntoIter<ContentOrigin, Vec<Suggestion<'s>>>;
699 fn into_iter(self) -> Self::IntoIter {
700 self.per_file.into_iter()
701 }
702}
703
704impl<'s> IntoIterator for &'s SuggestionSet<'s> {
705 type Item = (&'s ContentOrigin, &'s Vec<Suggestion<'s>>);
706 type IntoIter = indexmap::map::Iter<'s, ContentOrigin, Vec<Suggestion<'s>>>;
707 fn into_iter(self) -> Self::IntoIter {
708 self.per_file.iter()
709 }
710}
711
712#[cfg(test)]
713mod tests {
714 use super::*;
715 use crate::{CommentVariant, LineColumn};
716 use console;
717 use std::fmt;
718
719 fn assert_display_eq<D: fmt::Display, S: AsRef<str>>(display: D, s: S) {
724 let expected = s.as_ref();
725 let expected = console::strip_ansi_codes(expected);
726
727 let reality = display.to_string();
729 let reality = console::strip_ansi_codes(reality.as_str());
730 assert_eq!(reality, expected);
731 }
732
733 #[test]
734 fn fmt_0_single() {
735 const CONTENT: &str = " Is it dyrck again?";
736 let chunk = CheckableChunk::from_str(
737 CONTENT,
738 indexmap::indexmap! { 0..18 => Span {
739 start: LineColumn {
740 line: 1,
741 column: 0,
742 },
743 end: LineColumn {
744 line: 1,
745 column: 17,
746 }
747 }
748 },
749 CommentVariant::TripleSlash,
750 );
751
752 let suggestion = Suggestion {
753 detector: Detector::Dummy,
754 origin: ContentOrigin::TestEntityRust,
755 chunk: &chunk,
756 range: 7..12,
757 span: Span {
758 start: LineColumn { line: 1, column: 6 },
759 end: LineColumn {
760 line: 1,
761 column: 10,
762 },
763 },
764 replacements: vec![
765 "replacement_0".to_owned(),
766 "replacement_1".to_owned(),
767 "replacement_2".to_owned(),
768 ],
769 description: Some("Possible spelling mistake found.".to_owned()),
770 };
771
772 const EXPECTED: &str = r#"error: spellcheck(Dummy)
773 --> /tmp/test/entity.rs:1
774 |
775 1 | Is it dyrck again?
776 | ^^^^^
777 | - replacement_0, replacement_1, or replacement_2
778 |
779 | Possible spelling mistake found.
780"#;
781 assert_display_eq(suggestion, EXPECTED);
782 }
783
784 #[test]
785 fn fmt_0_no_suggestion() {
786 const CONTENT: &str = " Is it dyrck again?";
787 let chunk = CheckableChunk::from_str(
788 CONTENT,
789 indexmap::indexmap! { 0..18 => Span {
790 start: LineColumn {
791 line: 1,
792 column: 0,
793 },
794 end: LineColumn {
795 line: 1,
796 column: 17,
797 }
798 }
799 },
800 CommentVariant::TripleSlash,
801 );
802
803 let suggestion = Suggestion {
804 detector: Detector::Dummy,
805 origin: ContentOrigin::TestEntityRust,
806 chunk: &chunk,
807 range: 7..12,
808 span: Span {
809 start: LineColumn { line: 1, column: 6 },
810 end: LineColumn {
811 line: 1,
812 column: 10,
813 },
814 },
815 replacements: vec![],
816 description: Some("Possible spelling mistake found.".to_owned()),
817 };
818
819 const EXPECTED: &str = r#"error: spellcheck(Dummy)
820 --> /tmp/test/entity.rs:1
821 |
822 1 | Is it dyrck again?
823 | ^^^^^
824 | Possible spelling mistake found.
825"#;
826 assert_display_eq(suggestion, EXPECTED);
827 }
828
829 #[test]
830 fn fmt_1_multi() {
831 const CONTENT: &str = r#" Line mitake 1
832 Anowher 2
833 Last"#;
834
835 let chunk = CheckableChunk::from_str(
836 CONTENT,
837 indexmap::indexmap! {
838 0..13 => Span {
839 start: LineColumn {
840 line: 1,
841 column: 4,
842 },
843 end: LineColumn {
844 line: 1,
845 column: 16,
846 }
847 },
848 14..24 => Span {
849 start: LineColumn {
850 line: 2,
851 column: 4,
852 },
853 end: LineColumn {
854 line: 2,
855 column: 12,
856 }
857 },
858 25..29 => Span {
859 start: LineColumn {
860 line: 3,
861 column: 4,
862 },
863 end: LineColumn {
864 line: 3,
865 column: 7,
866 }
867 }
868 },
869 CommentVariant::TripleSlash,
870 );
871
872 let suggestion = Suggestion {
873 detector: Detector::Dummy,
874 origin: ContentOrigin::TestEntityRust,
875 chunk: &chunk,
876 range: 6..12,
877 span: Span {
878 start: LineColumn {
879 line: 1,
880 column: 10,
881 },
882 end: LineColumn {
883 line: 1,
884 column: 15,
885 },
886 },
887 replacements: vec![
888 "replacement_0".to_owned(),
889 "replacement_1".to_owned(),
890 "replacement_2".to_owned(),
891 ],
892 description: Some("Possible spelling mistake found.".to_owned()),
893 };
894
895 const EXPECTED: &str = r#"error: spellcheck(Dummy)
896 --> /tmp/test/entity.rs:1
897 |
898 1 | Line mitake 1
899 | ^^^^^^
900 | - replacement_0, replacement_1, or replacement_2
901 |
902 | Possible spelling mistake found.
903"#;
904
905 assert_display_eq(suggestion, EXPECTED);
906 }
907
908 #[test]
909 fn fmt_2_multi_80_plus() {
910 const CONTENT: &str = r#" Line mitake 1
911 Suuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuper duuuuuuuuuuuuuuuuuuuuuuuuper too long
912 "#;
913
914 let chunk = CheckableChunk::from_str(
915 CONTENT,
916 indexmap::indexmap! {
917 0..13 => Span {
918 start: LineColumn {
919 line: 1,
920 column: 4,
921 },
922 end: LineColumn {
923 line: 1,
924 column: 16,
925 }
926 },
927 14..101 => Span {
928 start: LineColumn {
929 line: 2,
930 column: 4,
931 },
932 end: LineColumn {
933 line: 2,
934 column: 90,
935 }
936 }
937 },
938 CommentVariant::TripleSlash,
939 );
940
941 let suggestion = Suggestion {
942 detector: Detector::Dummy,
943 origin: ContentOrigin::TestEntityRust,
944 chunk: &chunk,
945 range: 66..94,
946 span: Span {
947 start: LineColumn { line: 2, column: 5 },
948 end: LineColumn {
949 line: 2,
950 column: 92,
951 },
952 },
953 replacements: vec![
954 "replacement_0".to_owned(),
955 "replacement_1".to_owned(),
956 "replacement_2".to_owned(),
957 ],
958 description: Some("Possible spelling mistake found.".to_owned()),
959 };
960
961 const EXPECTED: &str = r#"error: spellcheck(Dummy)
962 --> /tmp/test/entity.rs:2
963 |
964 2 | ..uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuper duuu...uper too long
965 | ^^^^^^^^^^^
966 | - replacement_0, replacement_1, or replacement_2
967 |
968 | Possible spelling mistake found.
969"#;
970
971 assert_display_eq(suggestion, EXPECTED);
972 }
973
974 #[test]
975 fn multiline_is_dbg_printable() {
976 let _ = env_logger::builder()
977 .is_test(true)
978 .filter(None, log::LevelFilter::Trace)
979 .try_init();
980
981 use crate::documentation::CheckableChunk;
982 let chunk = CheckableChunk::from_str(
983 r#"0
9842345
9857@n"#,
986 indexmap::indexmap! { 0..10 => Span {
987 start : LineColumn {
988 line: 7usize,
989 column: 8usize,
990 },
991 end : LineColumn {
992 line: 9usize,
993 column: 4usize,
994 }
995 } },
996 CommentVariant::TripleSlash,
997 );
998
999 let suggestion = Suggestion {
1000 detector: Detector::Dummy,
1001 origin: ContentOrigin::TestEntityRust,
1002 chunk: &chunk,
1003 span: Span {
1004 start: LineColumn {
1005 line: 8usize,
1006 column: 0,
1007 },
1008 end: LineColumn {
1009 line: 8usize,
1010 column: 3,
1011 },
1012 },
1013 range: 2..6,
1014 replacements: vec!["whocares".to_owned()],
1015 description: None,
1016 };
1017
1018 let suggestion = dbg!(suggestion);
1019
1020 log::info!("fmt debug=\n{suggestion:?}\n<");
1021 log::info!("fmt display=\n{suggestion}\n<");
1022 }
1023
1024 #[test]
1025 fn overlapped() {
1026 let chunk = CheckableChunk::from_str(
1027 r#"0
10282345
10297@n"#,
1030 indexmap::indexmap! { 0..10 => Span {
1031 start : LineColumn {
1032 line: 7usize,
1033 column: 8usize,
1034 },
1035 end : LineColumn {
1036 line: 9usize,
1037 column: 4usize,
1038 }
1039 } },
1040 CommentVariant::TripleSlash,
1041 );
1042 let suggestion = Suggestion {
1043 detector: Detector::Dummy,
1044 origin: ContentOrigin::TestEntityRust,
1045 chunk: &chunk,
1046 span: Span {
1047 start: LineColumn {
1048 line: 8usize,
1049 column: 1,
1050 },
1051 end: LineColumn {
1052 line: 8usize,
1053 column: 3,
1054 },
1055 },
1056 range: 2..6,
1057 replacements: vec!["whocares".to_owned()],
1058 description: None,
1059 };
1060 let overlapped_smaller_suggestion = Suggestion {
1061 detector: Detector::Dummy,
1062 origin: ContentOrigin::TestEntityRust,
1063 chunk: &chunk,
1064 span: Span {
1065 start: LineColumn {
1066 line: 8usize,
1067 column: 0,
1068 },
1069 end: LineColumn {
1070 line: 8usize,
1071 column: 2,
1072 },
1073 },
1074 range: 2..6,
1075 replacements: vec!["whocares".to_owned()],
1076 description: None,
1077 };
1078
1079 let overlapped_larger_suggestion = Suggestion {
1080 detector: Detector::Dummy,
1081 origin: ContentOrigin::TestEntityRust,
1082 chunk: &chunk,
1083 span: Span {
1084 start: LineColumn {
1085 line: 8usize,
1086 column: 2,
1087 },
1088 end: LineColumn {
1089 line: 8usize,
1090 column: 3,
1091 },
1092 },
1093 range: 2..6,
1094 replacements: vec!["whocares".to_owned()],
1095 description: None,
1096 };
1097
1098 assert!(suggestion.is_overlapped(&overlapped_smaller_suggestion));
1099 assert!(suggestion.is_overlapped(&overlapped_larger_suggestion));
1100 }
1101}