oli_server/tools/fs/
file_ops.rs

1use 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        // Check if file exists to determine if this is an update or new file
51        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        // Generate a diff
60        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        // Ensure parent directory exists
68        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        // Count occurrences to ensure we're replacing a unique string
94        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        // Generate a diff
105        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        // Sort by name
128        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}