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}