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}