flop_cli/
editor.rs

1use anyhow::{Context, Result};
2use regex::Regex;
3use std::collections::{HashMap, HashSet};
4use std::fs;
5use std::path::PathBuf;
6
7use crate::types::Match;
8
9pub fn apply_changes(matches: &[Match], uncomment: bool) -> Result<()> {
10    // Group matches by file
11    let mut files_map: HashMap<PathBuf, Vec<&Match>> = HashMap::new();
12
13    for m in matches {
14        files_map.entry(m.file_path.clone()).or_default().push(m);
15    }
16
17    for (file_path, file_matches) in files_map {
18        let content = fs::read_to_string(&file_path)?;
19        let mut lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
20
21        // Sort by line number in reverse order to avoid index shifting
22        let mut sorted_matches = file_matches;
23        sorted_matches.sort_by(|a, b| b.line_number.cmp(&a.line_number));
24
25        for m in sorted_matches {
26            // For multiline statements, only comment/uncomment the first line
27            // This will effectively disable/enable the entire statement
28            let idx = m.line_number - 1;
29            if idx < lines.len() {
30                if uncomment {
31                    // Remove the comment
32                    lines[idx] = uncomment_line(&lines[idx]);
33                } else {
34                    // Add comment
35                    lines[idx] = comment_line(&lines[idx]);
36                }
37            }
38        }
39
40        let new_content = lines.join("\n") + "\n";
41        fs::write(&file_path, new_content)
42            .with_context(|| format!("Failed to write file: {}", file_path.display()))?;
43    }
44
45    Ok(())
46}
47
48pub fn delete_changes(matches: &[Match]) -> Result<()> {
49    // Group matches by file
50    let mut files_map: HashMap<PathBuf, Vec<&Match>> = HashMap::new();
51
52    for m in matches {
53        files_map.entry(m.file_path.clone()).or_default().push(m);
54    }
55
56    for (file_path, file_matches) in files_map {
57        let content = fs::read_to_string(&file_path)?;
58        let lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
59
60        // Sort by line number in reverse order to avoid index shifting
61        let mut sorted_matches = file_matches;
62        sorted_matches.sort_by(|a, b| b.line_number.cmp(&a.line_number));
63
64        // Collect line numbers to delete (all lines from start to end of each statement)
65        let mut lines_to_delete: HashSet<usize> = HashSet::new();
66        for m in sorted_matches {
67            for line_num in m.line_number..=m.end_line_number {
68                lines_to_delete.insert(line_num - 1);
69            }
70        }
71
72        // Filter out lines to delete
73        let new_lines: Vec<String> = lines
74            .into_iter()
75            .enumerate()
76            .filter(|(idx, _)| !lines_to_delete.contains(idx))
77            .map(|(_, line)| line)
78            .collect();
79
80        let new_content = new_lines.join("\n") + "\n";
81        fs::write(&file_path, new_content)
82            .with_context(|| format!("Failed to write file: {}", file_path.display()))?;
83    }
84
85    Ok(())
86}
87
88fn comment_line(line: &str) -> String {
89    // Find the first non-whitespace character and insert // before it
90    let trimmed = line.trim_start();
91    let leading_spaces = line.len() - trimmed.len();
92    format!("{}// {}", " ".repeat(leading_spaces), trimmed)
93}
94
95fn uncomment_line(line: &str) -> String {
96    // Remove the // comment marker
97    let re = Regex::new(r"^(\s*)//\s*(.*)$").unwrap();
98    if let Some(caps) = re.captures(line) {
99        format!("{}{}", &caps[1], &caps[2])
100    } else {
101        line.to_string()
102    }
103}