Skip to main content

codelens_engine/file_ops/
writer.rs

1use crate::project::ProjectRoot;
2use anyhow::{Context, Result, bail};
3use regex::Regex;
4use std::fs;
5
6pub fn create_text_file(
7    project: &ProjectRoot,
8    relative_path: &str,
9    content: &str,
10    overwrite: bool,
11) -> Result<()> {
12    let resolved = project.resolve(relative_path)?;
13    if !overwrite && resolved.exists() {
14        bail!("file already exists: {}", resolved.display());
15    }
16    if let Some(parent) = resolved.parent() {
17        fs::create_dir_all(parent)
18            .with_context(|| format!("failed to create directories for {}", resolved.display()))?;
19    }
20    fs::write(&resolved, content).with_context(|| format!("failed to write {}", resolved.display()))
21}
22
23pub fn delete_lines(
24    project: &ProjectRoot,
25    relative_path: &str,
26    start_line: usize,
27    end_line: usize,
28) -> Result<String> {
29    let resolved = project.resolve(relative_path)?;
30    let content = fs::read_to_string(&resolved)
31        .with_context(|| format!("failed to read {}", resolved.display()))?;
32    let mut lines: Vec<&str> = content.lines().collect();
33    let total = lines.len();
34    if start_line < 1 || start_line > total + 1 {
35        bail!(
36            "start_line {} out of range (file has {} lines)",
37            start_line,
38            total
39        );
40    }
41    if end_line < start_line || end_line > total + 1 {
42        bail!("end_line {} out of range", end_line);
43    }
44    // Convert from 1-indexed inclusive-start/exclusive-end to 0-indexed
45    let from = start_line - 1;
46    let to = (end_line - 1).min(lines.len());
47    lines.drain(from..to);
48    let result = lines.join("\n");
49    // Preserve trailing newline if original had one
50    let result = if content.ends_with('\n') {
51        format!("{result}\n")
52    } else {
53        result
54    };
55    fs::write(&resolved, &result)
56        .with_context(|| format!("failed to write {}", resolved.display()))?;
57    Ok(result)
58}
59
60pub fn insert_at_line(
61    project: &ProjectRoot,
62    relative_path: &str,
63    line: usize,
64    content_to_insert: &str,
65) -> Result<String> {
66    let resolved = project.resolve(relative_path)?;
67    let content = fs::read_to_string(&resolved)
68        .with_context(|| format!("failed to read {}", resolved.display()))?;
69    let mut lines: Vec<&str> = content.lines().collect();
70    let total = lines.len();
71    if line < 1 || line > total + 1 {
72        bail!("line {} out of range (file has {} lines)", line, total);
73    }
74    let insert_pos = line - 1;
75    let new_lines: Vec<&str> = content_to_insert.lines().collect();
76    for (i, new_line) in new_lines.iter().enumerate() {
77        lines.insert(insert_pos + i, new_line);
78    }
79    let result = lines.join("\n");
80    let result = if content.ends_with('\n') || content_to_insert.ends_with('\n') {
81        format!("{result}\n")
82    } else {
83        result
84    };
85    fs::write(&resolved, &result)
86        .with_context(|| format!("failed to write {}", resolved.display()))?;
87    Ok(result)
88}
89
90pub fn replace_lines(
91    project: &ProjectRoot,
92    relative_path: &str,
93    start_line: usize,
94    end_line: usize,
95    new_content: &str,
96) -> Result<String> {
97    let resolved = project.resolve(relative_path)?;
98    let content = fs::read_to_string(&resolved)
99        .with_context(|| format!("failed to read {}", resolved.display()))?;
100    let mut lines: Vec<&str> = content.lines().collect();
101    let total = lines.len();
102    if start_line < 1 || start_line > total + 1 {
103        bail!(
104            "start_line {} out of range (file has {} lines)",
105            start_line,
106            total
107        );
108    }
109    if end_line < start_line || end_line > total + 1 {
110        bail!("end_line {} out of range", end_line);
111    }
112    let from = start_line - 1;
113    let to = (end_line - 1).min(lines.len());
114    lines.drain(from..to);
115    let replacement: Vec<&str> = new_content.lines().collect();
116    for (i, rep_line) in replacement.iter().enumerate() {
117        lines.insert(from + i, rep_line);
118    }
119    let result = lines.join("\n");
120    let result = if content.ends_with('\n') {
121        format!("{result}\n")
122    } else {
123        result
124    };
125    fs::write(&resolved, &result)
126        .with_context(|| format!("failed to write {}", resolved.display()))?;
127    Ok(result)
128}
129
130pub fn replace_content(
131    project: &ProjectRoot,
132    relative_path: &str,
133    old_text: &str,
134    new_text: &str,
135    regex_mode: bool,
136) -> Result<(String, usize)> {
137    let resolved = project.resolve(relative_path)?;
138    let content = fs::read_to_string(&resolved)
139        .with_context(|| format!("failed to read {}", resolved.display()))?;
140    let (result, count) = if regex_mode {
141        let re = Regex::new(old_text).with_context(|| format!("invalid regex: {old_text}"))?;
142        let mut count = 0usize;
143        let replaced = re
144            .replace_all(&content, |_caps: &regex::Captures| {
145                count += 1;
146                new_text
147            })
148            .into_owned();
149        (replaced, count)
150    } else {
151        let count = content.matches(old_text).count();
152        let replaced = content.replace(old_text, new_text);
153        (replaced, count)
154    };
155    fs::write(&resolved, &result)
156        .with_context(|| format!("failed to write {}", resolved.display()))?;
157    Ok((result, count))
158}
159
160pub fn replace_symbol_body(
161    project: &ProjectRoot,
162    relative_path: &str,
163    symbol_name: &str,
164    name_path: Option<&str>,
165    new_body: &str,
166) -> Result<String> {
167    let (start_byte, end_byte) =
168        crate::symbols::find_symbol_range(project, relative_path, symbol_name, name_path)?;
169    let resolved = project.resolve(relative_path)?;
170    let content = fs::read_to_string(&resolved)
171        .with_context(|| format!("failed to read {}", resolved.display()))?;
172    let bytes = content.as_bytes();
173    let mut result = Vec::with_capacity(bytes.len());
174    result.extend_from_slice(&bytes[..start_byte]);
175    result.extend_from_slice(new_body.as_bytes());
176    result.extend_from_slice(&bytes[end_byte..]);
177    let result =
178        String::from_utf8(result).with_context(|| "result is not valid UTF-8 after replacement")?;
179    fs::write(&resolved, &result)
180        .with_context(|| format!("failed to write {}", resolved.display()))?;
181    Ok(result)
182}
183
184pub fn insert_before_symbol(
185    project: &ProjectRoot,
186    relative_path: &str,
187    symbol_name: &str,
188    name_path: Option<&str>,
189    content_to_insert: &str,
190) -> Result<String> {
191    let (start_byte, _) =
192        crate::symbols::find_symbol_range(project, relative_path, symbol_name, name_path)?;
193    let resolved = project.resolve(relative_path)?;
194    let content = fs::read_to_string(&resolved)
195        .with_context(|| format!("failed to read {}", resolved.display()))?;
196    let bytes = content.as_bytes();
197    let mut result = Vec::with_capacity(bytes.len() + content_to_insert.len());
198    result.extend_from_slice(&bytes[..start_byte]);
199    result.extend_from_slice(content_to_insert.as_bytes());
200    result.extend_from_slice(&bytes[start_byte..]);
201    let result =
202        String::from_utf8(result).with_context(|| "result is not valid UTF-8 after insertion")?;
203    fs::write(&resolved, &result)
204        .with_context(|| format!("failed to write {}", resolved.display()))?;
205    Ok(result)
206}
207
208pub fn insert_after_symbol(
209    project: &ProjectRoot,
210    relative_path: &str,
211    symbol_name: &str,
212    name_path: Option<&str>,
213    content_to_insert: &str,
214) -> Result<String> {
215    let (_, end_byte) =
216        crate::symbols::find_symbol_range(project, relative_path, symbol_name, name_path)?;
217    let resolved = project.resolve(relative_path)?;
218    let content = fs::read_to_string(&resolved)
219        .with_context(|| format!("failed to read {}", resolved.display()))?;
220    let bytes = content.as_bytes();
221    let mut result = Vec::with_capacity(bytes.len() + content_to_insert.len());
222    result.extend_from_slice(&bytes[..end_byte]);
223    result.extend_from_slice(content_to_insert.as_bytes());
224    result.extend_from_slice(&bytes[end_byte..]);
225    let result =
226        String::from_utf8(result).with_context(|| "result is not valid UTF-8 after insertion")?;
227    fs::write(&resolved, &result)
228        .with_context(|| format!("failed to write {}", resolved.display()))?;
229    Ok(result)
230}