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}