1use crate::conflict_detector::FileConflictInfo;
8use crate::conflict_resolver::ConflictStrategy;
9use crate::error::GenerationError;
10use std::io::{self, BufRead, Write};
11
12pub struct ConflictPrompter;
17
18#[derive(Debug, Clone)]
20pub struct PromptResult {
21 pub strategy: ConflictStrategy,
23 pub apply_to_all: bool,
25}
26
27impl ConflictPrompter {
28 pub fn new() -> Self {
30 Self
31 }
32
33 pub fn prompt_for_conflict(
48 &self,
49 conflict: &FileConflictInfo,
50 conflict_number: usize,
51 total_conflicts: usize,
52 ) -> Result<PromptResult, GenerationError> {
53 self.display_conflict(conflict, conflict_number, total_conflicts)?;
55
56 loop {
58 self.display_options()?;
59 let input = self.read_user_input()?;
60 let input = input.trim().to_lowercase();
61
62 match input.as_str() {
63 "s" | "skip" => {
64 let apply_to_all = self.prompt_apply_to_all()?;
65 return Ok(PromptResult {
66 strategy: ConflictStrategy::Skip,
67 apply_to_all,
68 });
69 }
70 "o" | "overwrite" => {
71 let apply_to_all = self.prompt_apply_to_all()?;
72 return Ok(PromptResult {
73 strategy: ConflictStrategy::Overwrite,
74 apply_to_all,
75 });
76 }
77 "m" | "merge" => {
78 let apply_to_all = self.prompt_apply_to_all()?;
79 return Ok(PromptResult {
80 strategy: ConflictStrategy::Merge,
81 apply_to_all,
82 });
83 }
84 "d" | "diff" => {
85 self.display_diff(conflict)?;
86 }
88 "q" | "quit" => {
89 return Err(GenerationError::ValidationError {
90 file: conflict.path.to_string_lossy().to_string(),
91 line: 0,
92 message: "User cancelled conflict resolution".to_string(),
93 });
94 }
95 _ => {
96 println!("Invalid choice. Please try again.");
97 }
98 }
99 }
100 }
101
102 pub fn prompt_for_conflicts(
113 &self,
114 conflicts: &[FileConflictInfo],
115 ) -> Result<Vec<(String, ConflictStrategy)>, GenerationError> {
116 let mut results = Vec::new();
117 let mut apply_to_all_strategy: Option<ConflictStrategy> = None;
118
119 for (i, conflict) in conflicts.iter().enumerate() {
120 let conflict_num = i + 1;
121
122 if let Some(strategy) = apply_to_all_strategy {
124 results.push((conflict.path.to_string_lossy().to_string(), strategy));
125 continue;
126 }
127
128 let prompt_result =
130 self.prompt_for_conflict(conflict, conflict_num, conflicts.len())?;
131
132 results.push((
133 conflict.path.to_string_lossy().to_string(),
134 prompt_result.strategy,
135 ));
136
137 if prompt_result.apply_to_all {
139 apply_to_all_strategy = Some(prompt_result.strategy);
140 }
141 }
142
143 Ok(results)
144 }
145
146 fn display_conflict(
150 &self,
151 conflict: &FileConflictInfo,
152 conflict_number: usize,
153 total_conflicts: usize,
154 ) -> Result<(), GenerationError> {
155 println!("\n{}", "=".repeat(70));
156 println!(
157 "Conflict {}/{}: {}",
158 conflict_number,
159 total_conflicts,
160 conflict.path.display()
161 );
162 println!("{}", "=".repeat(70));
163
164 println!("\nConflict Summary:");
165 println!(" Added lines: {}", conflict.diff.added_lines.len());
166 println!(" Removed lines: {}", conflict.diff.removed_lines.len());
167 println!(" Modified lines: {}", conflict.diff.modified_lines.len());
168
169 println!("\nFile sizes:");
170 println!(" Original: {} bytes", conflict.old_content.len());
171 println!(" Generated: {} bytes", conflict.new_content.len());
172
173 Ok(())
174 }
175
176 fn display_options(&self) -> Result<(), GenerationError> {
178 println!("\nResolution options:");
179 println!(" (s)kip - Don't write this file");
180 println!(" (o)verwrite - Write new file (backup original)");
181 println!(" (m)erge - Merge changes (mark conflicts)");
182 println!(" (d)iff - Show detailed diff");
183 println!(" (q)uit - Cancel generation");
184 print!("\nChoose option: ");
185 io::stdout().flush().ok();
186
187 Ok(())
188 }
189
190 fn display_diff(&self, conflict: &FileConflictInfo) -> Result<(), GenerationError> {
192 println!("\n{}", "-".repeat(70));
193 println!("Detailed Diff:");
194 println!("{}", "-".repeat(70));
195
196 if !conflict.diff.removed_lines.is_empty() {
197 println!("\nRemoved lines:");
198 for line in &conflict.diff.removed_lines {
199 println!(" - [{}] {}", line.line_number, line.content);
200 }
201 }
202
203 if !conflict.diff.added_lines.is_empty() {
204 println!("\nAdded lines:");
205 for line in &conflict.diff.added_lines {
206 println!(" + [{}] {}", line.line_number, line.content);
207 }
208 }
209
210 if !conflict.diff.modified_lines.is_empty() {
211 println!("\nModified lines:");
212 for (old, new) in &conflict.diff.modified_lines {
213 println!(" - [{}] {}", old.line_number, old.content);
214 println!(" + [{}] {}", new.line_number, new.content);
215 }
216 }
217
218 println!("{}", "-".repeat(70));
219
220 Ok(())
221 }
222
223 fn prompt_apply_to_all(&self) -> Result<bool, GenerationError> {
225 print!("\nApply this choice to all remaining conflicts? (y/n): ");
226 io::stdout().flush().ok();
227
228 let input = self.read_user_input()?;
229 let input = input.trim().to_lowercase();
230
231 Ok(input == "y" || input == "yes")
232 }
233
234 fn read_user_input(&self) -> Result<String, GenerationError> {
236 let stdin = io::stdin();
237 let mut line = String::new();
238 stdin
239 .lock()
240 .read_line(&mut line)
241 .map_err(|e| GenerationError::ValidationError {
242 file: "stdin".to_string(),
243 line: 0,
244 message: format!("Failed to read user input: {}", e),
245 })?;
246
247 Ok(line)
248 }
249
250 pub fn display_summary(&self, conflicts: &[FileConflictInfo]) -> Result<(), GenerationError> {
255 println!("\n{}", "=".repeat(70));
256 println!("Conflict Summary");
257 println!("{}", "=".repeat(70));
258 println!("Total conflicts: {}", conflicts.len());
259
260 for (i, conflict) in conflicts.iter().enumerate() {
261 println!(
262 " {}. {} ({} changes)",
263 i + 1,
264 conflict.path.display(),
265 conflict.diff.total_changes
266 );
267 }
268
269 println!("{}", "=".repeat(70));
270
271 Ok(())
272 }
273}
274
275impl Default for ConflictPrompter {
276 fn default() -> Self {
277 Self::new()
278 }
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284 use std::path::PathBuf;
285
286 fn create_test_conflict() -> FileConflictInfo {
287 FileConflictInfo {
288 path: PathBuf::from("test.rs"),
289 old_content: "old content".to_string(),
290 new_content: "new content".to_string(),
291 diff: crate::conflict_detector::FileDiff {
292 added_lines: vec![crate::conflict_detector::DiffLine {
293 line_number: 1,
294 content: "added".to_string(),
295 }],
296 removed_lines: vec![crate::conflict_detector::DiffLine {
297 line_number: 2,
298 content: "removed".to_string(),
299 }],
300 modified_lines: vec![],
301 total_changes: 2,
302 },
303 }
304 }
305
306 #[test]
307 fn test_create_prompter() {
308 let _prompter = ConflictPrompter::new();
309 }
310
311 #[test]
312 fn test_display_conflict() {
313 let prompter = ConflictPrompter::new();
314 let conflict = create_test_conflict();
315
316 let result = prompter.display_conflict(&conflict, 1, 1);
318 assert!(result.is_ok());
319 }
320
321 #[test]
322 fn test_display_options() {
323 let prompter = ConflictPrompter::new();
324
325 let result = prompter.display_options();
327 assert!(result.is_ok());
328 }
329
330 #[test]
331 fn test_display_diff() {
332 let prompter = ConflictPrompter::new();
333 let conflict = create_test_conflict();
334
335 let result = prompter.display_diff(&conflict);
337 assert!(result.is_ok());
338 }
339
340 #[test]
341 fn test_display_summary() {
342 let prompter = ConflictPrompter::new();
343 let conflicts = vec![create_test_conflict(), create_test_conflict()];
344
345 let result = prompter.display_summary(&conflicts);
347 assert!(result.is_ok());
348 }
349}