zee_edit/
lib.rs

1pub mod graphemes;
2pub mod movement;
3pub mod tree;
4
5mod diff;
6
7use ropey::{Rope, RopeSlice};
8use std::{cmp, ops::Range};
9
10pub use self::{
11    diff::{DeleteOperation, OpaqueDiff},
12    graphemes::{ByteIndex, CharIndex, LineIndex, RopeExt, RopeGraphemes},
13    movement::Direction,
14};
15
16trait RopeCursorExt {
17    fn cursor_to_line(&self, cursor: &Cursor) -> usize;
18
19    #[allow(dead_code)]
20    fn slice_cursor(&self, cursor: &Cursor) -> RopeSlice;
21}
22
23impl RopeCursorExt for Rope {
24    fn cursor_to_line(&self, cursor: &Cursor) -> usize {
25        self.char_to_line(cursor.range.start)
26    }
27
28    fn slice_cursor(&self, cursor: &Cursor) -> RopeSlice {
29        self.slice(cursor.range.start..cursor.range.end)
30    }
31}
32
33/// A `Cursor` represents a user cursor associated with a text buffer.
34///
35/// `Cursor`s consist of a location in a `Rope` and optionally a selection and
36/// desired visual offset.
37#[derive(Clone, Debug, PartialEq)]
38pub struct Cursor {
39    /// The cursor position represented as the index of the gap between two adjacent
40    /// characters inside a `Rope`.
41    ///
42    /// For a rope of length len, the valid range is 0..=length. The position is
43    /// aligned to extended grapheme clusters and will never index a gap inside
44    /// a grapheme.
45    range: Range<CharIndex>,
46    /// The start of a selection if in select mode, ending at `range.start` or
47    /// `range.end`, depending on direction. Aligned to extended grapheme
48    /// clusters.
49    selection: Option<CharIndex>,
50    visual_horizontal_offset: Option<usize>,
51}
52
53impl Default for Cursor {
54    fn default() -> Self {
55        Self::new()
56    }
57}
58
59impl Cursor {
60    pub fn new() -> Self {
61        Self {
62            range: 0..0,
63            selection: None,
64            visual_horizontal_offset: None,
65        }
66    }
67
68    pub fn with_range(range: Range<CharIndex>) -> Self {
69        Self {
70            range,
71            ..Self::new()
72        }
73    }
74
75    #[cfg(test)]
76    pub fn end_of_buffer(text: &Rope) -> Self {
77        Self {
78            range: text.prev_grapheme_boundary(text.len_chars())..text.len_chars(),
79            visual_horizontal_offset: None,
80            selection: None,
81        }
82    }
83
84    pub fn is_empty(&self) -> bool {
85        self.range.is_empty()
86    }
87
88    pub fn range(&self) -> Range<CharIndex> {
89        self.range.clone()
90    }
91
92    pub fn selection(&self) -> Range<CharIndex> {
93        match self.selection {
94            Some(selection) if selection > self.range.start => self.range.start..selection,
95            Some(selection) if selection < self.range.start => selection..self.range.start,
96            _ => self.range.clone(),
97        }
98    }
99
100    pub fn column_offset(&self, tab_width: usize, text: &Rope) -> usize {
101        let char_line_start = text.line_to_char(text.cursor_to_line(self));
102        graphemes::width(tab_width, &text.slice(char_line_start..self.range.start))
103    }
104
105    pub fn reconcile(&mut self, new_text: &Rope, diff: &OpaqueDiff) {
106        let OpaqueDiff {
107            char_index,
108            old_char_length,
109            new_char_length,
110            ..
111        } = *diff;
112
113        let modified_range = char_index..cmp::max(old_char_length, new_char_length);
114
115        // The edit starts after the end of the cursor, nothing to do
116        if modified_range.start >= self.range.end {
117            return;
118        }
119
120        // The edit ends before the start of the cursor
121        if modified_range.end <= self.range.start {
122            let (start, end) = (self.range.start, self.range.end);
123            if old_char_length > new_char_length {
124                let length_change = old_char_length - new_char_length;
125                self.range = start.saturating_sub(length_change)..end.saturating_sub(length_change);
126            } else {
127                let length_change = new_char_length - old_char_length;
128                self.range = start + length_change..end + length_change;
129            };
130        }
131
132        // Otherwise, the change overlaps with the cursor
133        let grapheme_start =
134            new_text.prev_grapheme_boundary(cmp::min(self.range.end, new_text.len_chars()));
135        let grapheme_end = new_text.next_grapheme_boundary(grapheme_start);
136        self.range = grapheme_start..grapheme_end
137    }
138
139    pub fn begin_selection(&mut self) {
140        self.selection = Some(self.range.start)
141    }
142
143    pub fn clear_selection(&mut self) {
144        self.selection = None;
145    }
146
147    pub fn select_all(&mut self, text: &Rope) {
148        movement::move_to_start_of_buffer(text, self);
149        self.selection = Some(text.len_chars());
150    }
151
152    // Editing
153
154    pub fn insert_char(&mut self, text: &mut Rope, character: char) -> OpaqueDiff {
155        self.clear_selection();
156        text.insert_char(self.range.start, character);
157        OpaqueDiff::new(
158            text.char_to_byte(self.range.start),
159            0,
160            character.len_utf8(),
161            self.range.start,
162            0,
163            1,
164        )
165    }
166
167    pub fn insert_chars(
168        &mut self,
169        text: &mut Rope,
170        characters: impl IntoIterator<Item = char>,
171    ) -> OpaqueDiff {
172        self.clear_selection();
173        let mut num_bytes = 0;
174        let mut num_chars = 0;
175        characters
176            .into_iter()
177            .enumerate()
178            .for_each(|(offset, character)| {
179                text.insert_char(self.range.start + offset, character);
180                num_bytes += character.len_utf8();
181                num_chars += 1;
182            });
183        OpaqueDiff::new(
184            text.char_to_byte(self.range.start),
185            0,
186            num_bytes,
187            self.range.start,
188            0,
189            num_chars,
190        )
191    }
192
193    pub fn delete_forward(&mut self, text: &mut Rope) -> DeleteOperation {
194        if text.len_chars() == 0 || text.len_chars() == self.range.start {
195            return DeleteOperation::empty();
196        }
197
198        let byte_range = text.char_to_byte(self.range.start)..text.char_to_byte(self.range.end);
199        let diff = OpaqueDiff::new(
200            byte_range.start,
201            byte_range.end - byte_range.start,
202            0,
203            self.range.start,
204            self.range.end - self.range.start,
205            0,
206        );
207        text.remove(self.range.clone());
208
209        let grapheme_start = self.range.start;
210        let grapheme_end = text.next_grapheme_boundary(self.range.start);
211        let deleted = text.slice(grapheme_start..grapheme_end).into();
212
213        *self = Cursor::with_range(grapheme_start..grapheme_end);
214
215        DeleteOperation { diff, deleted }
216    }
217
218    pub fn delete_backward(&mut self, text: &mut Rope) -> DeleteOperation {
219        if self.range.start > 0 {
220            movement::move_horizontally(text, self, Direction::Backward, 1);
221            self.delete_forward(text)
222        } else {
223            DeleteOperation::empty()
224        }
225    }
226
227    pub fn delete_line(&mut self, text: &mut Rope) -> DeleteOperation {
228        if text.len_chars() == 0 {
229            return DeleteOperation::empty();
230        }
231
232        // Delete line
233        let line_index = text.char_to_line(self.range.start);
234        let delete_range_start = text.line_to_char(line_index);
235        let delete_range_end = text.line_to_char(line_index + 1);
236        let deleted = text.slice(delete_range_start..delete_range_end).into();
237        let diff = OpaqueDiff::new(
238            text.char_to_byte(delete_range_start),
239            text.char_to_byte(delete_range_end) - text.char_to_byte(delete_range_start),
240            0,
241            delete_range_start,
242            delete_range_end - delete_range_start,
243            0,
244        );
245        text.remove(delete_range_start..delete_range_end);
246
247        // Update cursor position
248        let grapheme_start =
249            text.line_to_char(cmp::min(line_index, text.len_lines().saturating_sub(2)));
250        let grapheme_end = text.next_grapheme_boundary(grapheme_start);
251
252        *self = Cursor::with_range(grapheme_start..grapheme_end);
253
254        DeleteOperation { diff, deleted }
255    }
256
257    pub fn delete_selection(&mut self, text: &mut Rope) -> DeleteOperation {
258        if text.len_chars() == 0 {
259            return DeleteOperation::empty();
260        }
261
262        // Delete selection
263        let selection = self.selection();
264        let deleted = text.slice(selection.start..selection.end).into();
265        let diff = OpaqueDiff::new(
266            text.char_to_byte(selection.start),
267            text.char_to_byte(selection.end) - text.char_to_byte(selection.start),
268            0,
269            selection.start,
270            selection.end - selection.start,
271            0,
272        );
273        text.remove(selection.start..selection.end);
274
275        // Update cursor position
276        let grapheme_start = cmp::min(
277            self.range.start,
278            text.prev_grapheme_boundary(text.len_chars()),
279        );
280        let grapheme_end = text.next_grapheme_boundary(grapheme_start);
281
282        *self = Cursor::with_range(grapheme_start..grapheme_end);
283
284        DeleteOperation { diff, deleted }
285    }
286
287    pub fn sync(&mut self, current_text: &Rope, new_text: &Rope) {
288        let current_line = current_text.char_to_line(self.range.start);
289        let current_line_offset = self.range.start - current_text.line_to_char(current_line);
290
291        let new_line = cmp::min(current_line, new_text.len_lines().saturating_sub(1));
292        let new_line_offset = cmp::min(
293            current_line_offset,
294            new_text.line(new_line).len_chars().saturating_sub(1),
295        );
296        let grapheme_end =
297            new_text.next_grapheme_boundary(new_text.line_to_char(new_line) + new_line_offset);
298        let grapheme_start = new_text.prev_grapheme_boundary(grapheme_end);
299
300        *self = Cursor::with_range(grapheme_start..grapheme_end);
301    }
302}
303
304#[cfg(test)]
305mod tests {
306    use ropey::Rope;
307
308    use super::*;
309
310    fn text_with_cursor(text: impl Into<Rope>) -> (Rope, Cursor) {
311        let text = text.into();
312        let mut cursor = Cursor::new();
313        movement::move_horizontally(&text, &mut cursor, Direction::Backward, 1);
314        (text, cursor)
315    }
316
317    #[test]
318    fn sync_with_empty() {
319        let current_text = Rope::from("Buy a milk goat\nAt the market\n");
320        let new_text = Rope::from("");
321        let mut cursor = Cursor::new();
322        movement::move_horizontally(&current_text, &mut cursor, Direction::Forward, 4);
323        cursor.sync(&current_text, &new_text);
324        assert_eq!(Cursor::new(), cursor);
325    }
326
327    // Delete forward
328    #[test]
329    fn delete_forward_at_the_end() {
330        let (mut text, mut cursor) = text_with_cursor(TEXT);
331        let expected = text.clone();
332        movement::move_to_end_of_buffer(&text, &mut cursor);
333        cursor.delete_forward(&mut text);
334        assert_eq!(expected, text);
335    }
336
337    #[test]
338    fn delete_forward_empty_text() {
339        let (mut text, mut cursor) = text_with_cursor("");
340        cursor.delete_forward(&mut text);
341        assert_eq!(cursor, Cursor::new());
342    }
343
344    #[test]
345    fn delete_forward_at_the_begining() {
346        let (mut text, mut cursor) = text_with_cursor("// Hello world!\n\n");
347        let expected = Rope::from("Hello world!\n\n");
348        cursor.delete_forward(&mut text);
349        cursor.delete_forward(&mut text);
350        cursor.delete_forward(&mut text);
351        assert_eq!(expected, text);
352    }
353
354    // Delete backward
355    #[test]
356    fn delete_backward_at_the_end() {
357        let (mut text, mut cursor) = text_with_cursor("// Hello world!\n");
358        movement::move_to_end_of_buffer(&text, &mut cursor);
359        cursor.delete_backward(&mut text);
360        assert_eq!(Rope::from("// Hello world!"), text);
361        cursor.delete_backward(&mut text);
362        assert_eq!(Rope::from("// Hello world"), text);
363    }
364
365    #[test]
366    fn delete_backward_empty_text() {
367        let (mut text, mut cursor) = text_with_cursor("");
368        cursor.delete_backward(&mut text);
369        assert_eq!(cursor, Cursor::new());
370    }
371
372    #[test]
373    fn delete_backward_at_the_begining() {
374        let (mut text, mut cursor) = text_with_cursor("// Hello world!\n");
375        let expected = text.clone();
376        cursor.delete_backward(&mut text);
377        assert_eq!(expected, text);
378    }
379
380    const TEXT: &str = r#"
381Basic Latin
382    ! " # $ % & ' ( ) *+,-./012ABCDEFGHI` a m  t u v z { | } ~
383CJK
384    豈 更 車 Ⅷ
385"#;
386}