Skip to main content

pfx_editor_doc/
files.rs

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}