1use std::cell::Cell;
2
3use crossterm::event::KeyCode;
4use unicode_segmentation::UnicodeSegmentation;
5use unicode_width::UnicodeWidthStr;
6
7use crate::{
8 Component,
9 Event,
10 Focusable,
11 InputResult,
12 RenderError,
13 Rendered,
14 kill_ring::KillRing,
15 theme::{
16 Style,
17 Theme,
18 },
19 undo_stack::UndoStack,
20};
21
22#[derive(Clone)]
24pub struct EditAction {
25 pub text: String,
27 pub cursor: usize,
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum InputVimMode {
34 Normal,
36 Insert,
38}
39
40pub struct Input {
49 text: String,
50 cursor: usize,
51 focused: bool,
52 kill_ring: KillRing,
53 undo_stack: UndoStack<EditAction>,
54 scroll: Cell<usize>,
55 vim_mode_enabled: bool,
56 mode: InputVimMode,
57}
58
59impl Input {
60 pub fn new() -> Self {
62 Self {
63 text: String::new(),
64 cursor: 0,
65 focused: false,
66 kill_ring: KillRing::new(),
67 undo_stack: UndoStack::new(),
68 scroll: Cell::new(0),
69 vim_mode_enabled: false,
70 mode: InputVimMode::Normal,
71 }
72 }
73
74 pub fn vim_mode_enabled(&self) -> bool {
76 self.vim_mode_enabled
77 }
78
79 pub fn set_vim_mode_enabled(&mut self, enabled: bool) {
85 self.vim_mode_enabled = enabled;
86 self.mode = InputVimMode::Normal;
87 }
88
89 pub fn mode(&self) -> InputVimMode {
91 self.mode
92 }
93
94 pub fn set_mode(&mut self, mode: InputVimMode) {
96 self.mode = mode;
97 }
98
99 pub fn text(&self) -> &str {
101 &self.text
102 }
103
104 pub fn cursor(&self) -> usize {
106 self.cursor
107 }
108
109 pub fn scroll(&self) -> usize {
111 self.scroll.get()
112 }
113
114 pub fn set_text(&mut self, text: impl Into<String>) {
116 self.save_undo();
117 self.text = text.into();
118 self.cursor = self.graphemes().len();
119 self.scroll.set(0);
120 }
121
122 fn save_undo(&mut self) {
123 self.undo_stack.push(EditAction {
124 text: self.text.clone(),
125 cursor: self.cursor,
126 });
127 }
128
129 fn graphemes(&self) -> Vec<&str> {
130 self.text.graphemes(true).collect()
131 }
132
133 fn byte_index(&self, grapheme_idx: usize) -> usize {
134 self.text
135 .grapheme_indices(true)
136 .nth(grapheme_idx)
137 .map(|(i, _)| i)
138 .unwrap_or(self.text.len())
139 }
140
141 fn insert_char(&mut self, c: char) {
142 let idx = self.byte_index(self.cursor);
143 self.text.insert(idx, c);
144 self.cursor += 1;
145 }
146
147 fn delete_backward(&mut self) {
148 if self.cursor > 0 {
149 let start = self.byte_index(self.cursor - 1);
150 let end = self.byte_index(self.cursor);
151 let killed = self.text.drain(start..end).collect::<String>();
152 self.kill_ring.push(killed);
153 self.cursor -= 1;
154 }
155 }
156
157 fn delete_forward(&mut self) {
158 if self.cursor < self.graphemes().len() {
159 let start = self.byte_index(self.cursor);
160 let end = self.byte_index(self.cursor + 1);
161 self.text.drain(start..end);
162 }
163 }
164
165 fn move_cursor_left(&mut self) {
166 if self.cursor > 0 {
167 self.cursor -= 1;
168 }
169 }
170
171 fn move_cursor_right(&mut self) {
172 if self.cursor < self.graphemes().len() {
173 self.cursor += 1;
174 }
175 }
176
177 fn move_cursor_home(&mut self) {
178 self.cursor = 0;
179 }
180
181 fn move_cursor_end(&mut self) {
182 self.cursor = self.graphemes().len();
183 }
184
185 fn yank(&mut self) {
186 if let Some(text) = self.kill_ring.yank() {
187 let idx = self.byte_index(self.cursor);
188 self.text.insert_str(idx, text);
189 self.cursor += text.graphemes(true).count();
190 }
191 }
192
193 fn undo(&mut self) {
194 if let Some(action) = self.undo_stack.undo() {
195 self.text = action.text.clone();
196 self.cursor = action.cursor;
197 }
198 }
199}
200
201impl Default for Input {
202 fn default() -> Self {
203 Self::new()
204 }
205}
206
207impl Focusable for Input {
208 fn focused(&self) -> bool {
209 self.focused
210 }
211
212 fn set_focused(&mut self, focused: bool) {
213 self.focused = focused;
214 }
215}
216
217impl Input {
218 fn handle_insert_mode(&mut self, key: &crossterm::event::KeyEvent) -> InputResult {
219 use crossterm::event::KeyModifiers;
220 self.save_undo();
221 match key.code {
222 | KeyCode::Char(c) => {
223 if key.modifiers.contains(KeyModifiers::CONTROL) {
224 match c {
225 | 'a' => self.move_cursor_home(),
226 | 'e' => self.move_cursor_end(),
227 | 'b' => self.move_cursor_left(),
228 | 'f' => self.move_cursor_right(),
229 | 'd' => self.delete_forward(),
230 | 'h' => self.delete_backward(),
231 | 'k' => {
232 let idx = self.byte_index(self.cursor);
233 let killed = self.text.split_off(idx);
234 self.kill_ring.push(killed);
235 },
236 | 'y' => self.yank(),
237 | '-' | '_' => self.undo(),
238 | _ => return InputResult::Ignored,
239 }
240 } else {
241 self.insert_char(c);
242 }
243 InputResult::Handled
244 },
245 | KeyCode::Left => {
246 self.move_cursor_left();
247 InputResult::Handled
248 },
249 | KeyCode::Right => {
250 self.move_cursor_right();
251 InputResult::Handled
252 },
253 | KeyCode::Home => {
254 self.move_cursor_home();
255 InputResult::Handled
256 },
257 | KeyCode::End => {
258 self.move_cursor_end();
259 InputResult::Handled
260 },
261 | KeyCode::Backspace => {
262 self.delete_backward();
263 InputResult::Handled
264 },
265 | KeyCode::Delete => {
266 self.delete_forward();
267 InputResult::Handled
268 },
269 | KeyCode::Esc => {
270 self.mode = InputVimMode::Normal;
271 InputResult::Handled
272 },
273 | _ => InputResult::Ignored,
274 }
275 }
276
277 fn handle_normal_mode(&mut self, key: &crossterm::event::KeyEvent) -> InputResult {
278 match key.code {
279 | KeyCode::Char(c) => {
280 match c {
281 | 'h' => self.move_cursor_left(),
282 | 'l' => self.move_cursor_right(),
283 | 'x' => {
284 self.save_undo();
285 self.delete_forward();
286 },
287 | '0' => self.move_cursor_home(),
288 | '$' => self.move_cursor_end(),
289 | 'i' => self.mode = InputVimMode::Insert,
290 | 'a' => {
291 self.move_cursor_right();
292 self.mode = InputVimMode::Insert;
293 },
294 | 'p' => {
295 self.save_undo();
296 self.yank();
297 },
298 | 'u' => {
299 self.save_undo();
300 self.undo();
301 },
302 | _ => return InputResult::Ignored,
303 }
304 InputResult::Handled
305 },
306 | KeyCode::Left => {
307 self.move_cursor_left();
308 InputResult::Handled
309 },
310 | KeyCode::Right => {
311 self.move_cursor_right();
312 InputResult::Handled
313 },
314 | KeyCode::Home => {
315 self.move_cursor_home();
316 InputResult::Handled
317 },
318 | KeyCode::End => {
319 self.move_cursor_end();
320 InputResult::Handled
321 },
322 | KeyCode::Backspace => {
323 self.move_cursor_left();
324 InputResult::Handled
325 },
326 | _ => InputResult::Ignored,
327 }
328 }
329}
330
331impl Component for Input {
332 fn render(&self, width: u16) -> Result<Rendered, RenderError> {
333 let w = width as usize;
334 let graphemes = self.graphemes();
335
336 let mut cum_vw = vec![0usize; graphemes.len() + 1];
338 for (i, g) in graphemes.iter().enumerate() {
339 cum_vw[i + 1] = cum_vw[i] + g.width();
340 }
341
342 let cursor_vw = cum_vw[self.cursor.min(graphemes.len())];
343 let mut scroll = self.scroll.get().min(graphemes.len());
344
345 let cursor_screen_vw = cursor_vw.saturating_sub(cum_vw[scroll]);
347 if cursor_screen_vw > w.saturating_sub(1) {
348 let target = cursor_vw.saturating_sub(w.saturating_sub(1));
349 scroll = cum_vw.partition_point(|&v| v < target);
350 scroll = scroll.min(graphemes.len());
351 } else if self.cursor < scroll {
352 scroll = self.cursor;
353 }
354
355 self.scroll.set(scroll);
356
357 let mut line = String::new();
359 let mut display_vw = 0;
360 for g in graphemes.iter().skip(scroll) {
361 let gw = g.width();
362 if display_vw + gw > w {
363 break;
364 }
365 line.push_str(g);
366 display_vw += gw;
367 }
368
369 let cursor_col = cursor_vw.saturating_sub(cum_vw[scroll]);
370 let rendered_line = if self.focused {
371 let theme = Theme::palette();
372 let edit_style = Style::new().fg(theme.text()).bg(theme.surface());
373 let cursor_style = Style::new().fg(theme.cursor()).bg(theme.surface());
374 crate::utils::render_line_with_cursor(
375 &line,
376 cursor_col,
377 width,
378 &edit_style,
379 &cursor_style,
380 )
381 } else {
382 if display_vw < w {
383 line.push_str(&" ".repeat(w - display_vw));
384 }
385 line
386 };
387
388 Ok(Rendered {
389 lines: vec![rendered_line],
390 cursor: None,
391 images: Vec::new(),
392 })
393 }
394
395 fn handle_input(&mut self, event: &Event) -> InputResult {
396 if let Event::Key(key) = event {
397 if self.vim_mode_enabled {
398 match self.mode {
399 | InputVimMode::Insert => self.handle_insert_mode(key),
400 | InputVimMode::Normal => self.handle_normal_mode(key),
401 }
402 } else {
403 self.handle_insert_mode(key)
404 }
405 } else {
406 InputResult::Ignored
407 }
408 }
409
410 fn as_focusable(&self) -> Option<&dyn Focusable> {
411 Some(self)
412 }
413
414 fn as_focusable_mut(&mut self) -> Option<&mut dyn Focusable> {
415 Some(self)
416 }
417}
418
419#[cfg(test)]
420mod tests {
421 use crossterm::event::{
422 KeyCode,
423 KeyEvent,
424 KeyModifiers,
425 };
426
427 use super::*;
428
429 fn key_event(code: KeyCode) -> Event {
430 Event::Key(code.into())
431 }
432
433 fn ctrl_event(c: char) -> Event {
434 Event::Key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::CONTROL))
435 }
436
437 #[test]
438 fn input_delete_forward_at_end() {
439 let mut input = Input::new();
440 input.insert_char('a');
441 input.move_cursor_end();
442 input.delete_forward();
443 assert_eq!(input.text(), "a");
444 }
445
446 #[test]
447 fn input_scrolls_when_long() {
448 let mut input = Input::new();
449 input.set_focused(true);
450 input.set_mode(InputVimMode::Insert);
451 for _ in 0..20 {
452 input.handle_input(&key_event(KeyCode::Char('x')));
453 }
454 let r = input.render(10).unwrap();
455 assert_eq!(crate::utils::visible_width(&r.lines[0]), 10);
456 assert!(r.cursor.is_none());
457 assert_eq!(input.scroll(), 11);
458 assert!(r.lines[0].contains(crate::utils::EDIT_CURSOR));
459 assert!(r.lines[0].contains("\x1b[48;"));
460 }
461
462 #[test]
463 fn input_scrolls_back_left() {
464 let mut input = Input::new();
465 input.set_focused(true);
466 input.set_mode(InputVimMode::Insert);
467 for _ in 0..20 {
468 input.handle_input(&key_event(KeyCode::Char('x')));
469 }
470 input.render(10).unwrap();
471 assert_eq!(input.scroll(), 11);
472 input.handle_input(&ctrl_event('a'));
474 let r = input.render(10).unwrap();
475 assert_eq!(input.scroll(), 0);
476 assert!(r.cursor.is_none());
477 assert!(r.lines[0].starts_with('\x1b'));
478 assert!(r.lines[0].contains(crate::utils::EDIT_CURSOR));
479 }
480
481 #[test]
482 fn input_render_unfocused() {
483 let mut input = Input::new();
484 input.set_focused(false);
485 input.insert_char('a');
486 let r = input.render(10).unwrap();
487 assert!(r.cursor.is_none());
488 }
489
490 #[test]
491 fn input_yank() {
492 let mut input = Input::new();
493 input.set_mode(InputVimMode::Insert);
494 input.insert_char('a');
495 input.insert_char('b');
496 input.move_cursor_home();
497 input.handle_input(&ctrl_event('k'));
498 assert_eq!(input.text(), "");
499 input.yank();
500 assert_eq!(input.text(), "ab");
501 }
502
503 #[test]
504 fn input_ctrl_d_delete() {
505 let mut input = Input::new();
506 input.set_mode(InputVimMode::Insert);
507 input.insert_char('a');
508 input.insert_char('b');
509 input.move_cursor_home();
510 input.handle_input(&ctrl_event('d'));
511 assert_eq!(input.text(), "b");
512 }
513
514 #[test]
515 fn input_ignored_ctrl_key() {
516 let mut input = Input::new();
517 let result = input.handle_input(&ctrl_event('z'));
518 assert!(matches!(result, InputResult::Ignored));
519 }
520
521 #[test]
522 fn input_resize_ignored() {
523 let mut input = Input::new();
524 let result = input.handle_input(&Event::Resize(80, 24));
525 assert!(matches!(result, InputResult::Ignored));
526 }
527
528 #[test]
529 fn input_delete_backward_at_start() {
530 let mut input = Input::new();
531 input.delete_backward();
532 assert_eq!(input.text(), "");
533 }
534
535 #[test]
536 fn input_move_past_bounds() {
537 let mut input = Input::new();
538 input.move_cursor_left();
539 assert_eq!(input.cursor(), 0);
540 input.insert_char('a');
541 input.move_cursor_right();
542 input.move_cursor_right();
543 assert_eq!(input.cursor(), 1);
544 }
545
546 #[test]
547 fn input_enter_ignored() {
548 let mut input = Input::new();
549 let result = input.handle_input(&key_event(KeyCode::Enter));
550 assert!(matches!(result, InputResult::Ignored));
551 }
552
553 #[test]
554 fn input_tab_ignored() {
555 let mut input = Input::new();
556 let result = input.handle_input(&key_event(KeyCode::Tab));
557 assert!(matches!(result, InputResult::Ignored));
558 }
559
560 #[test]
561 fn input_render_pads_with_spaces() {
562 let mut input = Input::new();
563 input.set_focused(true);
564 input.insert_char('a');
565 let r = input.render(10).unwrap();
566 assert_eq!(crate::utils::visible_width(&r.lines[0]), 10);
567 assert!(r.lines[0].contains(crate::utils::EDIT_CURSOR));
568 assert!(r.lines[0].contains("\x1b[48;"));
569 }
570
571 #[test]
572 fn input_render_focused_cursor_uses_accent_colour() {
573 Theme::with(Theme::Light, || {
574 let mut input = Input::new();
575 input.set_focused(true);
576 input.insert_char('a');
577 let r = input.render(10).unwrap();
578 assert!(r.lines[0].contains("\x1b[38;2;250;82;15m"));
579 });
580 }
581
582 #[test]
585 fn vim_input_starts_in_normal_mode() {
586 let mut input = Input::new();
587 input.set_vim_mode_enabled(true);
588 assert_eq!(input.mode(), InputVimMode::Normal);
589 }
590
591 #[test]
592 fn vim_input_hl_navigation() {
593 let mut input = Input::new();
594 input.set_vim_mode_enabled(true);
595 input.set_mode(InputVimMode::Insert);
596 input.insert_char('a');
597 input.insert_char('b');
598 input.set_mode(InputVimMode::Normal);
599 input.handle_input(&Event::Key(KeyEvent::new(
600 KeyCode::Char('h'),
601 KeyModifiers::empty(),
602 )));
603 assert_eq!(input.cursor(), 1);
604 input.handle_input(&Event::Key(KeyEvent::new(
605 KeyCode::Char('l'),
606 KeyModifiers::empty(),
607 )));
608 assert_eq!(input.cursor(), 2);
609 }
610
611 #[test]
612 fn vim_input_i_enters_insert() {
613 let mut input = Input::new();
614 input.set_vim_mode_enabled(true);
615 input.handle_input(&Event::Key(KeyEvent::new(
616 KeyCode::Char('i'),
617 KeyModifiers::empty(),
618 )));
619 assert_eq!(input.mode(), InputVimMode::Insert);
620 input.handle_input(&key_event(KeyCode::Char('x')));
621 assert_eq!(input.text(), "x");
622 }
623
624 #[test]
625 fn vim_input_esc_returns_to_normal() {
626 let mut input = Input::new();
627 input.set_vim_mode_enabled(true);
628 input.set_mode(InputVimMode::Insert);
629 input.handle_input(&key_event(KeyCode::Esc));
630 assert_eq!(input.mode(), InputVimMode::Normal);
631 }
632
633 #[test]
634 fn vim_input_x_deletes() {
635 let mut input = Input::new();
636 input.set_vim_mode_enabled(true);
637 input.set_mode(InputVimMode::Insert);
638 input.insert_char('a');
639 input.insert_char('b');
640 input.set_mode(InputVimMode::Normal);
641 input.move_cursor_home();
642 input.handle_input(&Event::Key(KeyEvent::new(
643 KeyCode::Char('x'),
644 KeyModifiers::empty(),
645 )));
646 assert_eq!(input.text(), "b");
647 }
648
649 #[test]
650 fn vim_input_a_appends() {
651 let mut input = Input::new();
652 input.set_vim_mode_enabled(true);
653 input.set_mode(InputVimMode::Insert);
654 input.insert_char('a');
655 input.set_mode(InputVimMode::Normal);
656 input.move_cursor_home();
657 input.handle_input(&Event::Key(KeyEvent::new(
658 KeyCode::Char('a'),
659 KeyModifiers::empty(),
660 )));
661 assert_eq!(input.mode(), InputVimMode::Insert);
662 input.handle_input(&key_event(KeyCode::Char('b')));
663 assert_eq!(input.text(), "ab");
664 }
665
666 #[test]
667 fn vim_input_0_and_dollar() {
668 let mut input = Input::new();
669 input.set_vim_mode_enabled(true);
670 input.set_mode(InputVimMode::Insert);
671 input.insert_char('a');
672 input.insert_char('b');
673 input.set_mode(InputVimMode::Normal);
674 input.move_cursor_end();
675 input.handle_input(&Event::Key(KeyEvent::new(
676 KeyCode::Char('0'),
677 KeyModifiers::empty(),
678 )));
679 assert_eq!(input.cursor(), 0);
680 input.handle_input(&Event::Key(KeyEvent::new(
681 KeyCode::Char('$'),
682 KeyModifiers::empty(),
683 )));
684 assert_eq!(input.cursor(), 2);
685 }
686
687 #[test]
688 fn vim_input_p_paste() {
689 let mut input = Input::new();
690 input.set_vim_mode_enabled(true);
691 input.set_mode(InputVimMode::Insert);
692 input.insert_char('a');
693 input.insert_char('b');
694 input.move_cursor_home();
695 input.handle_input(&ctrl_event('k'));
696 assert_eq!(input.text(), "");
697 input.set_mode(InputVimMode::Normal);
698 input.handle_input(&Event::Key(KeyEvent::new(
699 KeyCode::Char('p'),
700 KeyModifiers::empty(),
701 )));
702 assert_eq!(input.text(), "ab");
703 }
704
705 #[test]
706 fn vim_input_u_undo() {
707 let mut input = Input::new();
708 input.set_vim_mode_enabled(true);
709 input.set_mode(InputVimMode::Insert);
710 input.handle_input(&key_event(KeyCode::Char('a')));
711 input.handle_input(&key_event(KeyCode::Char('b')));
712 input.set_mode(InputVimMode::Normal);
713 input.handle_input(&Event::Key(KeyEvent::new(
714 KeyCode::Char('u'),
715 KeyModifiers::empty(),
716 )));
717 assert_eq!(input.text(), "a");
718 }
719
720 #[test]
724 fn input_respects_width_with_wide_chars() {
725 let mut input = Input::new();
726 input.set_text("中文测试");
727 let rendered = input.render(4).unwrap();
728 let vw = crate::utils::visible_width(&rendered.lines[0]);
729 assert!(
730 vw <= 4,
731 "input line exceeds width 4 (actual {}): {:?}",
732 vw,
733 rendered.lines[0]
734 );
735 }
736}