ricecoder_refactoring/preview/
generator.rs

1//! Preview generation for refactoring operations
2
3use crate::error::Result;
4use crate::impact::ImpactAnalyzer;
5use crate::types::{Refactoring, RefactoringPreview};
6
7/// Generates previews of refactoring operations
8pub struct PreviewGenerator;
9
10impl PreviewGenerator {
11    /// Create a new preview generator
12    pub fn new() -> Self {
13        Self
14    }
15}
16
17impl Default for PreviewGenerator {
18    fn default() -> Self {
19        Self::new()
20    }
21}
22
23/// Represents a diff hunk (a contiguous block of changes)
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct DiffHunk {
26    /// Starting line in original file
27    pub original_start: usize,
28    /// Number of lines in original file
29    pub original_count: usize,
30    /// Starting line in new file
31    pub new_start: usize,
32    /// Number of lines in new file
33    pub new_count: usize,
34    /// Lines in the hunk (with +/- prefix)
35    pub lines: Vec<String>,
36}
37
38/// Represents a unified diff
39#[derive(Debug, Clone)]
40pub struct UnifiedDiff {
41    /// Original file path
42    pub original_path: String,
43    /// New file path
44    pub new_path: String,
45    /// Hunks in the diff
46    pub hunks: Vec<DiffHunk>,
47}
48
49impl PreviewGenerator {
50    /// Generate a preview of a refactoring
51    pub fn generate(refactoring: &Refactoring) -> Result<RefactoringPreview> {
52        let analyzer = ImpactAnalyzer::new();
53        let impact = analyzer.analyze(refactoring)?;
54
55        // For now, create empty changes
56        // In a real implementation, this would generate actual code changes
57        let changes = vec![];
58
59        Ok(RefactoringPreview {
60            changes,
61            impact,
62            estimated_time_seconds: 5,
63        })
64    }
65
66    /// Generate a unified diff between original and new content
67    ///
68    /// Returns a unified diff format string that shows the changes
69    /// between the original and new content.
70    pub fn generate_unified_diff(original: &str, new: &str) -> String {
71        let hunks = Self::compute_hunks(original, new);
72        Self::format_unified_diff("original", "new", &hunks)
73    }
74
75    /// Generate a side-by-side diff between original and new content
76    ///
77    /// Returns a side-by-side diff format that shows original and new
78    /// content side by side for easier comparison.
79    pub fn generate_side_by_side_diff(original: &str, new: &str) -> String {
80        let original_lines: Vec<&str> = original.lines().collect();
81        let new_lines: Vec<&str> = new.lines().collect();
82
83        let mut result = String::new();
84        result.push_str("--- original\t\t+++ new\n");
85        result.push_str("---\n");
86
87        let max_lines = original_lines.len().max(new_lines.len());
88        for i in 0..max_lines {
89            let orig_line = original_lines.get(i).copied().unwrap_or("");
90            let new_line = new_lines.get(i).copied().unwrap_or("");
91
92            if orig_line == new_line {
93                result.push_str(&format!("{:<40} | {}\n", orig_line, new_line));
94            } else {
95                result.push_str(&format!("< {:<38} | > {}\n", orig_line, new_line));
96            }
97        }
98
99        result
100    }
101
102    /// Compute diff hunks between original and new content
103    ///
104    /// Uses a simple line-based diff algorithm to identify contiguous
105    /// blocks of changes (hunks).
106    fn compute_hunks(original: &str, new: &str) -> Vec<DiffHunk> {
107        let original_lines: Vec<&str> = original.lines().collect();
108        let new_lines: Vec<&str> = new.lines().collect();
109
110        let mut hunks = Vec::new();
111        let mut i = 0;
112        let mut j = 0;
113
114        while i < original_lines.len() || j < new_lines.len() {
115            // Skip matching lines
116            while i < original_lines.len()
117                && j < new_lines.len()
118                && original_lines[i] == new_lines[j]
119            {
120                i += 1;
121                j += 1;
122            }
123
124            if i >= original_lines.len() && j >= new_lines.len() {
125                break;
126            }
127
128            // Found a difference, collect the hunk
129            let hunk_start_orig = i + 1; // 1-indexed
130            let hunk_start_new = j + 1; // 1-indexed
131            let mut hunk_lines = Vec::new();
132
133            // Collect removed lines
134            let removed_start = i;
135            while i < original_lines.len() && (j >= new_lines.len() || original_lines[i] != new_lines[j]) {
136                hunk_lines.push(format!("-{}", original_lines[i]));
137                i += 1;
138            }
139
140            // Collect added lines
141            let added_start = j;
142            while j < new_lines.len() && (i >= original_lines.len() || original_lines[i] != new_lines[j]) {
143                hunk_lines.push(format!("+{}", new_lines[j]));
144                j += 1;
145            }
146
147            // Add context lines (up to 3 lines after changes)
148            let context_count = 3;
149            let mut context_added = 0;
150            while context_added < context_count
151                && i < original_lines.len()
152                && j < new_lines.len()
153                && original_lines[i] == new_lines[j]
154            {
155                hunk_lines.push(format!(" {}", original_lines[i]));
156                i += 1;
157                j += 1;
158                context_added += 1;
159            }
160
161            let original_count = i - removed_start;
162            let new_count = j - added_start;
163
164            hunks.push(DiffHunk {
165                original_start: hunk_start_orig,
166                original_count,
167                new_start: hunk_start_new,
168                new_count,
169                lines: hunk_lines,
170            });
171        }
172
173        hunks
174    }
175
176    /// Format hunks as unified diff
177    fn format_unified_diff(original_path: &str, new_path: &str, hunks: &[DiffHunk]) -> String {
178        let mut result = String::new();
179        result.push_str(&format!("--- {}\n", original_path));
180        result.push_str(&format!("+++ {}\n", new_path));
181
182        for hunk in hunks {
183            result.push_str(&format!(
184                "@@ -{},{} +{},{} @@\n",
185                hunk.original_start, hunk.original_count, hunk.new_start, hunk.new_count
186            ));
187
188            for line in &hunk.lines {
189                result.push_str(line);
190                result.push('\n');
191            }
192        }
193
194        result
195    }
196
197    /// Generate a diff for a file change
198    ///
199    /// This is a convenience method that generates a unified diff
200    /// between the original and new content.
201    pub fn generate_diff(original: &str, new: &str) -> String {
202        Self::generate_unified_diff(original, new)
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209    use crate::types::{RefactoringOptions, RefactoringTarget, RefactoringType};
210    use std::path::PathBuf;
211
212    #[test]
213    fn test_generate_preview() -> Result<()> {
214        let refactoring = Refactoring {
215            id: "test-refactoring".to_string(),
216            refactoring_type: RefactoringType::Rename,
217            target: RefactoringTarget {
218                file: PathBuf::from("src/main.rs"),
219                symbol: "old_name".to_string(),
220                range: None,
221            },
222            options: RefactoringOptions::default(),
223        };
224
225        let preview = PreviewGenerator::generate(&refactoring)?;
226        assert_eq!(preview.estimated_time_seconds, 5);
227
228        Ok(())
229    }
230
231    #[test]
232    fn test_generate_unified_diff_simple() {
233        let original = "fn old_name() {}\n";
234        let new = "fn new_name() {}\n";
235        let diff = PreviewGenerator::generate_unified_diff(original, new);
236
237        assert!(diff.contains("--- original"));
238        assert!(diff.contains("+++ new"));
239        assert!(diff.contains("-fn old_name() {}"));
240        assert!(diff.contains("+fn new_name() {}"));
241    }
242
243    #[test]
244    fn test_generate_unified_diff_multiline() {
245        let original = "line 1\nline 2\nline 3\n";
246        let new = "line 1\nmodified line 2\nline 3\n";
247        let diff = PreviewGenerator::generate_unified_diff(original, new);
248
249        assert!(diff.contains("--- original"));
250        assert!(diff.contains("+++ new"));
251        assert!(diff.contains("-line 2"));
252        assert!(diff.contains("+modified line 2"));
253    }
254
255    #[test]
256    fn test_generate_side_by_side_diff() {
257        let original = "fn old_name() {}";
258        let new = "fn new_name() {}";
259        let diff = PreviewGenerator::generate_side_by_side_diff(original, new);
260
261        assert!(diff.contains("--- original"));
262        assert!(diff.contains("+++ new"));
263        assert!(diff.contains("old_name"));
264        assert!(diff.contains("new_name"));
265    }
266
267    #[test]
268    fn test_generate_diff_identical() {
269        let content = "fn test() {}";
270        let diff = PreviewGenerator::generate_diff(content, content);
271
272        // Should have headers but no changes
273        assert!(diff.contains("--- original"));
274        assert!(diff.contains("+++ new"));
275    }
276
277    #[test]
278    fn test_compute_hunks_simple() {
279        let original = "a\nb\nc\n";
280        let new = "a\nx\nc\n";
281        let hunks = PreviewGenerator::compute_hunks(original, new);
282
283        assert_eq!(hunks.len(), 1);
284        assert_eq!(hunks[0].original_start, 2);
285        assert_eq!(hunks[0].new_start, 2);
286    }
287
288    #[test]
289    fn test_compute_hunks_multiple_changes() {
290        let original = "a\nb\nc\nd\ne\n";
291        let new = "a\nx\nc\ny\ne\n";
292        let hunks = PreviewGenerator::compute_hunks(original, new);
293
294        // Should have two hunks for the two separate changes
295        assert!(hunks.len() >= 1);
296    }
297
298    #[test]
299    fn test_compute_hunks_empty_original() {
300        let original = "";
301        let new = "a\nb\nc\n";
302        let hunks = PreviewGenerator::compute_hunks(original, new);
303
304        assert!(!hunks.is_empty());
305    }
306
307    #[test]
308    fn test_compute_hunks_empty_new() {
309        let original = "a\nb\nc\n";
310        let new = "";
311        let hunks = PreviewGenerator::compute_hunks(original, new);
312
313        assert!(!hunks.is_empty());
314    }
315}