Skip to main content

vtcode_vim/
lib.rs

1//! Vim-style prompt editing engine shared by VT Code terminal surfaces.
2
3mod engine;
4mod text;
5mod types;
6
7pub use engine::{Editor, HandleKeyOutcome, handle_key};
8pub use types::{VimMode, VimState};
9
10#[cfg(test)]
11mod tests {
12    use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
13
14    use super::{Editor, VimState, handle_key};
15
16    #[derive(Debug)]
17    struct TestEditor {
18        content: String,
19        cursor: usize,
20    }
21
22    impl TestEditor {
23        fn new(content: &str, cursor: usize) -> Self {
24            Self {
25                content: content.to_string(),
26                cursor,
27            }
28        }
29    }
30
31    impl Editor for TestEditor {
32        fn content(&self) -> &str {
33            &self.content
34        }
35
36        fn cursor(&self) -> usize {
37            self.cursor
38        }
39
40        fn set_cursor(&mut self, pos: usize) {
41            self.cursor = pos.min(self.content.len());
42        }
43
44        fn move_left(&mut self) {
45            if self.cursor == 0 {
46                return;
47            }
48            let mut pos = self.cursor - 1;
49            while pos > 0 && !self.content.is_char_boundary(pos) {
50                pos -= 1;
51            }
52            self.cursor = pos;
53        }
54
55        fn move_right(&mut self) {
56            if self.cursor >= self.content.len() {
57                return;
58            }
59            let mut pos = self.cursor + 1;
60            while pos < self.content.len() && !self.content.is_char_boundary(pos) {
61                pos += 1;
62            }
63            self.cursor = pos;
64        }
65
66        fn delete_char_forward(&mut self) {
67            if self.cursor >= self.content.len() {
68                return;
69            }
70            let mut end = self.cursor + 1;
71            while end < self.content.len() && !self.content.is_char_boundary(end) {
72                end += 1;
73            }
74            self.content.drain(self.cursor..end);
75        }
76
77        fn insert_text(&mut self, text: &str) {
78            self.content.insert_str(self.cursor, text);
79            self.cursor += text.len();
80        }
81
82        fn replace(&mut self, content: String, cursor: usize) {
83            self.content = content;
84            self.cursor = cursor.min(self.content.len());
85        }
86    }
87
88    fn enable_normal_mode(state: &mut VimState, editor: &mut TestEditor, clipboard: &mut String) {
89        state.set_enabled(true);
90        let outcome = handle_key(
91            state,
92            editor,
93            clipboard,
94            &KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE),
95        );
96        assert!(outcome.handled);
97    }
98
99    #[test]
100    fn control_shortcuts_remain_unhandled() {
101        let mut state = VimState::new(true);
102        let mut editor = TestEditor::new("hello", 5);
103        let mut clipboard = String::new();
104
105        let outcome = handle_key(
106            &mut state,
107            &mut editor,
108            &mut clipboard,
109            &KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL),
110        );
111
112        assert!(!outcome.handled);
113        assert_eq!(editor.content, "hello");
114    }
115
116    #[test]
117    fn dd_deletes_current_line() {
118        let mut state = VimState::new(false);
119        let mut editor = TestEditor::new("one\ntwo\nthree", 4);
120        let mut clipboard = String::new();
121        enable_normal_mode(&mut state, &mut editor, &mut clipboard);
122
123        assert!(
124            handle_key(
125                &mut state,
126                &mut editor,
127                &mut clipboard,
128                &KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE),
129            )
130            .handled
131        );
132        assert!(
133            handle_key(
134                &mut state,
135                &mut editor,
136                &mut clipboard,
137                &KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE),
138            )
139            .handled
140        );
141
142        assert_eq!(editor.content, "one\nthree");
143        assert_eq!(editor.cursor, 4);
144    }
145
146    #[test]
147    fn dot_repeats_change_word_edit() {
148        let mut state = VimState::new(false);
149        let mut editor = TestEditor::new("alpha beta", 0);
150        let mut clipboard = String::new();
151        enable_normal_mode(&mut state, &mut editor, &mut clipboard);
152
153        let _ = handle_key(
154            &mut state,
155            &mut editor,
156            &mut clipboard,
157            &KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE),
158        );
159        let _ = handle_key(
160            &mut state,
161            &mut editor,
162            &mut clipboard,
163            &KeyEvent::new(KeyCode::Char('w'), KeyModifiers::NONE),
164        );
165        editor.insert_text("A");
166        let _ = handle_key(
167            &mut state,
168            &mut editor,
169            &mut clipboard,
170            &KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE),
171        );
172
173        editor.set_cursor(1);
174        let _ = handle_key(
175            &mut state,
176            &mut editor,
177            &mut clipboard,
178            &KeyEvent::new(KeyCode::Char('.'), KeyModifiers::NONE),
179        );
180
181        assert_eq!(editor.content, "AA");
182    }
183
184    #[test]
185    fn dot_repeats_change_line_edit() {
186        let mut state = VimState::new(false);
187        let mut editor = TestEditor::new("one\ntwo", 0);
188        let mut clipboard = String::new();
189        enable_normal_mode(&mut state, &mut editor, &mut clipboard);
190
191        let _ = handle_key(
192            &mut state,
193            &mut editor,
194            &mut clipboard,
195            &KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE),
196        );
197        let _ = handle_key(
198            &mut state,
199            &mut editor,
200            &mut clipboard,
201            &KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE),
202        );
203        editor.insert_text("ONE");
204        let _ = handle_key(
205            &mut state,
206            &mut editor,
207            &mut clipboard,
208            &KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE),
209        );
210
211        editor.set_cursor(4);
212        let _ = handle_key(
213            &mut state,
214            &mut editor,
215            &mut clipboard,
216            &KeyEvent::new(KeyCode::Char('.'), KeyModifiers::NONE),
217        );
218
219        assert_eq!(editor.content, "ONE\nONE");
220    }
221
222    #[test]
223    fn vertical_motion_preserves_preferred_column() {
224        let mut state = VimState::new(false);
225        let mut editor = TestEditor::new("abcd\nxy\nabcd", 3);
226        let mut clipboard = String::new();
227        enable_normal_mode(&mut state, &mut editor, &mut clipboard);
228
229        let _ = handle_key(
230            &mut state,
231            &mut editor,
232            &mut clipboard,
233            &KeyEvent::new(KeyCode::Char('j'), KeyModifiers::NONE),
234        );
235        assert_eq!(editor.cursor, 7);
236
237        let _ = handle_key(
238            &mut state,
239            &mut editor,
240            &mut clipboard,
241            &KeyEvent::new(KeyCode::Char('j'), KeyModifiers::NONE),
242        );
243        assert_eq!(editor.cursor, 11);
244
245        let _ = handle_key(
246            &mut state,
247            &mut editor,
248            &mut clipboard,
249            &KeyEvent::new(KeyCode::Char('k'), KeyModifiers::NONE),
250        );
251        assert_eq!(editor.cursor, 7);
252    }
253
254    #[test]
255    fn find_repeat_reuses_last_character_search() {
256        let mut state = VimState::new(false);
257        let mut editor = TestEditor::new("a b c b", 0);
258        let mut clipboard = String::new();
259        enable_normal_mode(&mut state, &mut editor, &mut clipboard);
260
261        let _ = handle_key(
262            &mut state,
263            &mut editor,
264            &mut clipboard,
265            &KeyEvent::new(KeyCode::Char('f'), KeyModifiers::NONE),
266        );
267        let _ = handle_key(
268            &mut state,
269            &mut editor,
270            &mut clipboard,
271            &KeyEvent::new(KeyCode::Char('b'), KeyModifiers::NONE),
272        );
273        assert_eq!(editor.cursor, 2);
274
275        let _ = handle_key(
276            &mut state,
277            &mut editor,
278            &mut clipboard,
279            &KeyEvent::new(KeyCode::Char(';'), KeyModifiers::NONE),
280        );
281        assert_eq!(editor.cursor, 6);
282
283        let _ = handle_key(
284            &mut state,
285            &mut editor,
286            &mut clipboard,
287            &KeyEvent::new(KeyCode::Char(','), KeyModifiers::NONE),
288        );
289        assert_eq!(editor.cursor, 2);
290    }
291}