Skip to main content

freya_edit/
rope_editor.rs

1use std::{
2    borrow::Cow,
3    fmt::Display,
4    ops::Range,
5};
6
7use ropey::{
8    Rope,
9    iter::Lines,
10};
11
12use crate::{
13    TextSelection,
14    editor_history::{
15        EditorHistory,
16        HistoryChange,
17    },
18    text_editor::{
19        Line,
20        TextEditor,
21    },
22};
23
24/// Tracks the position and length of IME preedit text within the rope.
25#[derive(Clone, Debug)]
26pub struct PreeditState {
27    /// Start position in UTF-16 code units.
28    pub start: usize,
29    /// Length in UTF-16 code units.
30    pub len: usize,
31}
32
33/// TextEditor implementing a Rope
34pub struct RopeEditor {
35    pub(crate) rope: Rope,
36    pub(crate) selection: TextSelection,
37    pub(crate) indentation: u8,
38    pub(crate) history: EditorHistory,
39    pub(crate) preedit: Option<PreeditState>,
40}
41
42impl Display for RopeEditor {
43    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44        f.write_str(&self.rope.to_string())
45    }
46}
47
48impl RopeEditor {
49    // Create a new [`RopeEditor`]
50    pub fn new(
51        text: String,
52        selection: TextSelection,
53        indentation: u8,
54        history: EditorHistory,
55    ) -> Self {
56        Self {
57            rope: Rope::from_str(&text),
58            selection,
59            indentation,
60            history,
61            preedit: None,
62        }
63    }
64
65    pub fn rope(&self) -> &Rope {
66        &self.rope
67    }
68
69    /// Insert or replace IME preedit text at the current cursor position.
70    ///
71    /// The preedit text is inserted directly into the rope without recording
72    /// undo history. If there is already active preedit text, it is replaced.
73    /// An empty `text` clears the preedit.
74    pub fn set_preedit(&mut self, text: &str) {
75        // Remove existing preedit text from the rope if any
76        let preedit_start = if let Some(preedit) = self.preedit.take() {
77            let start_char = self.rope.utf16_cu_to_char(preedit.start);
78            let end_char = self.rope.utf16_cu_to_char(preedit.start + preedit.len);
79            self.rope.remove(start_char..end_char);
80            preedit.start
81        } else {
82            self.cursor_pos()
83        };
84
85        // Insert new preedit text at the start position
86        let start_char = self.rope.utf16_cu_to_char(preedit_start);
87        let len_before = self.rope.len_utf16_cu();
88        self.rope.insert(start_char, text);
89        let len_after = self.rope.len_utf16_cu();
90        let preedit_len = len_after - len_before;
91
92        self.preedit = Some(PreeditState {
93            start: preedit_start,
94            len: preedit_len,
95        });
96        self.selection = TextSelection::Cursor(preedit_start + preedit_len);
97    }
98
99    /// Remove active preedit text from the rope and restore the cursor.
100    pub fn clear_preedit(&mut self) {
101        if let Some(preedit) = self.preedit.take() {
102            let start_char = self.rope.utf16_cu_to_char(preedit.start);
103            let end_char = self.rope.utf16_cu_to_char(preedit.start + preedit.len);
104            self.rope.remove(start_char..end_char);
105            self.selection = TextSelection::Cursor(preedit.start);
106        }
107    }
108
109    /// Whether there is active IME preedit text in the rope.
110    pub fn has_preedit(&self) -> bool {
111        self.preedit.is_some()
112    }
113
114    /// Returns the rope content with preedit text excluded.
115    ///
116    /// This represents the "committed" text that should be synced
117    /// to external state.
118    pub fn committed_text(&self) -> String {
119        if let Some(preedit) = &self.preedit {
120            let start_char = self.rope.utf16_cu_to_char(preedit.start);
121            let end_char = self.rope.utf16_cu_to_char(preedit.start + preedit.len);
122            let before = self.rope.slice(..start_char);
123            let after = self.rope.slice(end_char..);
124            format!("{before}{after}")
125        } else {
126            self.rope.to_string()
127        }
128    }
129
130    /// Returns the rope text split into (before_preedit, preedit, after_preedit).
131    ///
132    /// If there is no active preedit, returns the full rope text as `before`
133    /// with empty preedit and after segments.
134    pub fn preedit_text_segments(&self) -> (String, String, String) {
135        if let Some(preedit) = &self.preedit {
136            let start_char = self.rope.utf16_cu_to_char(preedit.start);
137            let end_char = self.rope.utf16_cu_to_char(preedit.start + preedit.len);
138            let before = self.rope.slice(..start_char).to_string();
139            let preedit_text = self.rope.slice(start_char..end_char).to_string();
140            let after = self.rope.slice(end_char..).to_string();
141            (before, preedit_text, after)
142        } else {
143            (self.rope.to_string(), String::new(), String::new())
144        }
145    }
146}
147
148impl TextEditor for RopeEditor {
149    type LinesIterator<'a> = LinesIterator<'a>;
150
151    fn lines(&self) -> Self::LinesIterator<'_> {
152        let lines = self.rope.lines();
153        LinesIterator { lines }
154    }
155
156    fn insert_char(&mut self, ch: char, idx: usize) -> usize {
157        let idx_utf8 = self.utf16_cu_to_char(idx);
158        let selection = self.selection.clone();
159
160        let len_before_insert = self.rope.len_utf16_cu();
161        self.rope.insert_char(idx_utf8, ch);
162        let len_after_insert = self.rope.len_utf16_cu();
163
164        let inserted_text_len = len_after_insert - len_before_insert;
165
166        self.history.push_change(HistoryChange::InsertChar {
167            idx,
168            ch,
169            len: inserted_text_len,
170            selection,
171        });
172
173        inserted_text_len
174    }
175
176    fn insert(&mut self, text: &str, idx: usize) -> usize {
177        let idx_utf8 = self.utf16_cu_to_char(idx);
178        let selection = self.selection.clone();
179
180        let len_before_insert = self.rope.len_utf16_cu();
181        self.rope.insert(idx_utf8, text);
182        let len_after_insert = self.rope.len_utf16_cu();
183
184        let inserted_text_len = len_after_insert - len_before_insert;
185
186        self.history.push_change(HistoryChange::InsertText {
187            idx,
188            text: text.to_owned(),
189            len: inserted_text_len,
190            selection,
191        });
192
193        inserted_text_len
194    }
195
196    fn remove(&mut self, range_utf16: Range<usize>) -> usize {
197        let range =
198            self.utf16_cu_to_char(range_utf16.start)..self.utf16_cu_to_char(range_utf16.end);
199        let text = self.rope.slice(range.clone()).to_string();
200        let selection = self.selection.clone();
201
202        let len_before_remove = self.rope.len_utf16_cu();
203        self.rope.remove(range);
204        let len_after_remove = self.rope.len_utf16_cu();
205
206        let removed_text_len = len_before_remove - len_after_remove;
207
208        self.history.push_change(HistoryChange::Remove {
209            idx: range_utf16.end - removed_text_len,
210            text,
211            len: removed_text_len,
212            selection,
213        });
214
215        removed_text_len
216    }
217
218    fn char_to_line(&self, char_idx: usize) -> usize {
219        self.rope.char_to_line(char_idx)
220    }
221
222    fn line_to_char(&self, line_idx: usize) -> usize {
223        self.rope.line_to_char(line_idx)
224    }
225
226    fn utf16_cu_to_char(&self, utf16_cu_idx: usize) -> usize {
227        self.rope.utf16_cu_to_char(utf16_cu_idx)
228    }
229
230    fn char_to_utf16_cu(&self, idx: usize) -> usize {
231        self.rope.char_to_utf16_cu(idx)
232    }
233
234    fn text(&self) -> Cow<'_, str> {
235        self.rope.slice(..).into()
236    }
237
238    fn line(&self, line_idx: usize) -> Option<Line<'_>> {
239        let line = self.rope.get_line(line_idx);
240
241        line.map(|line| Line {
242            text: line.into(),
243            utf16_len: line.len_utf16_cu(),
244        })
245    }
246
247    fn len_lines(&self) -> usize {
248        self.rope.len_lines()
249    }
250
251    fn len_chars(&self) -> usize {
252        self.rope.len_chars()
253    }
254
255    fn len_utf16_cu(&self) -> usize {
256        self.rope.len_utf16_cu()
257    }
258
259    fn selection(&self) -> &TextSelection {
260        &self.selection
261    }
262
263    fn selection_mut(&mut self) -> &mut TextSelection {
264        &mut self.selection
265    }
266
267    fn has_any_selection(&self) -> bool {
268        self.selection.is_range()
269    }
270
271    fn get_selection(&self) -> Option<(usize, usize)> {
272        match self.selection {
273            TextSelection::Cursor(_) => None,
274            TextSelection::Range { from, to } => Some((from, to)),
275        }
276    }
277
278    fn set(&mut self, text: &str) {
279        self.rope.remove(0..);
280        self.rope.insert(0, text);
281        if self.cursor_pos() > text.len() {
282            self.move_cursor_to(text.len());
283        }
284    }
285
286    fn clear_selection(&mut self) {
287        let end = self.selection().end();
288        self.selection_mut().set_as_cursor();
289        self.selection_mut().move_to(end);
290    }
291
292    fn set_selection(&mut self, (from, to): (usize, usize)) {
293        self.selection = TextSelection::Range { from, to };
294    }
295
296    fn get_selected_text(&self) -> Option<String> {
297        let (start, end) = self.get_selection_range()?;
298
299        let start = self.utf16_cu_to_char(start);
300        let end = self.utf16_cu_to_char(end);
301
302        Some(self.rope().get_slice(start..end)?.to_string())
303    }
304
305    fn get_selection_range(&self) -> Option<(usize, usize)> {
306        let (start, end) = match self.selection {
307            TextSelection::Cursor(_) => return None,
308            TextSelection::Range { from, to } => (from, to),
309        };
310
311        // Use left-to-right selection
312        let (start, end) = if start < end {
313            (start, end)
314        } else {
315            (end, start)
316        };
317
318        Some((start, end))
319    }
320
321    fn undo(&mut self) -> Option<TextSelection> {
322        self.history.undo(&mut self.rope)
323    }
324
325    fn redo(&mut self) -> Option<TextSelection> {
326        self.history.redo(&mut self.rope)
327    }
328
329    fn editor_history(&self) -> &EditorHistory {
330        &self.history
331    }
332
333    fn editor_history_mut(&mut self) -> &mut EditorHistory {
334        &mut self.history
335    }
336
337    fn get_indentation(&self) -> u8 {
338        self.indentation
339    }
340}
341
342/// Iterator over text lines.
343pub struct LinesIterator<'a> {
344    pub lines: Lines<'a>,
345}
346
347impl<'a> Iterator for LinesIterator<'a> {
348    type Item = Line<'a>;
349
350    fn next(&mut self) -> Option<Self::Item> {
351        let line = self.lines.next();
352
353        line.map(|line| Line {
354            text: line.into(),
355            utf16_len: line.len_utf16_cu(),
356        })
357    }
358}
359
360#[cfg(test)]
361mod test {
362    use std::time::Duration;
363
364    use keyboard_types::{
365        Key,
366        Modifiers,
367        NamedKey,
368    };
369
370    use super::RopeEditor;
371    use crate::{
372        EditorHistory,
373        TextSelection,
374        text_editor::TextEditor,
375    };
376
377    fn editor(text: &str) -> RopeEditor {
378        RopeEditor::new(
379            text.to_string(),
380            TextSelection::new_cursor(0),
381            4,
382            EditorHistory::new(Duration::ZERO),
383        )
384    }
385
386    fn press(ed: &mut RopeEditor, key: NamedKey) {
387        ed.process_key(
388            &Key::Named(key),
389            &Modifiers::empty(),
390            None,
391            None,
392            true,
393            true,
394            false,
395            false,
396        );
397    }
398
399    #[test]
400    fn cursor_moves_over_whole_graphemes() {
401        let mut ed = editor("a🙂👨‍👩‍👧e\u{301}\nb");
402
403        let mut positions = Vec::new();
404        while ed.cursor_right() {
405            positions.push(ed.cursor_pos());
406        }
407        assert_eq!(positions, [1, 3, 11, 13, 14, 15]);
408
409        positions.clear();
410        while ed.cursor_left() {
411            positions.push(ed.cursor_pos());
412        }
413        assert_eq!(positions, [14, 13, 11, 3, 1, 0]);
414    }
415
416    #[test]
417    fn backspace_and_delete_remove_whole_graphemes() {
418        let mut ed = editor("🙂a🙂");
419
420        press(&mut ed, NamedKey::Delete);
421        assert_eq!(ed.rope().to_string(), "a🙂");
422        assert_eq!(ed.cursor_pos(), 0);
423
424        ed.move_cursor_to(3);
425        press(&mut ed, NamedKey::Backspace);
426        assert_eq!(ed.rope().to_string(), "a");
427        assert_eq!(ed.cursor_pos(), 1);
428    }
429
430    #[test]
431    fn preedit_lifecycle() {
432        let mut ed = editor("Hello World");
433        // Place cursor at position 5 ("Hello| World")
434        ed.move_cursor_to(5);
435
436        // Initially no preedit
437        assert!(!ed.has_preedit());
438        assert_eq!(ed.committed_text(), "Hello World");
439
440        // Set preedit: text is inserted into the rope, cursor moves after it
441        ed.set_preedit("你好");
442        assert!(ed.has_preedit());
443        assert_eq!(ed.rope().to_string(), "Hello你好 World");
444        assert_eq!(ed.committed_text(), "Hello World");
445        assert_eq!(ed.cursor_pos(), 5 + "你好".encode_utf16().count());
446
447        // Replace preedit with different text
448        ed.set_preedit("世界abc");
449        assert!(ed.has_preedit());
450        assert_eq!(ed.rope().to_string(), "Hello世界abc World");
451        assert_eq!(ed.committed_text(), "Hello World");
452        assert_eq!(ed.cursor_pos(), 5 + "世界abc".encode_utf16().count());
453
454        // Clear preedit (simulates Ime::Preedit("", None))
455        ed.clear_preedit();
456        assert!(!ed.has_preedit());
457        assert_eq!(ed.rope().to_string(), "Hello World");
458        assert_eq!(ed.committed_text(), "Hello World");
459        assert_eq!(ed.cursor_pos(), 5);
460    }
461
462    #[test]
463    fn preedit_skips_undo_history_and_clear_restores() {
464        let mut ed = editor("Hello");
465        ed.move_cursor_to(5);
466        assert!(!ed.history.can_undo());
467
468        // Insert preedit, should NOT create undo history
469        ed.set_preedit("XY");
470        assert!(!ed.history.can_undo());
471        assert_eq!(ed.rope().to_string(), "HelloXY");
472
473        // Replace preedit, still no undo history
474        ed.set_preedit("Z");
475        assert!(!ed.history.can_undo());
476        assert_eq!(ed.rope().to_string(), "HelloZ");
477
478        // clear_preedit restores rope and cursor
479        ed.clear_preedit();
480        assert!(!ed.has_preedit());
481        assert!(!ed.history.can_undo());
482        assert_eq!(ed.rope().to_string(), "Hello");
483        assert_eq!(ed.cursor_pos(), 5);
484
485        // Clearing again is a no-op
486        ed.clear_preedit();
487        assert_eq!(ed.rope().to_string(), "Hello");
488        assert_eq!(ed.cursor_pos(), 5);
489    }
490}