Skip to main content

promkit_widgets/text_editor/
text_editor.rs

1use std::collections::HashSet;
2
3use promkit_core::grapheme::{StyledGrapheme, StyledGraphemes};
4
5use crate::cursor::Cursor;
6
7/// Edit mode.
8#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
9#[derive(Clone, Default)]
10pub enum Mode {
11    #[default]
12    /// Insert a char at the current position.
13    Insert,
14    /// Overwrite a char at the current position.
15    Overwrite,
16}
17
18/// A text editor that supports basic editing operations
19/// such as insert, delete, and overwrite.
20/// It utilizes a cursor to navigate and manipulate the text.
21#[derive(Clone)]
22pub struct TextEditor(Cursor<StyledGraphemes>);
23
24impl Default for TextEditor {
25    fn default() -> Self {
26        Self(Cursor::new(
27            // Set cursor
28            StyledGraphemes::from(" "),
29            0,
30            false,
31        ))
32    }
33}
34
35impl TextEditor {
36    pub fn new<S: AsRef<str>>(s: S) -> Self {
37        let mut buf = s.as_ref().to_owned();
38        buf.push(' ');
39        let pos = buf.len() - 1;
40        Self(Cursor::new(StyledGraphemes::from(buf), pos, false))
41    }
42
43    /// Returns the current text including the cursor.
44    pub fn text(&self) -> StyledGraphemes {
45        self.0.contents().clone()
46    }
47
48    /// Returns the text without the cursor.
49    pub fn text_without_cursor(&self) -> StyledGraphemes {
50        let mut ret = self.text();
51        ret.pop_back();
52        ret
53    }
54
55    /// Returns the current position of the cursor within the text.
56    pub fn position(&self) -> usize {
57        self.0.position()
58    }
59
60    /// Masks all characters except the cursor with the specified mask character.
61    pub fn masking(&self, mask: char) -> StyledGraphemes {
62        self.text()
63            .chars()
64            .into_iter()
65            .enumerate()
66            .map(|(i, c)| StyledGrapheme::from(if i == self.text().len() - 1 { c } else { mask }))
67            .collect::<StyledGraphemes>()
68    }
69
70    /// Replaces the current text with new text and positions the cursor at the end.
71    pub fn replace(&mut self, new: &str) {
72        let mut buf = new.to_owned();
73        buf.push(' ');
74        let pos = buf.len() - 1;
75        *self = Self(Cursor::new(StyledGraphemes::from(buf), pos, false));
76    }
77
78    /// Inserts a character at the current cursor position.
79    pub fn insert(&mut self, ch: char) {
80        let pos = self.position();
81        self.0.contents_mut().insert(pos, StyledGrapheme::from(ch));
82        self.forward();
83    }
84
85    pub fn insert_chars(&mut self, vch: &Vec<char>) {
86        for ch in vch {
87            self.insert(*ch);
88        }
89    }
90
91    /// Overwrites the character at the current cursor position with the specified character.
92    pub fn overwrite(&mut self, ch: char) {
93        if self.0.is_tail() {
94            self.insert(ch)
95        } else {
96            let pos = self.position();
97            self.0
98                .contents_mut()
99                .replace_range(pos..pos + 1, ch.to_string());
100            self.forward();
101        }
102    }
103
104    pub fn overwrite_chars(&mut self, vch: &Vec<char>) {
105        for ch in vch {
106            self.overwrite(*ch);
107        }
108    }
109
110    /// Erases the character before the cursor position.
111    pub fn erase(&mut self) {
112        if !self.0.is_head() {
113            self.backward();
114            let pos = self.position();
115            self.0.contents_mut().drain(pos..pos + 1);
116        }
117    }
118
119    /// Clears all text and resets the editor to its default state.
120    pub fn erase_all(&mut self) {
121        *self = Self::default();
122    }
123
124    /// Erases the text from the current cursor position to the specified position,
125    /// considering whether pos is greater or smaller than the current position.
126    fn erase_to_position(&mut self, pos: usize) {
127        let current_pos = self.position();
128        if pos > current_pos {
129            self.0.contents_mut().drain(current_pos..pos);
130        } else {
131            self.0.contents_mut().drain(pos..current_pos);
132            self.0.move_to(pos);
133        }
134    }
135
136    /// Finds the nearest previous index of any character in `word_break_chars` from the cursor position.
137    fn find_previous_nearest_index(&self, word_break_chars: &HashSet<char>) -> usize {
138        let current_position = self.position();
139        self.text()
140            .chars()
141            .iter()
142            .enumerate()
143            .filter(|&(i, _)| i < current_position.saturating_sub(1))
144            .rev()
145            .find(|&(_, c)| word_break_chars.contains(c))
146            .map(|(i, _)| i + 1)
147            .unwrap_or(0)
148    }
149
150    /// Erases the text from the current cursor position to the nearest previous character in `word_break_chars`.
151    pub fn erase_to_previous_nearest(&mut self, word_break_chars: &HashSet<char>) {
152        let pos = self.find_previous_nearest_index(word_break_chars);
153        self.erase_to_position(pos);
154    }
155
156    /// Moves the cursor to the nearest previous character in `word_break_chars`.
157    pub fn move_to_previous_nearest(&mut self, word_break_chars: &HashSet<char>) {
158        let pos = self.find_previous_nearest_index(word_break_chars);
159        self.0.move_to(pos);
160    }
161
162    /// Finds the nearest next index of any character in `word_break_chars` from the cursor position.
163    fn find_next_nearest_index(&self, word_break_chars: &HashSet<char>) -> usize {
164        let current_position = self.position();
165        self.text()
166            .chars()
167            .iter()
168            .enumerate()
169            .filter(|&(i, _)| i > current_position)
170            .find(|&(_, c)| word_break_chars.contains(c))
171            .map(|(i, _)| {
172                if i < self.0.contents().len() - 1 {
173                    i + 1
174                } else {
175                    self.0.contents().len() - 1
176                }
177            })
178            .unwrap_or(self.0.contents().len() - 1)
179    }
180
181    /// Erases the text from the current cursor position to the nearest next character in `word_break_chars`.
182    pub fn erase_to_next_nearest(&mut self, word_break_chars: &HashSet<char>) {
183        let pos = self.find_next_nearest_index(word_break_chars);
184        self.erase_to_position(pos);
185    }
186
187    /// Moves the cursor to the nearest next character in `word_break_chars`.
188    pub fn move_to_next_nearest(&mut self, word_break_chars: &HashSet<char>) {
189        let pos = self.find_next_nearest_index(word_break_chars);
190        self.0.move_to(pos);
191    }
192
193    /// Moves the cursor to the beginning of the text.
194    pub fn move_to_head(&mut self) {
195        self.0.move_to_head()
196    }
197
198    /// Moves the cursor to the end of the text.
199    pub fn move_to_tail(&mut self) {
200        self.0.move_to_tail()
201    }
202
203    pub fn shift(&mut self, backward: usize, forward: usize) -> bool {
204        self.0.shift(backward, forward)
205    }
206
207    /// Moves the cursor one position backward, if possible.
208    pub fn backward(&mut self) -> bool {
209        self.0.backward()
210    }
211
212    /// Moves the cursor one position forward, if possible.
213    pub fn forward(&mut self) -> bool {
214        self.0.forward()
215    }
216}
217
218#[cfg(test)]
219mod test {
220    use super::*;
221
222    fn new_with_position(s: String, p: usize) -> TextEditor {
223        TextEditor(Cursor::new(StyledGraphemes::from(s), p, false))
224    }
225
226    mod masking {
227        use super::*;
228
229        #[test]
230        fn test() {
231            let txt = new_with_position(String::from("abcde "), 0);
232            assert_eq!(StyledGraphemes::from("***** "), txt.masking('*'))
233        }
234    }
235
236    mod erase {
237        use super::*;
238
239        #[test]
240        fn test_for_empty() {
241            let txt = TextEditor::default();
242            assert_eq!(StyledGraphemes::from(" "), txt.text());
243            assert_eq!(0, txt.position());
244        }
245
246        #[test]
247        fn test_at_non_edge() {
248            let mut txt = new_with_position(
249                String::from("abc "),
250                1, // indicate `b`.
251            );
252            let new = new_with_position(
253                String::from("bc "),
254                0, // indicate `b`.
255            );
256            txt.erase();
257            assert_eq!(new.text(), txt.text());
258            assert_eq!(new.position(), txt.position());
259        }
260
261        #[test]
262        fn test_at_tail() {
263            let mut txt = new_with_position(
264                String::from("abc "),
265                3, // indicate tail.
266            );
267            let new = new_with_position(
268                String::from("ab "),
269                2, // indicate tail.
270            );
271            txt.erase();
272            assert_eq!(new.text(), txt.text());
273            assert_eq!(new.position(), txt.position());
274        }
275
276        #[test]
277        fn test_at_head() {
278            let txt = new_with_position(
279                String::from("abc "),
280                0, // indicate `a`.
281            );
282            assert_eq!(StyledGraphemes::from("abc "), txt.text());
283            assert_eq!(0, txt.position());
284        }
285    }
286
287    mod find_previous_nearest_index {
288        use super::*;
289
290        use std::collections::HashSet;
291
292        #[test]
293        fn test() {
294            let mut txt = new_with_position(String::from("koko momo jojo "), 11); // indicate `o`.
295            assert_eq!(10, txt.find_previous_nearest_index(&HashSet::from([' '])));
296            txt.0.move_to(10);
297            assert_eq!(5, txt.find_previous_nearest_index(&HashSet::from([' '])));
298        }
299
300        #[test]
301        fn test_with_no_target() {
302            let txt = new_with_position(String::from("koko momo jojo "), 7); // indicate `m`.
303            assert_eq!(0, txt.find_previous_nearest_index(&HashSet::from(['z'])));
304        }
305    }
306
307    mod find_next_nearest_index {
308        use super::*;
309
310        use std::collections::HashSet;
311
312        #[test]
313        fn test() {
314            let mut txt = new_with_position(String::from("koko momo jojo "), 7); // indicate `m`.
315            assert_eq!(10, txt.find_next_nearest_index(&HashSet::from([' '])));
316            txt.0.move_to(10);
317            assert_eq!(14, txt.find_next_nearest_index(&HashSet::from([' '])));
318        }
319
320        #[test]
321        fn test_with_no_target() {
322            let txt = new_with_position(String::from("koko momo jojo "), 7); // indicate `m`.
323            assert_eq!(14, txt.find_next_nearest_index(&HashSet::from(['z'])));
324        }
325    }
326
327    mod insert {
328        use super::*;
329
330        #[test]
331        fn test_for_empty() {
332            let mut txt = TextEditor::default();
333            let new = new_with_position(
334                String::from("d "),
335                1, // indicate tail.
336            );
337            txt.insert('d');
338            assert_eq!(new.text(), txt.text());
339            assert_eq!(new.position(), txt.position());
340        }
341
342        #[test]
343        fn test_at_non_edge() {
344            let mut txt = new_with_position(
345                String::from("abc "),
346                1, // indicate `b`.
347            );
348            let new = new_with_position(
349                String::from("adbc "),
350                2, // indicate `b`.
351            );
352            txt.insert('d');
353            assert_eq!(new.text(), txt.text());
354            assert_eq!(new.position(), txt.position());
355        }
356
357        #[test]
358        fn test_at_tail() {
359            let mut txt = new_with_position(
360                String::from("abc "),
361                3, // indicate tail.
362            );
363            let new = new_with_position(
364                String::from("abcd "),
365                4, // indicate tail.
366            );
367            txt.insert('d');
368            assert_eq!(new.text(), txt.text());
369            assert_eq!(new.position(), txt.position());
370        }
371
372        #[test]
373        fn test_at_head() {
374            let mut txt = new_with_position(
375                String::from("abc "),
376                0, // indicate `a`.
377            );
378            let new = new_with_position(
379                String::from("dabc "),
380                1, // indicate `a`.
381            );
382            txt.insert('d');
383            assert_eq!(new.text(), txt.text());
384            assert_eq!(new.position(), txt.position());
385        }
386    }
387
388    mod overwrite {
389        use super::*;
390
391        #[test]
392        fn test_for_empty() {
393            let mut txt = TextEditor::default();
394            let new = new_with_position(
395                String::from("d "),
396                1, // indicate tail.
397            );
398            txt.overwrite('d');
399            assert_eq!(new.text(), txt.text());
400            assert_eq!(new.position(), txt.position());
401        }
402
403        #[test]
404        fn test_at_non_edge() {
405            let mut txt = new_with_position(
406                String::from("abc "),
407                1, // indicate `b`.
408            );
409            let new = new_with_position(
410                String::from("adc "),
411                2, // indicate `c`.
412            );
413            txt.overwrite('d');
414            assert_eq!(new.text(), txt.text());
415            assert_eq!(new.position(), txt.position());
416        }
417
418        #[test]
419        fn test_at_tail() {
420            let mut txt = new_with_position(
421                String::from("abc "),
422                3, // indicate tail.
423            );
424            let new = new_with_position(
425                String::from("abcd "),
426                4, // indicate tail.
427            );
428            txt.overwrite('d');
429            assert_eq!(new.text(), txt.text());
430            assert_eq!(new.position(), txt.position());
431        }
432
433        #[test]
434        fn test_at_head() {
435            let mut txt = new_with_position(
436                String::from("abc "),
437                0, // indicate `a`.
438            );
439            let new = new_with_position(
440                String::from("dbc "),
441                1, // indicate `b`.
442            );
443            txt.overwrite('d');
444            assert_eq!(new.text(), txt.text());
445            assert_eq!(new.position(), txt.position());
446        }
447    }
448
449    mod backward {
450        use super::*;
451
452        #[test]
453        fn test_for_empty() {
454            let mut txt = TextEditor::default();
455            txt.backward();
456            assert_eq!(StyledGraphemes::from(" "), txt.text());
457            assert_eq!(0, txt.position());
458        }
459
460        #[test]
461        fn test_at_non_edge() {
462            let mut txt = new_with_position(
463                String::from("abc "),
464                1, // indicate `b`.
465            );
466            let new = new_with_position(
467                String::from("abc "),
468                0, // indicate `a`.
469            );
470            txt.backward();
471            assert_eq!(new.text(), txt.text());
472            assert_eq!(new.position(), txt.position());
473        }
474
475        #[test]
476        fn test_at_tail() {
477            let mut txt = new_with_position(
478                String::from("abc "),
479                3, // indicate tail.
480            );
481            let new = new_with_position(
482                String::from("abc "),
483                2, // indicate `c`.
484            );
485            txt.backward();
486            assert_eq!(new.text(), txt.text());
487            assert_eq!(new.position(), txt.position());
488        }
489
490        #[test]
491        fn test_at_head() {
492            let mut txt = new_with_position(
493                String::from("abc "),
494                0, // indicate `a`.
495            );
496            txt.backward();
497            assert_eq!(StyledGraphemes::from("abc "), txt.text());
498            assert_eq!(0, txt.position());
499        }
500    }
501
502    mod forward {
503        use super::*;
504
505        #[test]
506        fn test_for_empty() {
507            let mut txt = TextEditor::default();
508            txt.forward();
509            assert_eq!(StyledGraphemes::from(" "), txt.text());
510            assert_eq!(0, txt.position());
511        }
512
513        #[test]
514        fn test_at_non_edge() {
515            let mut txt = new_with_position(
516                String::from("abc "),
517                1, // indicate `b`.
518            );
519            let new = new_with_position(
520                String::from("abc "),
521                2, // indicate `c`.
522            );
523            txt.forward();
524            assert_eq!(new.text(), txt.text());
525            assert_eq!(new.position(), txt.position());
526        }
527
528        #[test]
529        fn test_at_tail() {
530            let mut txt = new_with_position(
531                String::from("abc "),
532                3, // indicate tail.
533            );
534            txt.forward();
535            assert_eq!(StyledGraphemes::from("abc "), txt.text());
536            assert_eq!(3, txt.position());
537        }
538
539        #[test]
540        fn test_at_head() {
541            let mut txt = new_with_position(
542                String::from("abc "),
543                0, // indicate `a`.
544            );
545            let new = new_with_position(
546                String::from("abc "),
547                1, // indicate `b`.
548            );
549            txt.forward();
550            assert_eq!(new.text(), txt.text());
551            assert_eq!(new.position(), txt.position());
552        }
553    }
554
555    mod to_head {
556        use super::*;
557
558        #[test]
559        fn test_for_empty() {
560            let mut txt = TextEditor::default();
561            txt.move_to_head();
562            assert_eq!(StyledGraphemes::from(" "), txt.text());
563            assert_eq!(0, txt.position());
564        }
565
566        #[test]
567        fn test_at_non_edge() {
568            let mut txt = new_with_position(
569                String::from("abc "),
570                1, // indicate `b`.
571            );
572            let new = new_with_position(
573                String::from("abc "),
574                0, // indicate `a`.
575            );
576            txt.move_to_head();
577            assert_eq!(new.text(), txt.text());
578            assert_eq!(new.position(), txt.position());
579        }
580
581        #[test]
582        fn test_at_tail() {
583            let mut txt = new_with_position(
584                String::from("abc "),
585                3, // indicate tail.
586            );
587            let new = new_with_position(
588                String::from("abc "),
589                0, // indicate `a`.
590            );
591            txt.move_to_head();
592            assert_eq!(new.text(), txt.text());
593            assert_eq!(new.position(), txt.position());
594        }
595
596        #[test]
597        fn test_at_head() {
598            let mut txt = new_with_position(
599                String::from("abc "),
600                0, // indicate `a`.
601            );
602            txt.move_to_head();
603            assert_eq!(StyledGraphemes::from("abc "), txt.text());
604            assert_eq!(0, txt.position());
605        }
606    }
607
608    mod to_tail {
609        use super::*;
610
611        #[test]
612        fn test_for_empty() {
613            let mut txt = TextEditor::default();
614            txt.move_to_tail();
615            assert_eq!(StyledGraphemes::from(" "), txt.text());
616            assert_eq!(0, txt.position());
617        }
618
619        #[test]
620        fn test_at_non_edge() {
621            let mut txt = new_with_position(
622                String::from("abc "),
623                1, // indicate `b`.
624            );
625            let new = new_with_position(
626                String::from("abc "),
627                3, // indicate tail.
628            );
629            txt.move_to_tail();
630            assert_eq!(new.text(), txt.text());
631            assert_eq!(new.position(), txt.position());
632        }
633
634        #[test]
635        fn test_at_tail() {
636            let mut txt = new_with_position(
637                String::from("abc "),
638                3, // indicate tail.
639            );
640            txt.move_to_tail();
641            assert_eq!(StyledGraphemes::from("abc "), txt.text());
642            assert_eq!(3, txt.position());
643        }
644
645        #[test]
646        fn test_at_head() {
647            let mut txt = new_with_position(
648                String::from("abc "),
649                0, // indicate `a`.
650            );
651            let new = new_with_position(
652                String::from("abc "),
653                3, // indicate tail.
654            );
655            txt.move_to_tail();
656            assert_eq!(new.text(), txt.text());
657            assert_eq!(new.position(), txt.position());
658        }
659    }
660}