freya_edit/
rope_editor.rs

1use std::{
2    cmp::Ordering,
3    fmt::Display,
4    ops::Range,
5};
6
7use ropey::{
8    Rope,
9    iter::Lines,
10};
11use unicode_segmentation::UnicodeSegmentation;
12
13use crate::{
14    EditorLine,
15    TextSelection,
16    editor_history::{
17        EditorHistory,
18        HistoryChange,
19    },
20    text_editor::{
21        Line,
22        TextEditor,
23    },
24};
25
26/// TextEditor implementing a Rope
27pub struct RopeEditor {
28    pub(crate) rope: Rope,
29    pub(crate) selection: TextSelection,
30    pub(crate) identation: u8,
31    pub(crate) history: EditorHistory,
32}
33
34impl Display for RopeEditor {
35    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36        f.write_str(&self.rope.to_string())
37    }
38}
39
40impl RopeEditor {
41    // Create a new [`RopeEditor`]
42    pub fn new(
43        text: String,
44        selection: TextSelection,
45        identation: u8,
46        history: EditorHistory,
47    ) -> Self {
48        Self {
49            rope: Rope::from_str(&text),
50            selection,
51            identation,
52            history,
53        }
54    }
55
56    pub fn rope(&self) -> &Rope {
57        &self.rope
58    }
59}
60
61impl TextEditor for RopeEditor {
62    type LinesIterator<'a> = LinesIterator<'a>;
63
64    fn lines(&self) -> Self::LinesIterator<'_> {
65        let lines = self.rope.lines();
66        LinesIterator { lines }
67    }
68
69    fn insert_char(&mut self, ch: char, idx: usize) -> usize {
70        let idx_utf8 = self.utf16_cu_to_char(idx);
71
72        let len_before_insert = self.rope.len_utf16_cu();
73        self.rope.insert_char(idx_utf8, ch);
74        let len_after_insert = self.rope.len_utf16_cu();
75
76        let inserted_text_len = len_after_insert - len_before_insert;
77
78        self.history.push_change(HistoryChange::InsertChar {
79            idx,
80            ch,
81            len: inserted_text_len,
82        });
83
84        inserted_text_len
85    }
86
87    fn insert(&mut self, text: &str, idx: usize) -> usize {
88        let idx_utf8 = self.utf16_cu_to_char(idx);
89
90        let len_before_insert = self.rope.len_utf16_cu();
91        self.rope.insert(idx_utf8, text);
92        let len_after_insert = self.rope.len_utf16_cu();
93
94        let inserted_text_len = len_after_insert - len_before_insert;
95
96        self.history.push_change(HistoryChange::InsertText {
97            idx,
98            text: text.to_owned(),
99            len: inserted_text_len,
100        });
101
102        inserted_text_len
103    }
104
105    fn remove(&mut self, range_utf16: Range<usize>) -> usize {
106        let range =
107            self.utf16_cu_to_char(range_utf16.start)..self.utf16_cu_to_char(range_utf16.end);
108        let text = self.rope.slice(range.clone()).to_string();
109
110        let len_before_remove = self.rope.len_utf16_cu();
111        self.rope.remove(range);
112        let len_after_remove = self.rope.len_utf16_cu();
113
114        let removed_text_len = len_before_remove - len_after_remove;
115
116        self.history.push_change(HistoryChange::Remove {
117            idx: range_utf16.end - removed_text_len,
118            text,
119            len: removed_text_len,
120        });
121
122        removed_text_len
123    }
124
125    fn char_to_line(&self, char_idx: usize) -> usize {
126        self.rope.char_to_line(char_idx)
127    }
128
129    fn line_to_char(&self, line_idx: usize) -> usize {
130        self.rope.line_to_char(line_idx)
131    }
132
133    fn utf16_cu_to_char(&self, utf16_cu_idx: usize) -> usize {
134        self.rope.utf16_cu_to_char(utf16_cu_idx)
135    }
136
137    fn char_to_utf16_cu(&self, idx: usize) -> usize {
138        self.rope.char_to_utf16_cu(idx)
139    }
140
141    fn line(&self, line_idx: usize) -> Option<Line<'_>> {
142        let line = self.rope.get_line(line_idx);
143
144        line.map(|line| Line {
145            text: line.into(),
146            utf16_len: line.len_utf16_cu(),
147        })
148    }
149
150    fn len_lines(&self) -> usize {
151        self.rope.len_lines()
152    }
153
154    fn len_chars(&self) -> usize {
155        self.rope.len_chars()
156    }
157
158    fn len_utf16_cu(&self) -> usize {
159        self.rope.len_utf16_cu()
160    }
161
162    fn selection(&self) -> &TextSelection {
163        &self.selection
164    }
165
166    fn selection_mut(&mut self) -> &mut TextSelection {
167        &mut self.selection
168    }
169
170    fn has_any_selection(&self) -> bool {
171        self.selection.is_range()
172    }
173
174    fn get_selection(&self) -> Option<(usize, usize)> {
175        match self.selection {
176            TextSelection::Cursor(_) => None,
177            TextSelection::Range { from, to } => Some((from, to)),
178        }
179    }
180
181    fn get_visible_selection(&self, editor_line: EditorLine) -> Option<(usize, usize)> {
182        let (selected_from, selected_to) = match self.selection {
183            TextSelection::Cursor(_) => return None,
184            TextSelection::Range { from, to } => (from, to),
185        };
186
187        match editor_line {
188            EditorLine::Paragraph(line_index) => {
189                let selected_from_row = self.char_to_line(self.utf16_cu_to_char(selected_from));
190                let selected_to_row = self.char_to_line(self.utf16_cu_to_char(selected_to));
191
192                let editor_row_idx = self.char_to_utf16_cu(self.line_to_char(line_index));
193                let selected_from_row_idx =
194                    self.char_to_utf16_cu(self.line_to_char(selected_from_row));
195                let selected_to_row_idx = self.char_to_utf16_cu(self.line_to_char(selected_to_row));
196
197                let selected_from_col_idx = selected_from - selected_from_row_idx;
198                let selected_to_col_idx = selected_to - selected_to_row_idx;
199
200                // Between starting line and endling line
201                if (line_index > selected_from_row && line_index < selected_to_row)
202                    || (line_index < selected_from_row && line_index > selected_to_row)
203                {
204                    let len = self.line(line_index).unwrap().utf16_len();
205                    return Some((0, len));
206                }
207
208                match selected_from_row.cmp(&selected_to_row) {
209                    // Selection direction is from bottom -> top
210                    Ordering::Greater => {
211                        if selected_from_row == line_index {
212                            // Starting line
213                            Some((0, selected_from_col_idx))
214                        } else if selected_to_row == line_index {
215                            // Ending line
216                            let len = self.line(selected_to_row).unwrap().utf16_len();
217                            Some((selected_to_col_idx, len))
218                        } else {
219                            None
220                        }
221                    }
222                    // Selection direction is from top -> bottom
223                    Ordering::Less => {
224                        if selected_from_row == line_index {
225                            // Starting line
226                            let len = self.line(selected_from_row).unwrap().utf16_len();
227                            Some((selected_from_col_idx, len))
228                        } else if selected_to_row == line_index {
229                            // Ending line
230                            Some((0, selected_to_col_idx))
231                        } else {
232                            None
233                        }
234                    }
235                    Ordering::Equal if selected_from_row == line_index => {
236                        // Starting and endline line are the same
237                        Some((selected_from - editor_row_idx, selected_to - editor_row_idx))
238                    }
239                    _ => None,
240                }
241            }
242            EditorLine::SingleParagraph => Some((selected_from, selected_to)),
243        }
244    }
245
246    fn set(&mut self, text: &str) {
247        self.rope.remove(0..);
248        self.rope.insert(0, text);
249        if self.cursor_pos() > text.len() {
250            self.move_cursor_to(text.len());
251        }
252    }
253
254    fn clear_selection(&mut self) {
255        let end = self.selection().end();
256        self.selection_mut().set_as_cursor();
257        self.selection_mut().move_to(end);
258    }
259
260    fn measure_selection(&self, to: usize, line_index: EditorLine) -> TextSelection {
261        let mut selection = self.selection().clone();
262
263        match line_index {
264            EditorLine::Paragraph(line_index) => {
265                let row_char = self.line_to_char(line_index);
266                let pos = self.char_to_utf16_cu(row_char) + to;
267                selection.move_to(pos);
268            }
269            EditorLine::SingleParagraph => {
270                selection.move_to(to);
271            }
272        }
273
274        selection
275    }
276
277    fn set_selection(&mut self, (from, to): (usize, usize)) {
278        self.selection = TextSelection::Range { from, to };
279    }
280
281    fn get_selected_text(&self) -> Option<String> {
282        let (start, end) = self.get_selection_range()?;
283
284        let start = self.utf16_cu_to_char(start);
285        let end = self.utf16_cu_to_char(end);
286
287        Some(self.rope().get_slice(start..end)?.to_string())
288    }
289
290    fn get_selection_range(&self) -> Option<(usize, usize)> {
291        let (start, end) = match self.selection {
292            TextSelection::Cursor(_) => return None,
293            TextSelection::Range { from, to } => (from, to),
294        };
295
296        // Use left-to-right selection
297        let (start, end) = if start < end {
298            (start, end)
299        } else {
300            (end, start)
301        };
302
303        Some((start, end))
304    }
305
306    fn undo(&mut self) -> Option<usize> {
307        self.history.undo(&mut self.rope)
308    }
309
310    fn redo(&mut self) -> Option<usize> {
311        self.history.redo(&mut self.rope)
312    }
313
314    fn editor_history(&mut self) -> &mut EditorHistory {
315        &mut self.history
316    }
317
318    fn get_identation(&self) -> u8 {
319        self.identation
320    }
321
322    fn find_word_boundaries(&self, pos: usize) -> (usize, usize) {
323        let pos_char = self.utf16_cu_to_char(pos);
324        let len_chars = self.rope.len_chars();
325
326        if len_chars == 0 {
327            return (pos, pos);
328        }
329
330        // Get the line containing the cursor
331        let line_idx = self.rope.char_to_line(pos_char);
332        let line_char = self.rope.line_to_char(line_idx);
333        let line = self.rope.line(line_idx);
334
335        let line_str: std::borrow::Cow<str> = line.into();
336        let pos_in_line = pos_char - line_char;
337
338        // Find word boundaries within the line
339        let mut char_offset = 0;
340        for word in line_str.split_word_bounds() {
341            let word_char_len = word.chars().count();
342            let word_start = char_offset;
343            let word_end = char_offset + word_char_len;
344
345            if pos_in_line >= word_start && pos_in_line < word_end {
346                let start_char = line_char + word_start;
347                let end_char = line_char + word_end;
348                return (
349                    self.char_to_utf16_cu(start_char),
350                    self.char_to_utf16_cu(end_char),
351                );
352            }
353
354            char_offset = word_end;
355        }
356
357        (pos, pos)
358    }
359}
360
361/// Iterator over text lines.
362pub struct LinesIterator<'a> {
363    pub lines: Lines<'a>,
364}
365
366impl<'a> Iterator for LinesIterator<'a> {
367    type Item = Line<'a>;
368
369    fn next(&mut self) -> Option<Self::Item> {
370        let line = self.lines.next();
371
372        line.map(|line| Line {
373            text: line.into(),
374            utf16_len: line.len_utf16_cu(),
375        })
376    }
377}