Skip to main content

freya_edit/
rope_editor.rs

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