1mod engine;
4mod text;
5mod types;
6
7pub use engine::{Editor, HandleKeyOutcome, handle_key};
8pub use text::{next_char_boundary, prev_char_boundary};
9pub use types::{VimMode, VimState};
10
11#[cfg(test)]
12mod tests {
13 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
14
15 use super::{Editor, VimState, handle_key, next_char_boundary, prev_char_boundary};
16
17 #[derive(Debug)]
18 struct TestEditor {
19 content: String,
20 cursor: usize,
21 }
22
23 impl TestEditor {
24 fn new(content: &str, cursor: usize) -> Self {
25 Self {
26 content: content.to_string(),
27 cursor,
28 }
29 }
30 }
31
32 impl Editor for TestEditor {
33 fn content(&self) -> &str {
34 &self.content
35 }
36
37 fn cursor(&self) -> usize {
38 self.cursor
39 }
40
41 fn set_cursor(&mut self, pos: usize) {
42 self.cursor = pos.min(self.content.len());
43 }
44
45 fn move_left(&mut self) {
46 self.cursor = prev_char_boundary(&self.content, self.cursor);
47 }
48
49 fn move_right(&mut self) {
50 self.cursor = next_char_boundary(&self.content, self.cursor);
51 }
52
53 fn delete_char_forward(&mut self) {
54 if self.cursor >= self.content.len() {
55 return;
56 }
57 let end = next_char_boundary(&self.content, self.cursor);
58 self.content.drain(self.cursor..end);
59 }
60
61 fn insert_text(&mut self, text: &str) {
62 self.content.insert_str(self.cursor, text);
63 self.cursor += text.len();
64 }
65
66 fn replace(&mut self, content: String, cursor: usize) {
67 self.content = content;
68 self.cursor = cursor.min(self.content.len());
69 }
70
71 fn replace_range(&mut self, start: usize, end: usize, text: &str) {
72 self.content.replace_range(start..end, text);
73 self.cursor = (start + text.len()).min(self.content.len());
74 }
75 }
76
77 fn enable_normal_mode(state: &mut VimState, editor: &mut TestEditor, clipboard: &mut String) {
78 state.set_enabled(true);
79 let outcome = handle_key(
80 state,
81 editor,
82 clipboard,
83 &KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE),
84 );
85 assert!(outcome.handled);
86 }
87
88 #[test]
89 fn control_shortcuts_remain_unhandled() {
90 let mut state = VimState::new(true);
91 let mut editor = TestEditor::new("hello", 5);
92 let mut clipboard = String::new();
93
94 let outcome = handle_key(
95 &mut state,
96 &mut editor,
97 &mut clipboard,
98 &KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL),
99 );
100
101 assert!(!outcome.handled);
102 assert_eq!(editor.content, "hello");
103 }
104
105 #[test]
106 fn dd_deletes_current_line() {
107 let mut state = VimState::new(false);
108 let mut editor = TestEditor::new("one\ntwo\nthree", 4);
109 let mut clipboard = String::new();
110 enable_normal_mode(&mut state, &mut editor, &mut clipboard);
111
112 assert!(
113 handle_key(
114 &mut state,
115 &mut editor,
116 &mut clipboard,
117 &KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE),
118 )
119 .handled
120 );
121 assert!(
122 handle_key(
123 &mut state,
124 &mut editor,
125 &mut clipboard,
126 &KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE),
127 )
128 .handled
129 );
130
131 assert_eq!(editor.content, "one\nthree");
132 assert_eq!(editor.cursor, 4);
133 }
134
135 #[test]
136 fn dot_repeats_change_word_edit() {
137 let mut state = VimState::new(false);
138 let mut editor = TestEditor::new("alpha beta", 0);
139 let mut clipboard = String::new();
140 enable_normal_mode(&mut state, &mut editor, &mut clipboard);
141
142 let _ = handle_key(
143 &mut state,
144 &mut editor,
145 &mut clipboard,
146 &KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE),
147 );
148 let _ = handle_key(
149 &mut state,
150 &mut editor,
151 &mut clipboard,
152 &KeyEvent::new(KeyCode::Char('w'), KeyModifiers::NONE),
153 );
154 editor.insert_text("A");
155 let _ = handle_key(
156 &mut state,
157 &mut editor,
158 &mut clipboard,
159 &KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE),
160 );
161
162 editor.set_cursor(1);
163 let _ = handle_key(
164 &mut state,
165 &mut editor,
166 &mut clipboard,
167 &KeyEvent::new(KeyCode::Char('.'), KeyModifiers::NONE),
168 );
169
170 assert_eq!(editor.content, "AA");
171 }
172
173 #[test]
174 fn dot_repeats_change_line_edit() {
175 let mut state = VimState::new(false);
176 let mut editor = TestEditor::new("one\ntwo", 0);
177 let mut clipboard = String::new();
178 enable_normal_mode(&mut state, &mut editor, &mut clipboard);
179
180 let _ = handle_key(
181 &mut state,
182 &mut editor,
183 &mut clipboard,
184 &KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE),
185 );
186 let _ = handle_key(
187 &mut state,
188 &mut editor,
189 &mut clipboard,
190 &KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE),
191 );
192 editor.insert_text("ONE");
193 let _ = handle_key(
194 &mut state,
195 &mut editor,
196 &mut clipboard,
197 &KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE),
198 );
199
200 editor.set_cursor(4);
201 let _ = handle_key(
202 &mut state,
203 &mut editor,
204 &mut clipboard,
205 &KeyEvent::new(KeyCode::Char('.'), KeyModifiers::NONE),
206 );
207
208 assert_eq!(editor.content, "ONE\nONE");
209 }
210
211 #[test]
212 fn vertical_motion_preserves_preferred_column() {
213 let mut state = VimState::new(false);
214 let mut editor = TestEditor::new("abcd\nxy\nabcd", 3);
215 let mut clipboard = String::new();
216 enable_normal_mode(&mut state, &mut editor, &mut clipboard);
217
218 let _ = handle_key(
219 &mut state,
220 &mut editor,
221 &mut clipboard,
222 &KeyEvent::new(KeyCode::Char('j'), KeyModifiers::NONE),
223 );
224 assert_eq!(editor.cursor, 7);
225
226 let _ = handle_key(
227 &mut state,
228 &mut editor,
229 &mut clipboard,
230 &KeyEvent::new(KeyCode::Char('j'), KeyModifiers::NONE),
231 );
232 assert_eq!(editor.cursor, 11);
233
234 let _ = handle_key(
235 &mut state,
236 &mut editor,
237 &mut clipboard,
238 &KeyEvent::new(KeyCode::Char('k'), KeyModifiers::NONE),
239 );
240 assert_eq!(editor.cursor, 7);
241 }
242
243 #[test]
244 fn find_repeat_reuses_last_character_search() {
245 let mut state = VimState::new(false);
246 let mut editor = TestEditor::new("a b c b", 0);
247 let mut clipboard = String::new();
248 enable_normal_mode(&mut state, &mut editor, &mut clipboard);
249
250 let _ = handle_key(
251 &mut state,
252 &mut editor,
253 &mut clipboard,
254 &KeyEvent::new(KeyCode::Char('f'), KeyModifiers::NONE),
255 );
256 let _ = handle_key(
257 &mut state,
258 &mut editor,
259 &mut clipboard,
260 &KeyEvent::new(KeyCode::Char('b'), KeyModifiers::NONE),
261 );
262 assert_eq!(editor.cursor, 2);
263
264 let _ = handle_key(
265 &mut state,
266 &mut editor,
267 &mut clipboard,
268 &KeyEvent::new(KeyCode::Char(';'), KeyModifiers::NONE),
269 );
270 assert_eq!(editor.cursor, 6);
271
272 let _ = handle_key(
273 &mut state,
274 &mut editor,
275 &mut clipboard,
276 &KeyEvent::new(KeyCode::Char(','), KeyModifiers::NONE),
277 );
278 assert_eq!(editor.cursor, 2);
279 }
280
281 #[test]
282 fn x_deletes_character_at_cursor() {
283 let mut state = VimState::new(false);
284 let mut editor = TestEditor::new("hello", 1);
285 let mut clipboard = String::new();
286 enable_normal_mode(&mut state, &mut editor, &mut clipboard);
287
288 let _ = handle_key(
289 &mut state,
290 &mut editor,
291 &mut clipboard,
292 &KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE),
293 );
294 assert_eq!(editor.content, "hllo");
295 assert_eq!(editor.cursor, 1);
296 }
297
298 #[test]
299 fn dw_deletes_word_forward() {
300 let mut state = VimState::new(false);
301 let mut editor = TestEditor::new("hello world", 0);
302 let mut clipboard = String::new();
303 enable_normal_mode(&mut state, &mut editor, &mut clipboard);
304
305 let _ = handle_key(
306 &mut state,
307 &mut editor,
308 &mut clipboard,
309 &KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE),
310 );
311 let _ = handle_key(
312 &mut state,
313 &mut editor,
314 &mut clipboard,
315 &KeyEvent::new(KeyCode::Char('w'), KeyModifiers::NONE),
316 );
317 assert_eq!(editor.content, "world");
318 assert_eq!(editor.cursor, 0);
319 }
320
321 #[test]
322 fn j_joins_lines() {
323 let mut state = VimState::new(false);
324 let mut editor = TestEditor::new("hello\nworld", 0);
325 let mut clipboard = String::new();
326 enable_normal_mode(&mut state, &mut editor, &mut clipboard);
327
328 let _ = handle_key(
329 &mut state,
330 &mut editor,
331 &mut clipboard,
332 &KeyEvent::new(KeyCode::Char('J'), KeyModifiers::NONE),
333 );
334 assert_eq!(editor.content, "hello world");
335 assert_eq!(editor.cursor, 6);
337 }
338
339 #[test]
340 fn p_pastes_charwise_after_cursor() {
341 let mut state = VimState::new(false);
342 let mut editor = TestEditor::new("abc", 1);
343 let mut clipboard = "XY".to_string();
344 enable_normal_mode(&mut state, &mut editor, &mut clipboard);
345
346 state.clipboard_kind = crate::types::ClipboardKind::CharWise;
347
348 let _ = handle_key(
349 &mut state,
350 &mut editor,
351 &mut clipboard,
352 &KeyEvent::new(KeyCode::Char('p'), KeyModifiers::NONE),
353 );
354 assert_eq!(editor.content, "abXYc");
356 }
357
358 #[test]
359 fn p_pastes_linewise_after_current_line() {
360 let mut state = VimState::new(false);
361 let mut editor = TestEditor::new("one\nthree", 0);
362 let mut clipboard = "two\n".to_string();
363 enable_normal_mode(&mut state, &mut editor, &mut clipboard);
364
365 state.clipboard_kind = crate::types::ClipboardKind::LineWise;
366
367 let _ = handle_key(
368 &mut state,
369 &mut editor,
370 &mut clipboard,
371 &KeyEvent::new(KeyCode::Char('p'), KeyModifiers::NONE),
372 );
373 assert_eq!(editor.content, "one\ntwo\nthree");
374 }
375
376 #[test]
377 fn y_then_p_yanks_and_pastes_line() {
378 let mut state = VimState::new(false);
379 let mut editor = TestEditor::new("one\ntwo\nthree", 0);
380 let mut clipboard = String::new();
381 enable_normal_mode(&mut state, &mut editor, &mut clipboard);
382
383 let _ = handle_key(
385 &mut state,
386 &mut editor,
387 &mut clipboard,
388 &KeyEvent::new(KeyCode::Char('Y'), KeyModifiers::NONE),
389 );
390 assert_eq!(clipboard, "one\n");
391
392 let _ = handle_key(
394 &mut state,
395 &mut editor,
396 &mut clipboard,
397 &KeyEvent::new(KeyCode::Char('p'), KeyModifiers::NONE),
398 );
399 assert_eq!(editor.content, "one\none\ntwo\nthree");
400 }
401
402 #[test]
403 fn d_deletes_to_line_end() {
404 let mut state = VimState::new(false);
405 let mut editor = TestEditor::new("hello world", 5);
406 let mut clipboard = String::new();
407 enable_normal_mode(&mut state, &mut editor, &mut clipboard);
408
409 let _ = handle_key(
410 &mut state,
411 &mut editor,
412 &mut clipboard,
413 &KeyEvent::new(KeyCode::Char('D'), KeyModifiers::NONE),
414 );
415 assert_eq!(editor.content, "hello");
416 assert_eq!(editor.cursor, 5);
417 }
418
419 #[test]
420 fn w_moves_to_next_word_start() {
421 let mut state = VimState::new(false);
422 let mut editor = TestEditor::new("hello world foo", 0);
423 let mut clipboard = String::new();
424 enable_normal_mode(&mut state, &mut editor, &mut clipboard);
425
426 let _ = handle_key(
427 &mut state,
428 &mut editor,
429 &mut clipboard,
430 &KeyEvent::new(KeyCode::Char('w'), KeyModifiers::NONE),
431 );
432 assert_eq!(editor.cursor, 6);
433
434 let _ = handle_key(
435 &mut state,
436 &mut editor,
437 &mut clipboard,
438 &KeyEvent::new(KeyCode::Char('w'), KeyModifiers::NONE),
439 );
440 assert_eq!(editor.cursor, 12);
441 }
442
443 #[test]
444 fn b_moves_to_prev_word_start() {
445 let mut state = VimState::new(false);
446 let mut editor = TestEditor::new("hello world foo", 12);
447 let mut clipboard = String::new();
448 enable_normal_mode(&mut state, &mut editor, &mut clipboard);
449
450 let _ = handle_key(
451 &mut state,
452 &mut editor,
453 &mut clipboard,
454 &KeyEvent::new(KeyCode::Char('b'), KeyModifiers::NONE),
455 );
456 assert_eq!(editor.cursor, 6);
457
458 let _ = handle_key(
459 &mut state,
460 &mut editor,
461 &mut clipboard,
462 &KeyEvent::new(KeyCode::Char('b'), KeyModifiers::NONE),
463 );
464 assert_eq!(editor.cursor, 0);
465 }
466
467 #[test]
468 fn indent_adds_whitespace() {
469 let mut state = VimState::new(false);
470 let mut editor = TestEditor::new("hello\nworld", 0);
471 let mut clipboard = String::new();
472 enable_normal_mode(&mut state, &mut editor, &mut clipboard);
473
474 let _ = handle_key(
475 &mut state,
476 &mut editor,
477 &mut clipboard,
478 &KeyEvent::new(KeyCode::Char('>'), KeyModifiers::NONE),
479 );
480 let _ = handle_key(
481 &mut state,
482 &mut editor,
483 &mut clipboard,
484 &KeyEvent::new(KeyCode::Char('>'), KeyModifiers::NONE),
485 );
486 assert_eq!(editor.content, " hello\nworld");
487 }
488
489 #[test]
490 fn ciw_changes_inner_word() {
491 let mut state = VimState::new(false);
492 let mut editor = TestEditor::new("hello world", 0);
493 let mut clipboard = String::new();
494 enable_normal_mode(&mut state, &mut editor, &mut clipboard);
495
496 let _ = handle_key(
498 &mut state,
499 &mut editor,
500 &mut clipboard,
501 &KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE),
502 );
503 let _ = handle_key(
504 &mut state,
505 &mut editor,
506 &mut clipboard,
507 &KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE),
508 );
509 let _ = handle_key(
510 &mut state,
511 &mut editor,
512 &mut clipboard,
513 &KeyEvent::new(KeyCode::Char('w'), KeyModifiers::NONE),
514 );
515 editor.insert_text("hi");
517 let _ = handle_key(
518 &mut state,
519 &mut editor,
520 &mut clipboard,
521 &KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE),
522 );
523 assert_eq!(editor.content, "hi world");
524 }
525}