file_editor/
editor.rs

1use std::{
2    fs, io,
3    path::{Path, PathBuf},
4};
5
6use crate::utils::line_indent;
7
8/// Handle to a UTF-8 text file kept in memory until [`save`](Editor::save) is called.
9///
10/// All mutating methods return `&mut self`, enabling a fluent builder style.
11///
12/// ```no_run
13/// # use file_editor::Editor;
14/// # fn run() -> std::io::Result<()> {
15/// Editor::open("Cargo.toml")?
16///     .insert_before("[dependencies]", "regex = \"1\"\n", false)
17///     .save()?;
18/// # Ok(()) }
19/// ```
20#[derive(Debug, Clone)]
21pub struct Editor {
22    path: PathBuf,
23    buf: String,
24    dirty: bool,
25}
26
27impl Editor {
28    /*──────────────── Constructors ───────────────────────────────*/
29
30    /// **Create** or truncate a file and return an editor over it.
31    ///
32    /// Equivalent to `fs::write(path, "")` followed by [`open`](Editor::open).
33    pub fn create<P: AsRef<Path>>(path: P) -> io::Result<Self> {
34        fs::write(&path, "")?;
35        Self::open(path)
36    }
37
38    /// **Open** an existing UTF-8 file into an in-memory buffer.
39    pub fn open<P: AsRef<Path>>(path: P) -> io::Result<Self> {
40        let p = path.as_ref().to_owned();
41        let buf = fs::read_to_string(&p)?;
42        Ok(Self {
43            path: p,
44            buf,
45            dirty: false,
46        })
47    }
48
49    /*──────────────── Meta operations ────────────────────────────*/
50
51    /// Rename the underlying file on disk **and** update the internal path.
52    pub fn rename<P: AsRef<Path>>(&mut self, new_name: P) -> io::Result<&mut Self> {
53        fs::rename(&self.path, &new_name)?;
54        self.path = new_name.as_ref().to_owned();
55        Ok(self)
56    }
57
58    /// Write the in-memory buffer back to disk **iff** it was modified.
59    ///
60    /// Returns `Ok(self)` even when there was nothing to do.
61    pub fn save(&mut self) -> io::Result<&mut Self> {
62        if self.dirty {
63            fs::write(&self.path, &self.buf)?;
64            self.dirty = false;
65        }
66        Ok(self)
67    }
68
69    /*──────────────── Content operations ────────────────────────*/
70
71    /// Insert `text` **at the beginning** of the buffer.
72    pub fn prepend(&mut self, text: &str) -> &mut Self {
73        self.buf.insert_str(0, text);
74        self.dirty = true;
75        self
76    }
77
78    /// Append `text` **to the end** of the buffer.
79    pub fn append(&mut self, text: &str) -> &mut Self {
80        self.buf.push_str(text);
81        self.dirty = true;
82        self
83    }
84
85    /// Insert `text` **before** the first occurrence of `marker`.
86    ///
87    /// * If `same_indent` is `true`, the current indentation of the line
88    ///   containing `marker` is copied and prepended to `text`.
89    pub fn insert_before(&mut self, marker: &str, text: &str, same_indent: bool) -> &mut Self {
90        if let Some(pos) = self.buf.find(marker) {
91            let insertion = if same_indent {
92                format!("{}{}", line_indent(&self.buf, pos), text)
93            } else {
94                text.to_owned()
95            };
96            self.buf.insert_str(pos, &insertion);
97            self.dirty = true;
98        }
99        self
100    }
101
102    /// Insert `text` **after** the first occurrence of `marker`.
103    ///
104    /// * If `marker` ends a line, the insertion starts on the next line.  
105    /// * Otherwise the insertion is in-line; a space is auto-inserted when needed.  
106    /// * When `same_indent` is `true`, every *subsequent* line in `text`
107    ///   is indented to match the marker line.
108    pub fn insert_after(&mut self, marker: &str, text: &str, same_indent: bool) -> &mut Self {
109        if let Some(pos) = self.buf.find(marker) {
110            let after_marker = pos + marker.len();
111            let insert_pos = if self.buf[after_marker..].starts_with('\n') {
112                after_marker + 1 // insert on next line
113            } else {
114                after_marker // insert in-line
115            };
116
117            let mut insertion = text.to_owned();
118
119            // Auto-space for inline insertions like `foo|bar` → `foo X bar`
120            if insert_pos == after_marker
121                && !insertion.starts_with(char::is_whitespace)
122                && !self.buf[insert_pos..].starts_with(char::is_whitespace)
123            {
124                insertion.insert(0, ' ');
125            }
126
127            // Re-indent multiline insertions
128            if same_indent && insertion.contains('\n') {
129                let indent = line_indent(&self.buf, pos);
130                insertion = insertion
131                    .split('\n')
132                    .enumerate()
133                    .map(|(i, line)| {
134                        if i == 0 {
135                            line.to_owned()
136                        } else {
137                            format!("{indent}{line}")
138                        }
139                    })
140                    .collect::<Vec<_>>()
141                    .join("\n");
142            }
143
144            self.buf.insert_str(insert_pos, &insertion);
145            self.dirty = true;
146        }
147        self
148    }
149
150    /// Replace the first occurrence of `marker` with `text`.
151    ///
152    /// When `same_indent` is `true`, the replacement receives the indentation
153    /// that preceded the marker.
154    pub fn replace_marker(&mut self, marker: &str, text: &str, same_indent: bool) -> &mut Self {
155        if let Some(pos) = self.buf.find(marker) {
156            let indent = if same_indent {
157                line_indent(&self.buf, pos)
158            } else {
159                String::new()
160            };
161            self.buf = self.buf.replacen(marker, &(indent + text), 1);
162            self.dirty = true;
163        }
164        self
165    }
166
167    /// Return **1-based** line numbers where `pattern` appears.
168    ///
169    /// Pass `limit = Some(n)` to cap the number of results.
170    pub fn find_lines(&self, pattern: &str, limit: Option<usize>) -> Vec<usize> {
171        self.buf
172            .lines()
173            .enumerate()
174            .filter(|(_, line)| line.contains(pattern))
175            .map(|(i, _)| i + 1)
176            .take(limit.unwrap_or(usize::MAX))
177            .collect()
178    }
179
180    /// Remove every occurrence of `pattern`.
181    pub fn erase(&mut self, pattern: &str) -> &mut Self {
182        self.buf = self.buf.replace(pattern, "");
183        self.dirty = true;
184        self
185    }
186
187    /// Replace every occurrence of `pattern` with `replacement`.
188    pub fn replace(&mut self, pattern: &str, replacement: &str) -> &mut Self {
189        self.buf = self.buf.replace(pattern, replacement);
190        self.dirty = true;
191        self
192    }
193
194    /// Mask every occurrence of `pattern` with `mask` (default: `"***"`).
195    pub fn mask(&mut self, pattern: &str, mask: &str) -> &mut Self {
196        self.replace(pattern, mask)
197    }
198}