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