ricecoder_generation/
conflict_prompter.rs

1//! User prompting for conflict resolution
2//!
3//! Handles interactive prompting for users to choose conflict resolution strategies.
4//! Implements requirement:
5//! - Requirement 4.5: Prompt user to choose strategy for each conflict
6
7use crate::conflict_detector::FileConflictInfo;
8use crate::conflict_resolver::ConflictStrategy;
9use crate::error::GenerationError;
10use std::io::{self, BufRead, Write};
11
12/// Prompts user for conflict resolution decisions
13///
14/// Implements requirement:
15/// - Requirement 4.5: Prompt user to choose strategy for each conflict
16pub struct ConflictPrompter;
17
18/// Result of user prompting for a conflict
19#[derive(Debug, Clone)]
20pub struct PromptResult {
21    /// Strategy chosen by user
22    pub strategy: ConflictStrategy,
23    /// Whether to apply to all remaining conflicts
24    pub apply_to_all: bool,
25}
26
27impl ConflictPrompter {
28    /// Create a new conflict prompter
29    pub fn new() -> Self {
30        Self
31    }
32
33    /// Prompt user for a single conflict
34    ///
35    /// Shows the conflict details and asks user to choose a resolution strategy.
36    ///
37    /// # Arguments
38    /// * `conflict` - Conflict to prompt for
39    /// * `conflict_number` - Number of this conflict (for display)
40    /// * `total_conflicts` - Total number of conflicts
41    ///
42    /// # Returns
43    /// User's choice of strategy
44    ///
45    /// # Requirements
46    /// - Requirement 4.5: Prompt user to choose strategy for each conflict
47    pub fn prompt_for_conflict(
48        &self,
49        conflict: &FileConflictInfo,
50        conflict_number: usize,
51        total_conflicts: usize,
52    ) -> Result<PromptResult, GenerationError> {
53        // Display conflict information
54        self.display_conflict(conflict, conflict_number, total_conflicts)?;
55
56        // Get user input
57        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                    // Loop back to show options again
87                }
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    /// Prompt user for multiple conflicts
103    ///
104    /// Iterates through conflicts and prompts for each one, with option to apply
105    /// same strategy to all remaining conflicts.
106    ///
107    /// # Arguments
108    /// * `conflicts` - List of conflicts to prompt for
109    ///
110    /// # Returns
111    /// Map of file paths to chosen strategies
112    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 user chose "apply to all", use that strategy
123            if let Some(strategy) = apply_to_all_strategy {
124                results.push((conflict.path.to_string_lossy().to_string(), strategy));
125                continue;
126            }
127
128            // Otherwise, prompt for this conflict
129            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 user chose "apply to all", remember the strategy
138            if prompt_result.apply_to_all {
139                apply_to_all_strategy = Some(prompt_result.strategy);
140            }
141        }
142
143        Ok(results)
144    }
145
146    /// Display conflict information
147    ///
148    /// Shows the file path, diff summary, and conflict details.
149    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    /// Display resolution options
177    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    /// Display detailed diff
191    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    /// Prompt user if they want to apply strategy to all remaining conflicts
224    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    /// Read user input from stdin
235    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    /// Display a summary of all conflicts
251    ///
252    /// # Arguments
253    /// * `conflicts` - List of conflicts
254    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        // This should not panic
317        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        // This should not panic
326        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        // This should not panic
336        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        // This should not panic
346        let result = prompter.display_summary(&conflicts);
347        assert!(result.is_ok());
348    }
349}