Skip to main content

pfx_editor_doc/
group.rs

1use std::path::PathBuf;
2
3use crate::edit::{Caret, Edit};
4use crate::error::DocError;
5use crate::files::Files;
6
7#[derive(Debug)]
8pub struct Group {
9    label: String,
10    edits: Vec<Box<dyn Edit>>,
11}
12
13impl Group {
14    pub fn new(label: impl Into<String>) -> Group {
15        Group {
16            label: label.into(),
17            edits: Vec::new(),
18        }
19    }
20
21    pub fn of(label: impl Into<String>, edits: Vec<Box<dyn Edit>>) -> Group {
22        Group {
23            label: label.into(),
24            edits,
25        }
26    }
27
28    pub fn with(mut self, edit: impl Edit) -> Group {
29        self.edits.push(Box::new(edit));
30        self
31    }
32
33    pub fn push(&mut self, edit: Box<dyn Edit>) {
34        self.edits.push(edit);
35    }
36
37    pub fn edits(&self) -> &[Box<dyn Edit>] {
38        &self.edits
39    }
40
41    pub fn len(&self) -> usize {
42        self.edits.len()
43    }
44
45    pub fn is_empty(&self) -> bool {
46        self.edits.is_empty()
47    }
48}
49
50impl Edit for Group {
51    fn label(&self) -> &str {
52        &self.label
53    }
54
55    fn files(&self) -> Vec<PathBuf> {
56        let mut files: Vec<PathBuf> = self.edits.iter().flat_map(|edit| edit.files()).collect();
57        files.sort();
58        files.dedup();
59        files
60    }
61
62    fn apply(&mut self, files: &mut Files) -> Result<(), DocError> {
63        let touched = self.files();
64        let edits = &mut self.edits;
65        files.attempt(&touched, |files| {
66            edits.iter_mut().try_for_each(|edit| edit.apply(files))
67        })
68    }
69
70    fn inverse(&self) -> Box<dyn Edit> {
71        Box::new(Group {
72            label: self.label.clone(),
73            edits: self.edits.iter().rev().map(|edit| edit.inverse()).collect(),
74        })
75    }
76
77    fn caret(&self) -> Option<Caret> {
78        self.edits.iter().rev().find_map(|edit| edit.caret())
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use std::path::Path;
85
86    use super::*;
87    use crate::patch::{TomlPatch, parse_path};
88    use crate::splice::TextSplice;
89    use toml_edit::value;
90
91    #[test]
92    fn a_group_applies_as_one_and_inverts_in_reverse() {
93        let mut files = Files::new();
94        files.open("a.toml", "x = 1\n");
95        files.open("b.txt", "hello\n");
96        let mut group = Group::new("drag")
97            .with(TomlPatch::set("a.toml", parse_path("x").unwrap(), value(2)))
98            .with(TextSplice::new("b.txt", 0..5, "hello", "bye"))
99            .with(TomlPatch::set("a.toml", parse_path("y").unwrap(), value(3)));
100        assert_eq!(group.files(), vec![PathBuf::from("a.toml"), "b.txt".into()]);
101        group.apply(&mut files).unwrap();
102        assert_eq!(files.text(Path::new("a.toml")), Some("x = 2\ny = 3\n"));
103        assert_eq!(files.text(Path::new("b.txt")), Some("bye\n"));
104        assert_eq!(group.caret().unwrap().selection, 3..3);
105        group.inverse().apply(&mut files).unwrap();
106        assert_eq!(files.text(Path::new("a.toml")), Some("x = 1\n"));
107        assert_eq!(files.text(Path::new("b.txt")), Some("hello\n"));
108    }
109
110    #[test]
111    fn a_refused_member_leaves_every_file_as_it_was() {
112        let mut files = Files::new();
113        files.open("a.toml", "x = 1\n");
114        files.open("b.txt", "hello\n");
115        let generation = files.generation();
116        let mut group = Group::new("paste")
117            .with(TomlPatch::set("a.toml", parse_path("x").unwrap(), value(2)))
118            .with(TextSplice::new("b.txt", 0..5, "other", "bye"));
119        assert!(group.apply(&mut files).is_err());
120        assert_eq!(files.text(Path::new("a.toml")), Some("x = 1\n"));
121        assert_eq!(files.text(Path::new("b.txt")), Some("hello\n"));
122        assert_eq!(files.generation(), generation);
123    }
124}