Skip to main content

pfx_editor_doc/
splice.rs

1use std::ops::Range;
2use std::path::{Path, PathBuf};
3
4use crate::edit::{Caret, Edit, downcast};
5use crate::error::DocError;
6use crate::files::{Files, check_range};
7
8pub const TYPING: &str = "typing";
9
10#[derive(Clone, Debug, PartialEq, Eq)]
11pub struct TextSplice {
12    pub label: String,
13    pub file: PathBuf,
14    pub range: Range<usize>,
15    pub old: String,
16    pub new: String,
17    pub caret_before: Range<usize>,
18    pub caret_after: Range<usize>,
19}
20
21pub(crate) fn diff(before: &str, after: &str) -> (usize, usize, usize) {
22    let (a, b) = (before.as_bytes(), after.as_bytes());
23    let mut prefix = a.iter().zip(b).take_while(|(x, y)| x == y).count();
24    while !before.is_char_boundary(prefix) || !after.is_char_boundary(prefix) {
25        prefix -= 1;
26    }
27    let room = a.len().min(b.len()) - prefix;
28    let mut suffix = a
29        .iter()
30        .rev()
31        .zip(b.iter().rev())
32        .take(room)
33        .take_while(|(x, y)| x == y)
34        .count();
35    while !before.is_char_boundary(a.len() - suffix) || !after.is_char_boundary(b.len() - suffix) {
36        suffix -= 1;
37    }
38    (prefix, a.len() - suffix, b.len() - suffix)
39}
40
41impl TextSplice {
42    pub fn new(
43        file: impl Into<PathBuf>,
44        range: Range<usize>,
45        old: impl Into<String>,
46        new: impl Into<String>,
47    ) -> TextSplice {
48        let new = new.into();
49        let end = range.start + new.len();
50        TextSplice {
51            label: TYPING.to_string(),
52            file: file.into(),
53            caret_before: range.clone(),
54            range,
55            old: old.into(),
56            new,
57            caret_after: end..end,
58        }
59    }
60
61    pub fn between(file: impl Into<PathBuf>, before: &str, after: &str) -> Option<TextSplice> {
62        if before == after {
63            return None;
64        }
65        let (start, old_end, new_end) = diff(before, after);
66        Some(TextSplice::new(
67            file,
68            start..old_end,
69            &before[start..old_end],
70            &after[start..new_end],
71        ))
72    }
73
74    pub fn labelled(mut self, label: impl Into<String>) -> TextSplice {
75        self.label = label.into();
76        self
77    }
78
79    pub fn with_carets(mut self, before: Range<usize>, after: Range<usize>) -> TextSplice {
80        self.caret_before = before;
81        self.caret_after = after;
82        self
83    }
84
85    pub fn file(&self) -> &Path {
86        &self.file
87    }
88
89    pub fn end(&self) -> usize {
90        self.range.start + self.new.len()
91    }
92
93    fn then(&self, next: &TextSplice) -> Option<TextSplice> {
94        if next.file != self.file {
95            return None;
96        }
97        let (s, e) = (self.range.start, self.end());
98        let (a, b) = (next.range.start, next.range.end);
99        if a > e || b < s {
100            return None;
101        }
102        let low = s.min(a);
103        let mut old = String::new();
104        if a < s {
105            old.push_str(next.old.get(..s - a)?);
106        }
107        old.push_str(&self.old);
108        if b > e {
109            old.push_str(next.old.get(e - a..)?);
110        }
111        let mut new = String::new();
112        if s < a {
113            new.push_str(self.new.get(..a - s)?);
114        }
115        new.push_str(&next.new);
116        if b < e {
117            new.push_str(self.new.get(b - s..)?);
118        }
119        Some(TextSplice {
120            label: self.label.clone(),
121            file: self.file.clone(),
122            range: low..low + old.len(),
123            old,
124            new,
125            caret_before: self.caret_before.clone(),
126            caret_after: next.caret_after.clone(),
127        })
128    }
129}
130
131impl Edit for TextSplice {
132    fn label(&self) -> &str {
133        &self.label
134    }
135
136    fn files(&self) -> Vec<PathBuf> {
137        vec![self.file.clone()]
138    }
139
140    fn apply(&mut self, files: &mut Files) -> Result<(), DocError> {
141        let text = files
142            .text(&self.file)
143            .ok_or_else(|| DocError::new(&self.file, "", "the file is not open"))?;
144        check_range(&self.file, text, &self.range)?;
145        if text[self.range.clone()] != self.old {
146            return Err(DocError::new(
147                &self.file,
148                "",
149                format!(
150                    "bytes {}..{} no longer hold the text this edit replaces",
151                    self.range.start, self.range.end
152                ),
153            ));
154        }
155        files.splice(&self.file, self.range.clone(), &self.new)?;
156        Ok(())
157    }
158
159    fn inverse(&self) -> Box<dyn Edit> {
160        Box::new(TextSplice {
161            label: self.label.clone(),
162            file: self.file.clone(),
163            range: self.range.start..self.end(),
164            old: self.new.clone(),
165            new: self.old.clone(),
166            caret_before: self.caret_after.clone(),
167            caret_after: self.caret_before.clone(),
168        })
169    }
170
171    fn merge(&self, next: &dyn Edit) -> Option<Box<dyn Edit>> {
172        let next = downcast::<TextSplice>(next)?;
173        self.then(next)
174            .map(|merged| Box::new(merged) as Box<dyn Edit>)
175    }
176
177    fn caret(&self) -> Option<Caret> {
178        Some(Caret {
179            file: self.file.clone(),
180            selection: self.caret_after.clone(),
181        })
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    const FILE: &str = "a.toml";
190
191    fn splice(range: Range<usize>, old: &str, new: &str) -> TextSplice {
192        TextSplice::new(FILE, range, old, new)
193    }
194
195    fn run(text: &str, splices: &[TextSplice]) -> String {
196        let mut files = Files::new();
197        files.open(FILE, text);
198        for s in splices {
199            s.clone().apply(&mut files).unwrap();
200        }
201        files.text(Path::new(FILE)).unwrap().to_string()
202    }
203
204    #[test]
205    fn between_finds_the_smallest_change() {
206        let s = TextSplice::between(FILE, "x = 1.0\n", "x = 12.0\n").unwrap();
207        assert_eq!(
208            (s.range.clone(), s.old.as_str(), s.new.as_str()),
209            (5..5, "", "2")
210        );
211        let s = TextSplice::between(FILE, "aaa", "aa").unwrap();
212        assert_eq!(
213            (s.range.clone(), s.old.as_str(), s.new.as_str()),
214            (2..3, "a", "")
215        );
216        let s = TextSplice::between(FILE, "ä", "ö").unwrap();
217        assert_eq!((s.old.as_str(), s.new.as_str()), ("ä", "ö"));
218        assert!(TextSplice::between(FILE, "same", "same").is_none());
219    }
220
221    #[test]
222    fn a_splice_and_its_inverse_restore_the_text() {
223        let text = "name = \"ä\"\nat = [1.0]\n";
224        let mut s = splice(8..10, "ä", "öö");
225        let mut files = Files::new();
226        files.open(FILE, text);
227        s.apply(&mut files).unwrap();
228        assert_eq!(
229            files.text(Path::new(FILE)),
230            Some("name = \"öö\"\nat = [1.0]\n")
231        );
232        s.inverse().apply(&mut files).unwrap();
233        assert_eq!(files.text(Path::new(FILE)), Some(text));
234    }
235
236    #[test]
237    fn a_splice_over_other_text_is_refused() {
238        let mut files = Files::new();
239        files.open(FILE, "x = 1\n");
240        assert!(splice(4..5, "2", "3").apply(&mut files).is_err());
241        assert!(splice(4..9, "1", "3").apply(&mut files).is_err());
242        assert_eq!(files.text(Path::new(FILE)), Some("x = 1\n"));
243    }
244
245    #[test]
246    fn composed_splices_equal_the_pair() {
247        let text = "abcdef";
248        let first = splice(2..3, "c", "XYZ");
249        let cases = [
250            splice(5..5, "", "!"),
251            splice(3..4, "Y", ""),
252            splice(1..3, "bX", "_"),
253            splice(4..7, "Zde", "-"),
254            splice(0..8, "abXYZdef", ""),
255            splice(2..2, "", "<"),
256        ];
257        for next in cases {
258            let merged = first.then(&next).unwrap();
259            assert_eq!(run(text, &[merged]), run(text, &[first.clone(), next]));
260        }
261        assert!(first.then(&splice(6..6, "", "?")).is_none());
262        assert!(first.then(&splice(0..1, "a", "")).is_none());
263    }
264
265    #[test]
266    fn typing_merges_into_one_splice_with_the_first_caret() {
267        let a = splice(0..0, "", "a").with_carets(0..0, 1..1);
268        let b = splice(1..1, "", "b").with_carets(1..1, 2..2);
269        let merged = a.merge(&b).unwrap();
270        let merged = downcast::<TextSplice>(merged.as_ref()).unwrap();
271        assert_eq!(merged.new, "ab");
272        assert_eq!(merged.caret_before, 0..0);
273        assert_eq!(merged.caret_after, 2..2);
274        assert_eq!(merged.inverse().caret().unwrap().selection, 0..0);
275    }
276}