Skip to main content

improv_tui/
edit.rs

1//! The edit line for a cell value or a formula: its text and a caret.
2
3use crossterm::event::KeyCode;
4
5/// Text being edited and the caret in it: a byte offset on a char
6/// boundary, `0..=text.len()`. Built from a `&str` with the caret at the
7/// end; derefs to the text.
8#[derive(Debug, Clone, Default, PartialEq)]
9pub struct LineEdit {
10    pub text: String,
11    pub caret: usize,
12}
13
14impl From<&str> for LineEdit {
15    fn from(s: &str) -> Self {
16        s.to_string().into()
17    }
18}
19
20impl From<String> for LineEdit {
21    fn from(text: String) -> Self {
22        LineEdit {
23            caret: text.len(),
24            text,
25        }
26    }
27}
28
29impl std::ops::Deref for LineEdit {
30    type Target = str;
31    fn deref(&self) -> &str {
32        &self.text
33    }
34}
35
36impl LineEdit {
37    /// Insert `s` at the caret and move the caret past it.
38    pub fn insert(&mut self, s: &str) {
39        self.text.insert_str(self.caret, s);
40        self.caret += s.len();
41    }
42
43    /// Replace `start..caret` with `s`, leaving the caret after it.
44    pub fn replace(&mut self, start: usize, s: &str) {
45        self.text.replace_range(start..self.caret, s);
46        self.caret = start + s.len();
47    }
48
49    fn prev(&self) -> usize {
50        self.text[..self.caret]
51            .chars()
52            .next_back()
53            .map_or(self.caret, |c| self.caret - c.len_utf8())
54    }
55
56    fn next(&self) -> usize {
57        self.text[self.caret..]
58            .chars()
59            .next()
60            .map_or(self.caret, |c| self.caret + c.len_utf8())
61    }
62
63    /// Apply a line-editing key; true if the text changed.
64    pub fn key(&mut self, code: KeyCode) -> bool {
65        let before = self.text.len();
66        match code {
67            KeyCode::Char(c) => self.insert(c.encode_utf8(&mut [0; 4])),
68            KeyCode::Backspace => {
69                let p = self.prev();
70                self.text.replace_range(p..self.caret, "");
71                self.caret = p;
72            }
73            KeyCode::Delete => {
74                let n = self.next();
75                self.text.replace_range(self.caret..n, "");
76            }
77            KeyCode::Left => self.caret = self.prev(),
78            KeyCode::Right => self.caret = self.next(),
79            KeyCode::Home => self.caret = 0,
80            KeyCode::End => self.caret = self.text.len(),
81            _ => {}
82        }
83        self.text.len() != before
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    #[test]
92    fn caret_edits_on_char_boundaries() {
93        let mut e = LineEdit::from("aé");
94        assert_eq!(e.caret, 3);
95        assert!(!e.key(KeyCode::Left));
96        assert_eq!(e.caret, 1, "stepped over the 2-byte é");
97        assert!(e.key(KeyCode::Char('x')));
98        assert_eq!(&*e, "axé");
99        assert!(!e.key(KeyCode::Home));
100        assert!(!e.key(KeyCode::Backspace), "nothing before the caret");
101        assert!(e.key(KeyCode::Delete));
102        assert_eq!(&*e, "xé");
103        assert!(!e.key(KeyCode::End));
104        assert!(!e.key(KeyCode::Delete), "nothing after the caret");
105        assert!(!e.key(KeyCode::Right));
106        assert!(e.key(KeyCode::Backspace));
107        assert_eq!((e.text.as_str(), e.caret), ("x", 1));
108        assert!(!e.key(KeyCode::F(5)));
109        e.caret = 0;
110        e.replace(0, "SUM(");
111        assert_eq!((e.text.as_str(), e.caret), ("SUM(x", 4));
112    }
113}