file_editor/
editor.rs

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