harper_core/linting/
suggestion.rs1use std::{
2 borrow::Borrow,
3 fmt::{Debug, Display},
4};
5
6use is_macro::Is;
7use serde::{Deserialize, Serialize};
8
9use crate::{Span, case};
10
11#[derive(Clone, Serialize, Deserialize, Is, PartialEq, Eq, Hash)]
13pub enum Suggestion {
14 ReplaceWith(Vec<char>),
16 InsertAfter(Vec<char>),
18 Remove,
20}
21
22impl Suggestion {
23 pub fn replace_with_match_case_str(
25 value: &str,
26 template: impl IntoIterator<Item = impl Borrow<char>>,
27 ) -> Self {
28 Self::replace_with_match_case(value.chars().collect(), template)
29 }
30
31 pub fn replace_with_match_case(
37 value: Vec<char>,
38 template: impl IntoIterator<Item = impl Borrow<char>>,
39 ) -> Self {
40 Self::ReplaceWith(case::copy_casing(template, value).to_vec())
41 }
42
43 pub fn apply(&self, span: Span<char>, source: &mut Vec<char>) {
45 match self {
46 Self::ReplaceWith(chars) => {
47 if chars.len() == span.len() {
49 for (index, c) in chars.iter().enumerate() {
50 source[index + span.start] = *c
51 }
52 } else {
53 let popped = source.split_off(span.start);
54
55 source.extend(chars);
56 source.extend(popped.into_iter().skip(span.len()));
57 }
58 }
59 Self::Remove => {
60 for i in span.end..source.len() {
61 source[i - span.len()] = source[i];
62 }
63
64 source.truncate(source.len() - span.len());
65 }
66 Self::InsertAfter(chars) => {
67 let popped = source.split_off(span.end);
68 source.extend(chars);
69 source.extend(popped);
70 }
71 }
72 }
73}
74
75impl Display for Suggestion {
76 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 match self {
78 Suggestion::ReplaceWith(with) => {
79 write!(f, "Replace with: “{}”", with.iter().collect::<String>())
80 }
81 Suggestion::InsertAfter(with) => {
82 write!(f, "Insert “{}”", with.iter().collect::<String>())
83 }
84 Suggestion::Remove => write!(f, "Remove error"),
85 }
86 }
87}
88
89impl Debug for Suggestion {
93 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94 <Self as Display>::fmt(self, f)
95 }
96}
97
98pub trait SuggestionCollectionExt {
99 fn to_replace_suggestions(
100 self,
101 case_template: impl IntoIterator<Item = impl Borrow<char>> + Clone,
102 ) -> impl Iterator<Item = Suggestion>;
103}
104
105impl<I, T> SuggestionCollectionExt for I
106where
107 I: IntoIterator<Item = T>,
108 T: AsRef<str>,
109{
110 fn to_replace_suggestions(
111 self,
112 case_template: impl IntoIterator<Item = impl Borrow<char>> + Clone,
113 ) -> impl Iterator<Item = Suggestion> {
114 self.into_iter().map(move |s| {
115 Suggestion::replace_with_match_case_str(s.as_ref(), case_template.clone())
116 })
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use crate::Span;
123
124 use super::Suggestion;
125
126 #[test]
127 fn insert_comma_after() {
128 let source = "This is a test";
129 let mut source_chars = source.chars().collect();
130 let sug = Suggestion::InsertAfter(vec![',']);
131 sug.apply(Span::new(0, 4), &mut source_chars);
132
133 assert_eq!(source_chars, "This, is a test".chars().collect::<Vec<_>>());
134 }
135
136 #[test]
137 fn suggestion_your_match_case() {
138 let template: Vec<_> = "You're".chars().collect();
139 let value: Vec<_> = "you are".chars().collect();
140
141 let correct = "You are".chars().collect();
142
143 assert_eq!(
144 Suggestion::replace_with_match_case(value, &template),
145 Suggestion::ReplaceWith(correct)
146 )
147 }
148
149 #[test]
150 fn issue_1065() {
151 let template: Vec<_> = "Stack Overflow".chars().collect();
152 let value: Vec<_> = "stackoverflow".chars().collect();
153
154 let correct = "StackOverflow".chars().collect();
155
156 assert_eq!(
157 Suggestion::replace_with_match_case(value, &template),
158 Suggestion::ReplaceWith(correct)
159 )
160 }
161}