Skip to main content

soma_core/cli/
preview.rs

1// Claude: Clean architecture for edit preview with syntax highlighting
2use crate::edit_control::ModifiableEdit;
3use std::collections::HashMap;
4
5/// Claude: Domain-driven design for preview functionality
6pub struct EditPreviewBuilder {
7    show_diff: bool,
8    show_risk_assessment: bool,
9    show_line_numbers: bool,
10    syntax_highlighting: bool,
11    theme: PreviewTheme,
12}
13
14impl EditPreviewBuilder {
15    pub fn new() -> Self {
16        Self {
17            show_diff: true,
18            show_risk_assessment: true,
19            show_line_numbers: true,
20            syntax_highlighting: true,
21            theme: PreviewTheme::Default,
22        }
23    }
24
25    pub fn with_diff(mut self, show: bool) -> Self {
26        self.show_diff = show;
27        self
28    }
29
30    pub fn with_risk_assessment(mut self, show: bool) -> Self {
31        self.show_risk_assessment = show;
32        self
33    }
34
35    pub fn with_syntax_highlighting(mut self, enabled: bool) -> Self {
36        self.syntax_highlighting = enabled;
37        self
38    }
39
40    pub fn with_theme(mut self, theme: PreviewTheme) -> Self {
41        self.theme = theme;
42        self
43    }
44
45    pub fn build(self) -> EditPreview {
46        EditPreview::new(self)
47    }
48}
49
50impl Default for EditPreviewBuilder {
51    fn default() -> Self {
52        Self::new()
53    }
54}
55
56pub struct EditPreview {
57    config: EditPreviewBuilder,
58    syntax_highlighter: SyntaxHighlighter,
59    diff_generator: DiffGenerator,
60}
61
62impl EditPreview {
63    fn new(config: EditPreviewBuilder) -> Self {
64        Self {
65            config,
66            syntax_highlighter: SyntaxHighlighter::new(),
67            diff_generator: DiffGenerator::new(),
68        }
69    }
70
71    /// Claude: Main preview generation with clean separation
72    pub fn generate_preview(&self, edit: &ModifiableEdit) -> PreviewResult {
73        let mut sections = Vec::new();
74
75        // Header section
76        sections.push(self.create_header_section(edit));
77
78        // Risk assessment section
79        if self.config.show_risk_assessment {
80            if let Some(risk_section) = self.create_risk_section(edit) {
81                sections.push(risk_section);
82            }
83        }
84
85        // Code preview section
86        sections.push(self.create_code_section(edit));
87
88        // Diff section
89        if self.config.show_diff && !edit.modifications.is_empty() {
90            sections.push(self.create_diff_section(edit));
91        }
92
93        // Validation results section
94        if !edit.processing_metadata.validation_results.is_empty() {
95            sections.push(self.create_validation_section(edit));
96        }
97
98        PreviewResult { sections }
99    }
100
101    /// Claude: Clean header generation
102    fn create_header_section(&self, edit: &ModifiableEdit) -> PreviewSection {
103        let confidence_bar = self.create_confidence_visualization(edit.get_effective_confidence());
104
105        PreviewSection {
106            title: "📋 Edit Summary".to_string(),
107            content: format!(
108                "File: {}\nLines: {}-{}\nReason: {}\nConfidence: {:.1}% {}\nApproval: {:?}",
109                edit.base_edit.file,
110                edit.base_edit.line_range.0,
111                edit.base_edit.line_range.1,
112                edit.base_edit.reason,
113                edit.get_effective_confidence() * 100.0,
114                confidence_bar,
115                edit.approval_state
116            ),
117            style: SectionStyle::Info,
118        }
119    }
120
121    /// Claude: Risk assessment with clear visual indicators
122    fn create_risk_section(&self, edit: &ModifiableEdit) -> Option<PreviewSection> {
123        // Mock risk assessment - in real implementation, this would call the classifier
124        let risk_score = self.calculate_risk_score(edit);
125        let risk_level = match risk_score {
126            r if r >= 0.8 => ("🔴 HIGH RISK", SectionStyle::Error),
127            r if r >= 0.5 => ("🟡 MEDIUM RISK", SectionStyle::Warning),
128            _ => ("🟢 LOW RISK", SectionStyle::Success),
129        };
130
131        Some(PreviewSection {
132            title: "⚠️ Risk Assessment".to_string(),
133            content: format!(
134                "{}\nRisk Score: {:.1}%\nRecommendation: {}",
135                risk_level.0,
136                risk_score * 100.0,
137                self.get_risk_recommendation(risk_score)
138            ),
139            style: risk_level.1,
140        })
141    }
142
143    /// Claude: Syntax highlighted code section
144    fn create_code_section(&self, edit: &ModifiableEdit) -> PreviewSection {
145        let final_code = edit.compute_final_code();
146        let highlighted_code = if self.config.syntax_highlighting {
147            self.syntax_highlighter
148                .highlight(&final_code, &edit.base_edit.file)
149        } else {
150            final_code
151        };
152
153        let numbered_lines = if self.config.show_line_numbers {
154            self.add_line_numbers(&highlighted_code, edit.base_edit.line_range.0)
155        } else {
156            highlighted_code
157        };
158
159        PreviewSection {
160            title: "📄 Code Preview".to_string(),
161            content: numbered_lines,
162            style: SectionStyle::Code,
163        }
164    }
165
166    /// Claude: Unified diff generation
167    fn create_diff_section(&self, edit: &ModifiableEdit) -> PreviewSection {
168        let diff = self
169            .diff_generator
170            .generate_diff(&edit.base_edit.new_code, &edit.compute_final_code());
171
172        PreviewSection {
173            title: "🔍 Changes".to_string(),
174            content: diff,
175            style: SectionStyle::Diff,
176        }
177    }
178
179    fn create_validation_section(&self, edit: &ModifiableEdit) -> PreviewSection {
180        let validation_summary = edit
181            .processing_metadata
182            .validation_results
183            .iter()
184            .map(|v| {
185                format!(
186                    "{}: {} - {}",
187                    v.validator_name,
188                    if v.passed { "✅" } else { "❌" },
189                    v.message
190                )
191            })
192            .collect::<Vec<_>>()
193            .join("\n");
194
195        PreviewSection {
196            title: "🔧 Validation Results".to_string(),
197            content: validation_summary,
198            style: SectionStyle::Info,
199        }
200    }
201
202    // Claude: Helper methods with clean implementation
203    fn create_confidence_visualization(&self, confidence: f64) -> String {
204        let filled = (confidence * 10.0) as usize;
205        let bar: String = (0..10)
206            .map(|i| if i < filled { "█" } else { "░" })
207            .collect();
208        format!("[{}]", bar)
209    }
210
211    fn add_line_numbers(&self, content: &str, start_line: usize) -> String {
212        content
213            .lines()
214            .enumerate()
215            .map(|(i, line)| format!("{:4} | {}", start_line + i, line))
216            .collect::<Vec<_>>()
217            .join("\n")
218    }
219
220    fn calculate_risk_score(&self, edit: &ModifiableEdit) -> f64 {
221        // Simplified risk calculation
222        let base_risk = 1.0 - edit.get_effective_confidence();
223        let modification_risk = edit.modifications.len() as f64 * 0.1;
224        (base_risk + modification_risk).min(1.0)
225    }
226
227    fn get_risk_recommendation(&self, risk_score: f64) -> String {
228        match risk_score {
229            r if r >= 0.8 => "Requires manual review and testing",
230            r if r >= 0.5 => "Consider additional validation",
231            _ => "Safe to apply with standard review",
232        }
233        .to_string()
234    }
235
236    /// Claude: Public method to render preview to string
237    pub fn render_to_string(&self, edit: &ModifiableEdit) -> String {
238        let preview = self.generate_preview(edit);
239        let mut output = Vec::new();
240
241        for section in preview.sections {
242            output.push(format!("═══ {} ═══", section.title));
243            output.push(section.content);
244            output.push("".to_string()); // Empty line between sections
245        }
246
247        output.join("\n")
248    }
249}
250
251// Claude: Clean data structures
252#[derive(Debug)]
253pub struct PreviewResult {
254    pub sections: Vec<PreviewSection>,
255}
256
257#[derive(Debug)]
258pub struct PreviewSection {
259    pub title: String,
260    pub content: String,
261    pub style: SectionStyle,
262}
263
264#[derive(Debug, Clone)]
265pub enum SectionStyle {
266    Info,
267    Warning,
268    Error,
269    Success,
270    Code,
271    Diff,
272}
273
274#[derive(Debug, Clone)]
275pub enum PreviewTheme {
276    Default,
277    Dark,
278    Light,
279}
280
281// Claude: Pluggable syntax highlighting
282pub struct SyntaxHighlighter {
283    rules: HashMap<String, Vec<HighlightRule>>,
284}
285
286impl SyntaxHighlighter {
287    pub fn new() -> Self {
288        let mut rules = HashMap::new();
289
290        // Rust syntax rules
291        rules.insert(
292            ".rs".to_string(),
293            vec![
294                HighlightRule::new(
295                    r"\b(fn|let|mut|pub|struct|enum|impl|trait|use|mod)\b",
296                    "keyword",
297                ),
298                HighlightRule::new(r"//.*$", "comment"),
299                HighlightRule::new(r#""[^"]*""#, "string"),
300                HighlightRule::new(r"\b\d+\b", "number"),
301                HighlightRule::new(r"\b(String|Vec|HashMap|Option|Result)\b", "type"),
302            ],
303        );
304
305        // JavaScript syntax rules
306        rules.insert(
307            ".js".to_string(),
308            vec![
309                HighlightRule::new(
310                    r"\b(function|var|let|const|if|else|for|while|return)\b",
311                    "keyword",
312                ),
313                HighlightRule::new(r"//.*$", "comment"),
314                HighlightRule::new(r#""[^"]*""#, "string"),
315            ],
316        );
317
318        // Python syntax rules
319        rules.insert(
320            ".py".to_string(),
321            vec![
322                HighlightRule::new(
323                    r"\b(def|class|if|else|for|while|return|import|from)\b",
324                    "keyword",
325                ),
326                HighlightRule::new(r"#.*$", "comment"),
327                HighlightRule::new(r#""[^"]*""#, "string"),
328            ],
329        );
330
331        Self { rules }
332    }
333
334    pub fn highlight(&self, code: &str, filename: &str) -> String {
335        let extension = std::path::Path::new(filename)
336            .extension()
337            .and_then(|ext| ext.to_str())
338            .map(|ext| format!(".{}", ext))
339            .unwrap_or_default();
340
341        if let Some(rules) = self.rules.get(&extension) {
342            self.apply_rules(code, rules)
343        } else {
344            code.to_string()
345        }
346    }
347
348    fn apply_rules(&self, code: &str, rules: &[HighlightRule]) -> String {
349        // Simplified highlighting - real implementation would use proper regex/parser
350        let mut result = code.to_string();
351        for rule in rules {
352            // Basic pattern replacement for demonstration
353            if rule.pattern.contains("fn") {
354                let styled_replacement = format!("[{}]fn[/{}] ", rule.style, rule.style);
355                result = result.replace("fn ", &styled_replacement);
356            }
357        }
358        result
359    }
360}
361
362#[derive(Debug)]
363struct HighlightRule {
364    pattern: String,
365    style: String,
366}
367
368impl HighlightRule {
369    fn new(pattern: &str, style: &str) -> Self {
370        Self {
371            pattern: pattern.to_string(),
372            style: style.to_string(),
373        }
374    }
375}
376
377// Claude: Clean diff generation
378pub struct DiffGenerator;
379
380impl DiffGenerator {
381    pub fn new() -> Self {
382        Self
383    }
384
385    pub fn generate_diff(&self, original: &str, modified: &str) -> String {
386        if original == modified {
387            return "No changes detected.".to_string();
388        }
389
390        // Simplified unified diff format
391        let original_lines: Vec<&str> = original.lines().collect();
392        let modified_lines: Vec<&str> = modified.lines().collect();
393
394        let mut diff = Vec::new();
395        diff.push("--- original".to_string());
396        diff.push("+++ modified".to_string());
397
398        // Simple line-by-line comparison
399        let max_lines = original_lines.len().max(modified_lines.len());
400        for i in 0..max_lines {
401            let orig_line = original_lines.get(i).unwrap_or(&"");
402            let mod_line = modified_lines.get(i).unwrap_or(&"");
403
404            if orig_line != mod_line {
405                if !orig_line.is_empty() {
406                    diff.push(format!("-{}", orig_line));
407                }
408                if !mod_line.is_empty() {
409                    diff.push(format!("+{}", mod_line));
410                }
411            } else if !orig_line.is_empty() {
412                diff.push(format!(" {}", orig_line));
413            }
414        }
415
416        diff.join("\n")
417    }
418}
419
420impl Default for DiffGenerator {
421    fn default() -> Self {
422        Self::new()
423    }
424}
425
426#[cfg(test)]
427mod tests {
428    use super::*;
429    use crate::agents::gpt4_agent::ProposedEdit;
430
431    fn create_test_edit() -> ModifiableEdit {
432        let proposed = ProposedEdit {
433            file: "test.rs".to_string(),
434            line_range: (10, 15),
435            new_code: "fn test() {\n    println!(\"Hello\");\n}".to_string(),
436            reason: "Test function".to_string(),
437            confidence: 0.9,
438        };
439        ModifiableEdit::from_proposed_edit(proposed)
440    }
441
442    #[test]
443    fn test_preview_builder() {
444        let preview = EditPreviewBuilder::new()
445            .with_diff(true)
446            .with_risk_assessment(false)
447            .with_syntax_highlighting(true)
448            .build();
449
450        assert!(preview.config.show_diff);
451        assert!(!preview.config.show_risk_assessment);
452        assert!(preview.config.syntax_highlighting);
453    }
454
455    #[test]
456    fn test_preview_generation() {
457        let edit = create_test_edit();
458        let preview = EditPreviewBuilder::new().build();
459        let result = preview.generate_preview(&edit);
460
461        assert!(!result.sections.is_empty());
462        assert!(result
463            .sections
464            .iter()
465            .any(|s| s.title.contains("Edit Summary")));
466        assert!(result
467            .sections
468            .iter()
469            .any(|s| s.title.contains("Code Preview")));
470    }
471
472    #[test]
473    fn test_diff_generation() {
474        let generator = DiffGenerator::new();
475        let original = "line1\nline2\nline3";
476        let modified = "line1\nmodified_line2\nline3";
477
478        let diff = generator.generate_diff(original, modified);
479        assert!(diff.contains("-line2"));
480        assert!(diff.contains("+modified_line2"));
481    }
482
483    #[test]
484    fn test_confidence_visualization() {
485        let preview = EditPreviewBuilder::new().build();
486        let vis = preview.create_confidence_visualization(0.7);
487
488        assert!(vis.contains("█"));
489        assert!(vis.contains("░"));
490        // The length should be: 1 (opening bracket) + 10 (bar characters) + 1 (closing bracket) = 12
491        // But Unicode characters might be counted differently, so let's check that it's reasonable
492        assert!(vis.len() >= 12); // Allow for Unicode character encoding
493    }
494
495    #[test]
496    fn test_syntax_highlighter() {
497        let highlighter = SyntaxHighlighter::new();
498        let code = "fn main() {\n    println!(\"Hello\");\n}";
499        let highlighted = highlighter.highlight(code, "test.rs");
500
501        // Basic test - should include keyword highlighting
502        assert!(highlighted.contains("[keyword]fn[/keyword]"));
503        assert!(highlighted.contains("main()"));
504    }
505
506    #[test]
507    fn test_render_to_string() {
508        let edit = create_test_edit();
509        let preview = EditPreviewBuilder::new().build();
510        let output = preview.render_to_string(&edit);
511
512        assert!(output.contains("═══"));
513        assert!(output.contains("Edit Summary"));
514        assert!(output.contains("Code Preview"));
515    }
516}