oli_server/tools/fs/
file_ops.rs1use anyhow::{Context, Result};
2use std::fs::{self, File};
3use std::io::{Read, Write};
4use std::path::{Path, PathBuf};
5
6use super::diff::DiffTools;
7
8pub struct FileOps;
9
10impl FileOps {
11 pub fn read_file(path: &Path) -> Result<String> {
12 let mut file =
13 File::open(path).with_context(|| format!("Failed to open file: {}", path.display()))?;
14 let mut content = String::new();
15 file.read_to_string(&mut content)
16 .with_context(|| format!("Failed to read file: {}", path.display()))?;
17 Ok(content)
18 }
19
20 pub fn read_file_with_line_numbers(path: &Path) -> Result<String> {
21 let content = Self::read_file(path)?;
22 let numbered_content = content
23 .lines()
24 .enumerate()
25 .map(|(i, line)| format!("{:4} | {}", i + 1, line))
26 .collect::<Vec<_>>()
27 .join("\n");
28 Ok(numbered_content)
29 }
30
31 pub fn read_file_lines(path: &Path, offset: usize, limit: Option<usize>) -> Result<String> {
32 let content = Self::read_file(path)?;
33 let lines: Vec<&str> = content.lines().collect();
34 let start = offset.min(lines.len());
35 let end = match limit {
36 Some(limit) => (start + limit).min(lines.len()),
37 None => lines.len(),
38 };
39
40 let numbered_content = lines[start..end]
41 .iter()
42 .enumerate()
43 .map(|(i, line)| format!("{:4} | {}", i + start + 1, line))
44 .collect::<Vec<_>>()
45 .join("\n");
46 Ok(numbered_content)
47 }
48
49 pub fn generate_write_diff(path: &Path, content: &str) -> Result<(String, bool)> {
50 let is_new_file = !path.exists();
52
53 let old_content = if is_new_file {
54 String::new()
55 } else {
56 Self::read_file(path)?
57 };
58
59 let diff_lines = DiffTools::generate_diff(&old_content, content);
61 let formatted_diff = DiffTools::format_diff(&diff_lines, &path.display().to_string())?;
62
63 Ok((formatted_diff, is_new_file))
64 }
65
66 pub fn write_file(path: &Path, content: &str) -> Result<()> {
67 if let Some(parent) = path.parent() {
69 fs::create_dir_all(parent)
70 .with_context(|| format!("Failed to create directory: {}", parent.display()))?;
71 }
72
73 let mut file = File::create(path)
74 .with_context(|| format!("Failed to create file: {}", path.display()))?;
75 file.write_all(content.as_bytes())
76 .with_context(|| format!("Failed to write to file: {}", path.display()))?;
77 Ok(())
78 }
79
80 pub fn write_file_with_diff(path: &Path, content: &str) -> Result<String> {
81 let (diff, _) = Self::generate_write_diff(path, content)?;
82 Self::write_file(path, content)?;
83 Ok(diff)
84 }
85
86 pub fn generate_edit_diff(
87 path: &Path,
88 old_string: &str,
89 new_string: &str,
90 ) -> Result<(String, String)> {
91 let content = Self::read_file(path)?;
92
93 let occurrences = content.matches(old_string).count();
95 if occurrences == 0 {
96 anyhow::bail!("The string to replace was not found in the file");
97 }
98 if occurrences > 1 {
99 anyhow::bail!("The string to replace appears multiple times in the file ({}). Please provide more context to ensure a unique match.", occurrences);
100 }
101
102 let new_content = content.replace(old_string, new_string);
103
104 let diff_lines = DiffTools::generate_diff(&content, &new_content);
106 let formatted_diff = DiffTools::format_diff(&diff_lines, &path.display().to_string())?;
107
108 Ok((new_content, formatted_diff))
109 }
110
111 pub fn edit_file(path: &Path, old_string: &str, new_string: &str) -> Result<String> {
112 let (new_content, diff) = Self::generate_edit_diff(path, old_string, new_string)?;
113 Self::write_file(path, &new_content)?;
114 Ok(diff)
115 }
116
117 pub fn list_directory(path: &Path) -> Result<Vec<PathBuf>> {
118 let entries = fs::read_dir(path)
119 .with_context(|| format!("Failed to read directory: {}", path.display()))?;
120
121 let mut paths = Vec::new();
122 for entry in entries {
123 let entry = entry.context("Failed to read directory entry")?;
124 paths.push(entry.path());
125 }
126
127 paths.sort();
129
130 Ok(paths)
131 }
132
133 #[allow(dead_code)]
134 pub fn create_directory(path: &Path) -> Result<()> {
135 fs::create_dir_all(path)
136 .with_context(|| format!("Failed to create directory: {}", path.display()))?;
137 Ok(())
138 }
139
140 #[allow(dead_code)]
141 pub fn get_file_info(path: &Path) -> Result<String> {
142 let metadata = fs::metadata(path)
143 .with_context(|| format!("Failed to get metadata for: {}", path.display()))?;
144
145 let file_type = if metadata.is_dir() {
146 "Directory"
147 } else if metadata.is_file() {
148 "File"
149 } else {
150 "Unknown"
151 };
152
153 let size = metadata.len();
154 let modified = metadata
155 .modified()
156 .context("Failed to get modification time")?;
157
158 let info = format!(
159 "Path: {}\nType: {}\nSize: {} bytes\nModified: {:?}",
160 path.display(),
161 file_type,
162 size,
163 modified
164 );
165
166 Ok(info)
167 }
168}