codelens_engine/file_ops/
writer.rs1use 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 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 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: ®ex::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}