string_cmd/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use commands::Command;
4
5/// Command and editing mode enums
6pub mod commands;
7/// Crossterm event handling (requires the `crossterm` feature)
8pub mod events;
9
10/// A `StringEditor` instance wraps a `String` and provides a way to edit it using a variety of commands.
11/// It also keeps track of the cursor position and the editing mode (Emacs or Vi), which can be used for rendering.
12///
13/// # Example
14///
15/// ```rust
16/// use string_cmd::StringEditor;
17///
18/// let mut editor = StringEditor::new();
19///
20/// // Insert text
21/// editor.execute(Command::Insert('H'));
22/// editor.execute(Command::Insert('e'));
23/// editor.execute(Command::Insert('l'));
24/// editor.execute(Command::Insert('l'));
25/// editor.execute(Command::Insert('o'));
26///
27/// // Get the current text
28/// let text = editor.get_text();
29///
30/// // Get the cursor position
31/// let cursor = editor.cursor_pos();
32/// ```
33///
34/// In most cases, you'll want to use the `StringEditor` in conjunction with a terminal event loop.
35/// If you're using `crossterm` and want to use the default keybindings, check out the [`events`] module
36/// (requires the `crossterm` feature).
37#[derive(Debug, Clone, Default)]
38pub struct StringEditor {
39    text: String,
40    cursor: usize,
41    // editing_mode: EditingMode,
42}
43
44impl StringEditor {
45    /// Create a new `StringEditor` with an empty string.
46    pub fn new() -> Self {
47        Self {
48            text: String::new(),
49            cursor: 0,
50            // editing_mode: EditingMode::Emacs,
51        }
52    }
53
54    /// Create a new `StringEditor` with a given string. Sets the cursor position to just after the end of the string.
55    pub fn with_string(text: &str) -> Self {
56        Self {
57            text: text.to_string(),
58            cursor: text.len(),
59            // editing_mode: EditingMode::Emacs,
60        }
61    }
62
63    /// Get the current text of the editor.
64    pub fn get_text(&self) -> &str {
65        &self.text
66    }
67
68    /// Get the current cursor position.
69    pub fn cursor_pos(&self) -> usize {
70        self.cursor
71    }
72}
73
74impl StringEditor {
75    /// Execute a command on the editor.
76    pub fn execute(&mut self, command: Command) {
77        match command {
78            Command::Insert(c) => {
79                self.text.insert(self.cursor, c);
80                self.cursor += 1;
81            }
82            Command::Type(s) => {
83                self.text.insert_str(self.cursor, &s);
84                self.cursor += s.len();
85            }
86            Command::CursorLeft(amt) => self.cursor = self.cursor.saturating_sub(amt),
87            Command::CursorRight(amt) => {
88                self.cursor += amt;
89                if self.cursor > self.text.len() {
90                    self.cursor = self.text.len();
91                }
92            }
93            Command::CursorToStartOfLine => self.cursor = 0,
94            Command::CursorToEndOfLine => self.cursor = self.text.len(),
95            Command::Delete => {
96                if self.cursor < self.text.len() {
97                    self.text.remove(self.cursor);
98                }
99            }
100            Command::Backspace => {
101                if self.cursor > 0 {
102                    self.text.remove(self.cursor - 1);
103                    self.cursor -= 1;
104                }
105            }
106            Command::DeleteStartOfLineToCursor => {
107                self.text.replace_range(0..self.cursor, "");
108                self.cursor = 0;
109            }
110            Command::DeleteToEndOfLine => {
111                self.text.replace_range(self.cursor..self.text.len(), "");
112            }
113            Command::DeleteWordLeadingToCursor => {
114                if self.cursor > 0 {
115                    let mut pos = self.cursor - 1;
116
117                    while pos > 0
118                        && !self.text[pos - 1..pos]
119                            .chars()
120                            .next()
121                            .unwrap()
122                            .is_whitespace()
123                        && !matches!(
124                            self.text[pos - 1..pos].chars().next().unwrap(),
125                            '-' | '_'
126                                | '+'
127                                | '='
128                                | ','
129                                | '.'
130                                | '/'
131                                | '\\'
132                                | ':'
133                                | ';'
134                                | '!'
135                                | '?'
136                                | '@'
137                                | '#'
138                                | '$'
139                                | '%'
140                                | '^'
141                                | '&'
142                                | '*'
143                                | '('
144                                | ')'
145                                | '['
146                                | ']'
147                                | '{'
148                                | '}'
149                        )
150                    {
151                        pos -= 1;
152                    }
153
154                    while pos < self.cursor {
155                        self.text.remove(pos);
156                        self.cursor -= 1;
157                    }
158                }
159            }
160            _ => todo!(),
161        }
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    #[test]
170    fn basic_commands() {
171        let mut editor = StringEditor::new();
172
173        // Text insertion
174        editor.execute(Command::Insert('a'));
175        editor.execute(Command::Insert('b'));
176        assert_eq!(editor.get_text(), "ab");
177
178        // Left and right cursor movement
179        editor.execute(Command::CursorLeft(1));
180        editor.execute(Command::Insert('c'));
181        assert_eq!(editor.get_text(), "acb");
182        editor.execute(Command::CursorRight(1));
183        assert_eq!(editor.cursor_pos(), 3);
184        editor.execute(Command::CursorRight(1));
185        assert_eq!(editor.cursor_pos(), 3);
186        editor.execute(Command::Insert('d'));
187        assert_eq!(editor.get_text(), "acbd");
188
189        // Start of string
190        editor.execute(Command::CursorToStartOfLine);
191        editor.execute(Command::Insert('e'));
192        assert_eq!(editor.get_text(), "eacbd");
193
194        // End of string
195        editor.execute(Command::CursorToEndOfLine);
196        editor.execute(Command::Insert('f'));
197        assert_eq!(editor.get_text(), "eacbdf");
198
199        // Delete
200        editor.execute(Command::Delete);
201        assert_eq!(editor.get_text(), "eacbdf");
202        editor.execute(Command::Backspace);
203        assert_eq!(editor.get_text(), "eacbd");
204        editor.execute(Command::CursorLeft(1));
205        editor.execute(Command::Backspace);
206        assert_eq!(editor.get_text(), "eacd");
207        editor.execute(Command::Delete);
208        assert_eq!(editor.get_text(), "eac");
209    }
210
211    #[test]
212    fn test_larger_edits() {
213        let mut editor = StringEditor::with_string("Hello, world!");
214        editor.execute(Command::CursorLeft(6));
215        editor.execute(Command::DeleteStartOfLineToCursor);
216        assert_eq!(editor.get_text(), "world!");
217        editor.execute(Command::Type("Hello, ".to_string()));
218        assert_eq!(editor.get_text(), "Hello, world!");
219        editor.execute(Command::DeleteToEndOfLine);
220        assert_eq!(editor.get_text(), "Hello, ");
221        editor.execute(Command::Insert('w'));
222        assert_eq!(editor.get_text(), "Hello, w");
223    }
224}