1use std::collections::BTreeMap;
2use std::io::ErrorKind;
3use std::ops::Range;
4use std::path::{Path, PathBuf};
5
6use toml_edit::{DocumentMut, TomlError};
7
8use crate::error::DocError;
9use crate::stamp::{Disk, Own, Seen, Stamp};
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum Format {
13 Toml,
14 Text,
15}
16
17impl Format {
18 pub fn of(path: &Path) -> Format {
19 match path.extension().and_then(|ext| ext.to_str()) {
20 Some(ext) if ext.eq_ignore_ascii_case("toml") => Format::Toml,
21 _ => Format::Text,
22 }
23 }
24}
25
26#[derive(Clone, Debug)]
27pub struct File {
28 format: Format,
29 text: String,
30 doc: Option<DocumentMut>,
31 error: Option<TomlError>,
32 generation: u64,
33 own: Own,
34}
35
36impl File {
37 fn new(format: Format, text: String, generation: u64) -> File {
38 let mut file = File {
39 format,
40 text: String::new(),
41 doc: None,
42 error: None,
43 generation,
44 own: Own::default(),
45 };
46 file.own.read(&text);
47 file.set(text);
48 file
49 }
50
51 fn set(&mut self, text: String) {
52 self.text = text;
53 if self.format == Format::Toml {
54 match self.text.parse::<DocumentMut>() {
55 Ok(doc) => {
56 self.doc = Some(doc);
57 self.error = None;
58 }
59 Err(error) => {
60 self.doc = None;
61 self.error = Some(error);
62 }
63 }
64 }
65 }
66
67 pub fn format(&self) -> Format {
68 self.format
69 }
70
71 pub fn text(&self) -> &str {
72 &self.text
73 }
74
75 pub fn doc(&self) -> Option<&DocumentMut> {
76 self.doc.as_ref()
77 }
78
79 pub fn error(&self) -> Option<&TomlError> {
80 self.error.as_ref()
81 }
82
83 pub fn generation(&self) -> u64 {
84 self.generation
85 }
86
87 pub fn disk_stamp(&self) -> Option<Stamp> {
88 self.own.current()
89 }
90
91 pub fn dirty(&self) -> bool {
92 self.own.current() != Some(Stamp::of(&self.text))
93 }
94}
95
96#[derive(Clone, Debug)]
97struct Snapshot {
98 generation: u64,
99 files: Vec<(PathBuf, Option<File>)>,
100}
101
102#[derive(Clone, Debug, Default)]
103pub struct Files {
104 files: BTreeMap<PathBuf, File>,
105 generation: u64,
106}
107
108fn read_error(path: &Path, what: &str, error: std::io::Error) -> DocError {
109 DocError::new(path, "", format!("cannot {what}: {error}"))
110}
111
112fn floor(text: &str, byte: usize) -> usize {
113 let mut byte = byte.min(text.len());
114 while !text.is_char_boundary(byte) {
115 byte -= 1;
116 }
117 byte
118}
119
120pub(crate) fn check_range(path: &Path, text: &str, range: &Range<usize>) -> Result<(), DocError> {
121 if range.start > range.end || range.end > text.len() {
122 return Err(DocError::new(
123 path,
124 "",
125 format!(
126 "bytes {}..{} are outside the text, which has {}",
127 range.start,
128 range.end,
129 text.len()
130 ),
131 ));
132 }
133 if floor(text, range.start) != range.start || floor(text, range.end) != range.end {
134 return Err(DocError::new(
135 path,
136 "",
137 format!("bytes {}..{} split a character", range.start, range.end),
138 ));
139 }
140 Ok(())
141}
142
143impl Files {
144 pub fn new() -> Files {
145 Files::default()
146 }
147
148 fn bump(&mut self) -> u64 {
149 self.generation += 1;
150 self.generation
151 }
152
153 pub fn open(&mut self, path: impl Into<PathBuf>, text: impl Into<String>) -> &File {
154 let path = path.into();
155 let format = Format::of(&path);
156 self.open_as(path, text, format)
157 }
158
159 pub fn open_as(
160 &mut self,
161 path: impl Into<PathBuf>,
162 text: impl Into<String>,
163 format: Format,
164 ) -> &File {
165 let generation = self.bump();
166 let path = path.into();
167 self.files
168 .insert(path.clone(), File::new(format, text.into(), generation));
169 &self.files[&path]
170 }
171
172 pub fn load(&mut self, path: impl Into<PathBuf>) -> Result<&File, DocError> {
173 let path = path.into();
174 let text =
175 std::fs::read_to_string(&path).map_err(|error| read_error(&path, "read", error))?;
176 Ok(self.open(path, text))
177 }
178
179 pub fn close(&mut self, path: &Path) -> Option<File> {
180 let file = self.files.remove(path)?;
181 self.bump();
182 Some(file)
183 }
184
185 pub fn get(&self, path: &Path) -> Option<&File> {
186 self.files.get(path)
187 }
188
189 pub fn text(&self, path: &Path) -> Option<&str> {
190 self.get(path).map(File::text)
191 }
192
193 pub fn doc(&self, path: &Path) -> Option<&DocumentMut> {
194 self.get(path).and_then(File::doc)
195 }
196
197 pub fn paths(&self) -> impl Iterator<Item = &Path> {
198 self.files.keys().map(PathBuf::as_path)
199 }
200
201 pub fn len(&self) -> usize {
202 self.files.len()
203 }
204
205 pub fn is_empty(&self) -> bool {
206 self.files.is_empty()
207 }
208
209 pub fn generation(&self) -> u64 {
210 self.generation
211 }
212
213 pub fn generation_of(&self, path: &Path) -> Option<u64> {
214 self.get(path).map(File::generation)
215 }
216
217 fn file(&self, path: &Path) -> Result<&File, DocError> {
218 self.files
219 .get(path)
220 .ok_or_else(|| DocError::new(path, "", "the file is not open"))
221 }
222
223 pub fn set_text(&mut self, path: &Path, text: impl Into<String>) -> Result<bool, DocError> {
224 let text = text.into();
225 if self.file(path)?.text == text {
226 return Ok(false);
227 }
228 let generation = self.bump();
229 let file = self.files.get_mut(path).expect("the file is open");
230 file.set(text);
231 file.generation = generation;
232 Ok(true)
233 }
234
235 pub(crate) fn set_parsed(
236 &mut self,
237 path: &Path,
238 text: String,
239 doc: DocumentMut,
240 ) -> Result<bool, DocError> {
241 let file = self.file(path)?;
242 if file.text == text {
243 return Ok(false);
244 }
245 if file.format != Format::Toml {
246 return self.set_text(path, text);
247 }
248 let generation = self.bump();
249 let file = self.files.get_mut(path).expect("the file is open");
250 file.text = text;
251 file.doc = Some(doc);
252 file.error = None;
253 file.generation = generation;
254 Ok(true)
255 }
256
257 pub fn splice(
258 &mut self,
259 path: &Path,
260 range: Range<usize>,
261 new: &str,
262 ) -> Result<bool, DocError> {
263 let file = self.file(path)?;
264 check_range(path, &file.text, &range)?;
265 if &file.text[range.clone()] == new {
266 return Ok(false);
267 }
268 let mut text = String::with_capacity(file.text.len() - range.len() + new.len());
269 text.push_str(&file.text[..range.start]);
270 text.push_str(new);
271 text.push_str(&file.text[range.end..]);
272 self.set_text(path, text)
273 }
274
275 pub fn save(&mut self, path: &Path) -> Result<Stamp, DocError> {
276 let file = self.file(path)?;
277 let name = path
278 .file_name()
279 .map_or_else(|| "file".to_string(), |n| n.to_string_lossy().into_owned());
280 let temp = path.with_file_name(format!(".{name}.save-tmp"));
281 std::fs::write(&temp, &file.text).map_err(|error| read_error(path, "write", error))?;
282 std::fs::rename(&temp, path).map_err(|error| {
283 let _ = std::fs::remove_file(&temp);
284 read_error(path, "replace", error)
285 })?;
286 let file = self.files.get_mut(path).expect("the file is open");
287 Ok(file.own.wrote(&file.text))
288 }
289
290 pub fn seen(&self, path: &Path, stamp: Stamp) -> Option<Seen> {
291 self.get(path).map(|file| file.own.seen(stamp))
292 }
293
294 pub fn disk(&self, path: &Path) -> Result<Disk, DocError> {
295 let file = self.file(path)?;
296 let text = match std::fs::read_to_string(path) {
297 Ok(text) => text,
298 Err(error) if error.kind() == ErrorKind::NotFound => return Ok(Disk::Missing),
299 Err(error) => return Err(read_error(path, "read", error)),
300 };
301 Ok(match file.own.seen(Stamp::of(&text)) {
302 Seen::Current => Disk::Same,
303 Seen::Earlier => Disk::Own,
304 Seen::Outside => Disk::Outside(text),
305 })
306 }
307
308 pub fn reload(&mut self, path: &Path, text: impl Into<String>) -> Result<bool, DocError> {
309 let text = text.into();
310 let changed = self.set_text(path, text.clone())?;
311 self.files
312 .get_mut(path)
313 .expect("the file is open")
314 .own
315 .read(&text);
316 Ok(changed)
317 }
318
319 fn snapshot(&self, paths: &[PathBuf]) -> Snapshot {
320 Snapshot {
321 generation: self.generation,
322 files: paths
323 .iter()
324 .map(|path| (path.clone(), self.files.get(path).cloned()))
325 .collect(),
326 }
327 }
328
329 fn restore(&mut self, snapshot: Snapshot) {
330 self.generation = snapshot.generation;
331 for (path, file) in snapshot.files {
332 match file {
333 Some(file) => {
334 self.files.insert(path, file);
335 }
336 None => {
337 self.files.remove(&path);
338 }
339 }
340 }
341 }
342
343 pub fn attempt<T>(
344 &mut self,
345 paths: &[PathBuf],
346 change: impl FnOnce(&mut Files) -> Result<T, DocError>,
347 ) -> Result<T, DocError> {
348 let snapshot = self.snapshot(paths);
349 change(self).inspect_err(|_| self.restore(snapshot))
350 }
351}
352
353#[cfg(test)]
354mod tests {
355 use super::*;
356
357 fn path(name: &str) -> PathBuf {
358 PathBuf::from(name)
359 }
360
361 #[test]
362 fn a_toml_file_keeps_a_document_in_step() {
363 let mut files = Files::new();
364 files.open("a.toml", "x = 1\n");
365 assert_eq!(
366 files.doc(&path("a.toml")).unwrap()["x"].as_integer(),
367 Some(1)
368 );
369 files.set_text(&path("a.toml"), "x = 2\n").unwrap();
370 assert_eq!(
371 files.doc(&path("a.toml")).unwrap()["x"].as_integer(),
372 Some(2)
373 );
374 files.set_text(&path("a.toml"), "x = \n").unwrap();
375 assert!(files.doc(&path("a.toml")).is_none());
376 assert!(files.get(&path("a.toml")).unwrap().error().is_some());
377 files.open("notes.txt", "x = \n");
378 assert_eq!(
379 files.get(&path("notes.txt")).unwrap().format(),
380 Format::Text
381 );
382 assert!(files.get(&path("notes.txt")).unwrap().error().is_none());
383 }
384
385 #[test]
386 fn the_generation_moves_only_on_a_real_change() {
387 let mut files = Files::new();
388 files.open("a.toml", "x = 1\n");
389 let a = path("a.toml");
390 let before = (files.generation(), files.generation_of(&a));
391 assert!(!files.set_text(&a, "x = 1\n").unwrap());
392 assert!(!files.splice(&a, 4..5, "1").unwrap());
393 assert_eq!((files.generation(), files.generation_of(&a)), before);
394 assert!(files.splice(&a, 4..5, "7").unwrap());
395 assert_eq!(files.text(&a), Some("x = 7\n"));
396 assert!(files.generation() > before.0);
397 assert_eq!(files.generation_of(&a), Some(files.generation()));
398 }
399
400 #[test]
401 fn a_splice_inside_a_character_is_refused() {
402 let mut files = Files::new();
403 files.open("a.txt", "ä\n");
404 let a = path("a.txt");
405 assert!(files.splice(&a, 1..2, "x").is_err());
406 assert!(files.splice(&a, 0..9, "x").is_err());
407 assert!(files.set_text(&path("b.txt"), "x").is_err());
408 assert_eq!(files.text(&a), Some("ä\n"));
409 }
410
411 #[test]
412 fn a_failed_attempt_restores_every_touched_file() {
413 let mut files = Files::new();
414 files.open("a.toml", "x = 1\n");
415 let a = path("a.toml");
416 let generation = files.generation();
417 let out: Result<(), DocError> = files.attempt(std::slice::from_ref(&a), |files| {
418 files.set_text(&a, "x = 2\n")?;
419 Err(DocError::new(&a, "", "refused"))
420 });
421 assert!(out.is_err());
422 assert_eq!(files.text(&a), Some("x = 1\n"));
423 assert_eq!(files.generation(), generation);
424 }
425
426 #[test]
427 fn an_edit_makes_a_file_dirty_until_it_reads_its_disk_again() {
428 let mut files = Files::new();
429 files.open("a.toml", "x = 1\n");
430 let a = path("a.toml");
431 assert!(!files.get(&a).unwrap().dirty());
432 files.set_text(&a, "x = 2\n").unwrap();
433 assert!(files.get(&a).unwrap().dirty());
434 files.set_text(&a, "x = 1\n").unwrap();
435 assert!(!files.get(&a).unwrap().dirty());
436 files.reload(&a, "x = 3\n").unwrap();
437 assert!(!files.get(&a).unwrap().dirty());
438 assert_eq!(files.seen(&a, Stamp::of("x = 3\n")), Some(Seen::Current));
439 }
440}