1use std::path::PathBuf;
7
8#[derive(Debug, Clone)]
14pub enum EditFailureReason {
15 NoMatchFound,
17
18 FuzzyMatchBelowThreshold {
20 similarity: f64,
21 threshold: f64,
22 found_text: String,
23 },
24
25 FuzzyMatchAboveThreshold {
27 similarity: f64,
28 is_whitespace_only: bool,
29 },
30
31 UnexpectedCount { expected: usize, found: usize },
33
34 EmptySearch,
36
37 IdenticalStrings,
39}
40
41#[derive(Debug, Clone)]
47pub struct SuggestionContext {
48 pub file_path: String,
49 pub search_string: String,
50 pub line_number: Option<usize>,
51
52 pub log_path: Option<PathBuf>,
54 pub execution_time_ms: Option<f64>,
55}
56
57#[derive(Debug, Clone)]
59pub struct Suggestion {
60 pub message: String,
62
63 pub actions: Vec<String>,
65}
66
67impl Suggestion {
68 #[must_use]
70 pub fn for_failure(reason: &EditFailureReason, context: &SuggestionContext) -> Self {
71 match reason {
72 EditFailureReason::FuzzyMatchAboveThreshold {
73 similarity,
74 is_whitespace_only,
75 } => Self::fuzzy_match_above_threshold(*similarity, *is_whitespace_only, context),
76
77 EditFailureReason::FuzzyMatchBelowThreshold {
78 similarity,
79 threshold,
80 found_text,
81 } => Self::fuzzy_match_below_threshold(*similarity, *threshold, found_text, context),
82
83 EditFailureReason::UnexpectedCount { expected, found } => {
84 Self::unexpected_count(*expected, *found, context)
85 }
86
87 EditFailureReason::NoMatchFound => Self::no_match_found(context),
88
89 EditFailureReason::EmptySearch => Self::empty_search(),
90
91 EditFailureReason::IdenticalStrings => Self::identical_strings(),
92 }
93 }
94
95 #[must_use]
97 pub fn format(&self) -> String {
98 let mut output = String::new();
99
100 if !self.actions.is_empty() {
101 output.push_str("\n💡 Suggestions:\n");
102 for (i, action) in self.actions.iter().enumerate() {
103 output.push_str(&format!("{}. {}\n", i + 1, action));
104 }
105 }
106
107 output
108 }
109
110 fn fuzzy_match_above_threshold(
115 similarity: f64,
116 is_whitespace_only: bool,
117 context: &SuggestionContext,
118 ) -> Self {
119 let mut actions = vec![
120 "Copy the exact text from the diff above".to_string(),
121 " Example: Use the text after {+...+} in the diff".to_string(),
122 ];
123
124 if is_whitespace_only {
125 actions.push(
126 "The difference is only whitespace - check for extra/missing spaces or tabs"
127 .to_string(),
128 );
129 }
130
131 if let Some(ref log_path) = context.log_path {
133 actions.push(format!(
134 "For detailed analysis, check log: {}",
135 log_path.display()
136 ));
137 }
138
139 let mut message = if let Some(line_num) = context.line_number {
140 format!(
141 "Exact match not found in {}, but found similar text at line {} with {:.1}% similarity",
142 context.file_path,
143 line_num,
144 similarity * 100.0
145 )
146 } else {
147 format!(
148 "Exact match not found in {}, but found similar text with {:.1}% similarity",
149 context.file_path,
150 similarity * 100.0
151 )
152 };
153
154 if let Some(time_ms) = context.execution_time_ms {
156 message.push_str(&format!(" (found in {time_ms:.2}ms)"));
157 }
158
159 message.push('.');
160
161 Self { message, actions }
162 }
163
164 fn fuzzy_match_below_threshold(
165 similarity: f64,
166 threshold: f64,
167 found_text: &str,
168 context: &SuggestionContext,
169 ) -> Self {
170 let mut message = format!(
171 "Search content not found in {}. The closest match was \"{}\" \
172 with only {:.1}% similarity, which is below the {:.1}% threshold",
173 context.file_path,
174 found_text,
175 similarity * 100.0,
176 threshold * 100.0
177 );
178
179 if let Some(time_ms) = context.execution_time_ms {
181 message.push_str(&format!(" (search completed in {time_ms:.2}ms)"));
182 }
183
184 message.push('.');
185
186 let mut actions = vec![
187 "Check if you're searching in the correct file".to_string(),
188 "Try a smaller, more unique search string".to_string(),
189 " Example: Instead of searching for entire function, search for a unique line within it".to_string(),
190 "Check for typos in your search string".to_string(),
191 ];
192
193 if let Some(ref log_path) = context.log_path {
195 actions.push(format!(
196 "For detailed analysis, check log: {}",
197 log_path.display()
198 ));
199 }
200
201 Self { message, actions }
202 }
203
204 fn unexpected_count(expected: usize, found: usize, _context: &SuggestionContext) -> Self {
205 let message = format!("Expected to replace {expected} occurrence(s), but found {found}.");
206
207 let actions = if found > expected {
208 vec![
209 format!(
210 "If you want to replace all {} occurrences, set expected_replacements to {}",
211 found, found
212 ),
213 format!(" Example: edit_block(..., expected_replacements: {})", found),
214 "To replace specific occurrences, make your search string more unique by including surrounding context".to_string(),
215 " Example: Instead of 'foo', use 'function bar() {\\n foo\\n}'".to_string(),
216 ]
217 } else {
218 vec![
219 "Some occurrences may have already been replaced".to_string(),
220 "Check if the file content has changed since you last read it".to_string(),
221 format!(
222 "Update expected_replacements to {} to match actual count",
223 found
224 ),
225 ]
226 };
227
228 Self { message, actions }
229 }
230
231 fn no_match_found(context: &SuggestionContext) -> Self {
232 let message = format!(
233 "No occurrences of the search string found in {}",
234 context.file_path
235 );
236
237 let actions = vec![
238 "Verify you're searching in the correct file".to_string(),
239 "Check for typos in your search string".to_string(),
240 "Try searching for a smaller, more specific substring".to_string(),
241 ];
242
243 Self { message, actions }
244 }
245
246 fn empty_search() -> Self {
247 Self {
248 message: "Empty search strings are not allowed.".to_string(),
249 actions: vec!["Provide a non-empty string to search for".to_string()],
250 }
251 }
252
253 fn identical_strings() -> Self {
254 Self {
255 message: "old_string and new_string are identical.".to_string(),
256 actions: vec!["No changes would be made - provide different strings".to_string()],
257 }
258 }
259}