Skip to main content

indigo_core/
text.rs

1use crate::{
2    history::History,
3    syntax::{Language, Syntax},
4};
5use indigo_kernel::edit::{self, Edit};
6use ropey::Rope;
7use std::ops::{Deref, Range};
8use thiserror::Error;
9
10#[derive(Debug, Error)]
11pub enum Error {
12    #[error("Text is empty")]
13    Empty,
14
15    #[error("Text does not end with a newline")]
16    MissingTrailingNewline,
17}
18
19#[derive(Clone)]
20struct BidiEdit {
21    /// Inverted operations. Apply to undo the edit.
22    undo: Edit,
23    /// Original operations. Apply to perform the edit.
24    redo: Edit,
25}
26
27impl Extend<Self> for BidiEdit {
28    fn extend<T>(&mut self, opss: T)
29    where
30        T: IntoIterator<Item = Self>,
31    {
32        for ops in opss {
33            if self.redo.is_empty() {
34                *self = ops;
35            } else {
36                self.undo = ops.undo.compose(&self.undo).unwrap();
37                self.redo = self.redo.compose(&ops.redo).unwrap();
38            }
39        }
40    }
41}
42
43#[derive(Clone)]
44pub struct Text {
45    rope: Rope,
46    syntax: Option<(Syntax, usize)>,
47    history: History<BidiEdit, BidiEdit>,
48    log: Vec<Edit>,
49    pub readonly: bool,
50}
51
52impl Default for Text {
53    fn default() -> Self {
54        Self {
55            rope: Rope::from("\n"),
56            syntax: None,
57            history: History::default(),
58            log: Vec::new(),
59            readonly: false,
60        }
61    }
62}
63
64impl Text {
65    #[must_use]
66    pub fn new() -> Self {
67        Self::default()
68    }
69
70    #[must_use]
71    pub fn rope(&self) -> &Rope {
72        &self.rope
73    }
74
75    #[must_use]
76    pub fn language(&self) -> Option<Language> {
77        self.syntax_stale().map(|s| s.language())
78    }
79
80    #[must_use]
81    pub fn syntax(&mut self) -> Option<&Syntax> {
82        self.reparse();
83        self.syntax_stale()
84    }
85
86    #[must_use]
87    pub fn syntax_stale(&self) -> Option<&Syntax> {
88        self.syntax.as_ref().map(|(s, _)| s)
89    }
90
91    pub fn insert(&mut self, byte_offset: usize, text: &str) -> anyhow::Result<()> {
92        let mut ops = Edit::new();
93        ops.retain(byte_offset);
94        ops.insert(text);
95        ops.retain_rest(&self.rope)?;
96        self.apply(&ops)?;
97        Ok(())
98    }
99
100    pub fn delete(&mut self, range: Range<usize>) -> anyhow::Result<()> {
101        let mut ops = Edit::new();
102        ops.retain(range.start);
103        ops.delete(&self.rope.slice(range.start..range.end).to_string());
104        if range.end == self.rope.len()
105            && (range.start == 0 || self.rope.byte(range.start - 1) != b'\n')
106        {
107            ops.insert("\n");
108        }
109        ops.retain_rest(&self.rope)?;
110        self.apply(&ops)?;
111        Ok(())
112    }
113
114    pub fn apply(&mut self, ops: &Edit) -> anyhow::Result<()> {
115        anyhow::ensure!(!self.readonly, "Cannot modify readonly text");
116        let undo = ops.invert();
117        ops.apply(&mut self.rope)?;
118        self.history.push(BidiEdit {
119            redo: ops.clone(),
120            undo,
121        });
122        self.log.push(ops.clone());
123        self.assert_invariants().unwrap();
124        Ok(())
125    }
126
127    pub fn commit(&mut self) {
128        self.history.commit();
129    }
130
131    pub fn undo(&mut self) -> anyhow::Result<bool> {
132        anyhow::ensure!(!self.readonly, "Cannot modify readonly text");
133        if let Some(ops) = self.history.undo() {
134            ops.undo.apply(&mut self.rope)?;
135            self.log.push(ops.undo.clone());
136            self.assert_invariants().unwrap();
137            Ok(true)
138        } else {
139            Ok(false)
140        }
141    }
142
143    pub fn redo(&mut self) -> anyhow::Result<bool> {
144        anyhow::ensure!(!self.readonly, "Cannot modify readonly text");
145        if let Some(ops) = self.history.redo() {
146            ops.redo.apply(&mut self.rope)?;
147            self.log.push(ops.redo.clone());
148            self.assert_invariants().unwrap();
149            Ok(true)
150        } else {
151            Ok(false)
152        }
153    }
154
155    pub fn set_language(&mut self, language: Language) {
156        self.syntax = Some((Syntax::parse(language, &self.rope), self.version()));
157    }
158
159    pub fn reparse(&mut self) {
160        let version = self.version();
161        if let Some((syntax, parsed_at)) = &mut self.syntax
162            && *parsed_at < version
163        {
164            syntax.reparse(&self.rope);
165            *parsed_at = version;
166        }
167    }
168
169    #[must_use]
170    pub fn create_anchor(&self, byte_offset: usize) -> Anchor {
171        Anchor::new(self, byte_offset)
172    }
173
174    #[must_use]
175    pub fn resolve_anchor(&self, anchor: &Anchor) -> Option<usize> {
176        anchor.resolve(self)
177    }
178
179    #[must_use]
180    pub fn version(&self) -> usize {
181        self.log.len()
182    }
183
184    #[must_use]
185    pub fn ops_since(&self, version: usize) -> Option<&[Edit]> {
186        self.log.get(version..)
187    }
188
189    pub fn assert_invariants(&self) -> anyhow::Result<()> {
190        if self.rope.len() == 0 {
191            anyhow::bail!(Error::Empty);
192        }
193        if self.rope.byte(self.rope.len() - 1) != b'\n' {
194            anyhow::bail!(Error::MissingTrailingNewline);
195        }
196        Ok(())
197    }
198}
199
200impl Deref for Text {
201    type Target = Rope;
202
203    fn deref(&self) -> &Self::Target {
204        &self.rope
205    }
206}
207
208impl<'a> From<&'a str> for Text {
209    fn from(str: &'a str) -> Self {
210        Self::from(Rope::from(str))
211    }
212}
213
214impl From<Rope> for Text {
215    fn from(mut rope: Rope) -> Self {
216        if rope.len() == 0 || rope.byte(rope.len() - 1) != b'\n' {
217            rope.insert(rope.len(), "\n");
218        }
219        Self {
220            rope,
221            ..Self::default()
222        }
223    }
224}
225
226pub struct Anchor {
227    byte_offset: usize,
228    version: usize,
229}
230
231impl Anchor {
232    #[must_use]
233    pub fn new(text: &Text, byte_offset: usize) -> Self {
234        Self {
235            byte_offset,
236            version: text.version(),
237        }
238    }
239
240    #[must_use]
241    pub fn resolve(&self, text: &Text) -> Option<usize> {
242        let mut byte_offset = self.byte_offset;
243        for ops in text.ops_since(self.version)? {
244            byte_offset = ops.map_position(byte_offset, edit::Bias::Forward);
245        }
246        Some(byte_offset)
247    }
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253    use std::cmp::{max, min};
254
255    fn has_trailing_newline(text: &Text) -> bool {
256        let rope = text.rope();
257        rope.len() > 0 && rope.byte(rope.len() - 1) == b'\n'
258    }
259
260    #[test]
261    fn constructors_normalize() {
262        assert_eq!(Text::new().to_string(), "\n");
263        assert_eq!(Text::default().to_string(), "\n");
264        assert_eq!(Text::from("").to_string(), "\n");
265        assert_eq!(Text::from("x").to_string(), "x\n");
266        assert_eq!(Text::from("x\n").to_string(), "x\n");
267        assert_eq!(Text::from(Rope::from("x")).to_string(), "x\n");
268        // A bare final `'\r'` gains a `'\n'`, forming a single CRLF grapheme.
269        assert_eq!(Text::from("x\r").to_string(), "x\r\n");
270    }
271
272    #[test]
273    fn delete_to_end_keeps_trailing_newline() {
274        let mut text = Text::from("ab\n");
275        text.delete(1..3).unwrap();
276        assert_eq!(text.to_string(), "a\n");
277
278        // Delete everything.
279        let mut text = Text::from("ab\n");
280        text.delete(0..3).unwrap();
281        assert_eq!(text.to_string(), "\n");
282
283        // Deleting a final line whose predecessor is already terminated adds nothing.
284        let mut text = Text::from("a\nb\n");
285        text.delete(2..4).unwrap();
286        assert_eq!(text.to_string(), "a\n");
287    }
288
289    #[test]
290    fn undo_redo_keep_trailing_newline() {
291        let mut text = Text::from("ab\n");
292        text.delete(0..3).unwrap();
293        text.commit();
294        assert_eq!(text.to_string(), "\n");
295        assert!(text.undo().unwrap());
296        assert_eq!(text.to_string(), "ab\n");
297        assert!(has_trailing_newline(&text));
298        assert!(text.redo().unwrap());
299        assert_eq!(text.to_string(), "\n");
300        assert!(has_trailing_newline(&text));
301    }
302
303    #[hegel::test(test_cases = 1_000)]
304    fn fuzz(tc: hegel::TestCase) {
305        use crate::rope::RopeExt as _;
306        use hegel::generators as gs;
307        #[derive(Default)]
308        struct StateMachine {
309            text: Text,
310        }
311        #[hegel::state_machine]
312        #[expect(clippy::needless_pass_by_value)]
313        impl StateMachine {
314            #[rule]
315            fn insert(&mut self, tc: hegel::TestCase) {
316                // Inserting at the very end is only invariant-safe when the inserted text ends
317                // with a newline (e.g. `prepare_append`), so draw positions before the final
318                // grapheme.
319                let byte_offset = tc.draw(gs::integers::<usize>().max_value(self.text.len()));
320                let byte_offset = self
321                    .text
322                    .snap_to_grapheme_start(byte_offset)
323                    .expect("Text is never empty");
324                let string = tc.draw(gs::text());
325                self.text.insert(byte_offset, &string).unwrap();
326            }
327            #[rule]
328            fn delete(&mut self, tc: hegel::TestCase) {
329                let a = tc.draw(gs::integers::<usize>().max_value(self.text.len()));
330                let b = tc.draw(gs::integers::<usize>().max_value(self.text.len()));
331                let start = self.text.floor_grapheme_boundary(min(a, b));
332                let end = self.text.floor_grapheme_boundary(max(a, b));
333                self.text.delete(start..end).unwrap();
334            }
335            #[rule]
336            fn commit(&mut self, _: hegel::TestCase) {
337                self.text.commit();
338            }
339            #[rule]
340            fn undo(&mut self, _: hegel::TestCase) {
341                self.text.undo().unwrap();
342            }
343            #[rule]
344            fn redo(&mut self, _: hegel::TestCase) {
345                self.text.redo().unwrap();
346            }
347            #[invariant]
348            fn invariants(&self, _: hegel::TestCase) {
349                self.text.assert_invariants().unwrap();
350            }
351        }
352        hegel::stateful::machine(StateMachine::default()).run(tc);
353    }
354}