1use crossterm::{
2 event::{self, Event},
3 terminal::{self, EnterAlternateScreen, LeaveAlternateScreen},
4 execute,
5};
6use ratatui::{
7 backend::CrosstermBackend,
8 layout::{Constraint, Direction, Layout, Rect},
9 style::{Color, Modifier, Style},
10 text::{Line, Span},
11 widgets::{Block, Borders, Paragraph, Wrap},
12 Terminal,
13};
14use tui_textarea::{CursorMove, Input, Key, TextArea};
15use std::io;
16use std::fmt;
17
18#[derive(Debug, Clone, PartialEq, Eq)]
21enum Mode {
22 Normal,
23 Insert,
24 Visual,
25 Operator(char),
26 Command(String), Search(String), }
29
30impl fmt::Display for Mode {
31 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32 match self {
33 Self::Normal => write!(f, "NORMAL"),
34 Self::Insert => write!(f, "INSERT"),
35 Self::Visual => write!(f, "VISUAL"),
36 Self::Operator(c) => write!(f, "OPERATOR({})", c),
37 Self::Command(_) => write!(f, "COMMAND"),
38 Self::Search(_) => write!(f, "SEARCH"),
39 }
40 }
41}
42
43impl Mode {
44 fn cursor_style(&self) -> Style {
45 let color = match self {
46 Self::Normal => Color::Reset,
47 Self::Insert => Color::LightBlue,
48 Self::Visual => Color::LightYellow,
49 Self::Operator(_) => Color::LightGreen,
50 Self::Command(_) => Color::Reset,
51 Self::Search(_) => Color::Reset,
52 };
53 Style::default().fg(color).add_modifier(Modifier::REVERSED)
54 }
55
56 fn border_color(&self) -> Color {
57 match self {
58 Self::Normal => Color::DarkGray,
59 Self::Insert => Color::Cyan,
60 Self::Visual => Color::LightYellow,
61 Self::Operator(_) => Color::LightGreen,
62 Self::Command(_) => Color::DarkGray,
63 Self::Search(_) => Color::Magenta,
64 }
65 }
66}
67
68#[derive(Debug, Clone)]
72struct SearchMatch {
73 line: usize,
74 start: usize,
75 #[allow(dead_code)]
76 end: usize, }
78
79#[derive(Debug, Clone, Default)]
81struct SearchState {
82 pattern: String,
83 matches: Vec<SearchMatch>,
84 current_index: usize,
85}
86
87impl SearchState {
88 fn new() -> Self {
89 Self::default()
90 }
91
92 fn search(&mut self, pattern: &str, lines: &[String]) -> usize {
94 self.pattern = pattern.to_string();
95 self.matches.clear();
96 self.current_index = 0;
97
98 if pattern.is_empty() {
99 return 0;
100 }
101
102 for (line_idx, line) in lines.iter().enumerate() {
103 let mut start = 0;
104 while let Some(pos) = line[start..].find(pattern) {
105 let abs_start = start + pos;
106 self.matches.push(SearchMatch {
107 line: line_idx,
108 start: abs_start,
109 end: abs_start + pattern.len(),
110 });
111 start = abs_start + pattern.len();
112 if start >= line.len() {
113 break;
114 }
115 }
116 }
117
118 self.matches.len()
119 }
120
121 fn next_match(&mut self) -> Option<(usize, usize)> {
123 if self.matches.is_empty() {
124 return None;
125 }
126 let m = &self.matches[self.current_index];
127 let result = Some((m.line, m.start));
128 self.current_index = (self.current_index + 1) % self.matches.len();
129 result
130 }
131
132 fn prev_match(&mut self) -> Option<(usize, usize)> {
134 if self.matches.is_empty() {
135 return None;
136 }
137 if self.current_index == 0 {
138 self.current_index = self.matches.len() - 1;
139 } else {
140 self.current_index -= 1;
141 }
142 let m = &self.matches[self.current_index];
143 Some((m.line, m.start))
144 }
145
146 fn current_info(&self) -> (usize, usize) {
148 if self.matches.is_empty() {
149 (0, 0)
150 } else {
151 let display_idx = if self.current_index == 0 {
152 self.matches.len()
153 } else {
154 self.current_index
155 };
156 (display_idx, self.matches.len())
157 }
158 }
159}
160
161fn apply_search_highlight(
165 buf: &mut ratatui::buffer::Buffer,
166 area: Rect,
167 search: &SearchState,
168) {
169 if search.pattern.is_empty() || search.matches.is_empty() {
170 return;
171 }
172
173 let pattern = &search.pattern;
174
175 for row in area.top()..area.bottom() {
177 let content_start = area.left() + 3; let mut chars_with_pos: Vec<(char, u16)> = Vec::new();
179
180 for col in content_start..area.right() {
181 if let Some(cell) = buf.cell((col, row)) {
182 let symbol = cell.symbol();
183 for c in symbol.chars() {
184 chars_with_pos.push((c, col));
185 }
186 }
187 }
188
189 let pattern_chars: Vec<char> = pattern.chars().collect();
190 if pattern_chars.is_empty() {
191 continue;
192 }
193
194 let mut i = 0;
195 while i + pattern_chars.len() <= chars_with_pos.len() {
196 let is_match = pattern_chars.iter().enumerate().all(|(j, pc)| {
197 chars_with_pos.get(i + j).map(|(c, _)| c == pc).unwrap_or(false)
198 });
199
200 if is_match {
201 let style = Style::default()
203 .fg(Color::Red)
204 .add_modifier(Modifier::BOLD);
205
206 for j in 0..pattern_chars.len() {
207 if let Some((_, col)) = chars_with_pos.get(i + j) {
208 buf[(*col, row)].set_style(style);
209 }
210 }
211
212 i += pattern_chars.len();
213 } else {
214 i += 1;
215 }
216 }
217 }
218}
219
220fn jump_to_match(textarea: &mut TextArea, line: usize, col: usize) {
222 textarea.move_cursor(CursorMove::Jump(
223 line.try_into().unwrap_or(0),
224 col.try_into().unwrap_or(0),
225 ));
226}
227
228enum Transition {
231 Nop,
232 Mode(Mode),
233 Pending(Input),
234 Submit, Quit, TryQuit, Search(String), NextMatch, PrevMatch, }
241
242struct Vim {
244 mode: Mode,
245 pending: Input,
246 search: SearchState,
247}
248
249impl Vim {
250 fn new(mode: Mode) -> Self {
251 Self {
252 mode,
253 pending: Input::default(),
254 search: SearchState::new(),
255 }
256 }
257
258 fn with_pending(self, pending: Input) -> Self {
259 Self {
260 mode: self.mode,
261 pending,
262 search: self.search,
263 }
264 }
265
266 fn transition(&self, input: Input, textarea: &mut TextArea<'_>) -> Transition {
268 if input.key == Key::Null {
269 return Transition::Nop;
270 }
271
272 if input.ctrl && input.key == Key::Char('s') {
274 return Transition::Submit;
275 }
276
277 if input.ctrl && input.key == Key::Char('q') {
279 return Transition::Quit;
280 }
281
282 match &self.mode {
283 Mode::Command(cmd) => self.handle_command_mode(input, cmd),
284 Mode::Search(pattern) => self.handle_search_mode(input, pattern),
285 Mode::Insert => self.handle_insert_mode(input, textarea),
286 Mode::Normal | Mode::Visual | Mode::Operator(_) => {
287 self.handle_normal_visual_operator(input, textarea)
288 }
289 }
290 }
291
292 fn handle_insert_mode(&self, input: Input, textarea: &mut TextArea<'_>) -> Transition {
294 match input {
295 Input { key: Key::Esc, .. }
296 | Input {
297 key: Key::Char('c'),
298 ctrl: true,
299 ..
300 } => Transition::Mode(Mode::Normal),
301 input => {
302 textarea.input(input);
303 Transition::Mode(Mode::Insert)
304 }
305 }
306 }
307
308 fn handle_command_mode(&self, input: Input, cmd: &str) -> Transition {
310 match input.key {
311 Key::Esc => Transition::Mode(Mode::Normal),
312 Key::Enter => {
313 let cmd = cmd.trim();
315 match cmd {
316 "wq" | "x" => Transition::Submit,
317 "w" => Transition::Submit,
318 "q" => Transition::TryQuit, "q!" => Transition::Quit, _ => Transition::Mode(Mode::Normal), }
322 }
323 Key::Backspace => {
324 if cmd.is_empty() {
325 Transition::Mode(Mode::Normal)
326 } else {
327 let mut new_cmd = cmd.to_string();
328 new_cmd.pop();
329 Transition::Mode(Mode::Command(new_cmd))
330 }
331 }
332 Key::Char(c) => {
333 let mut new_cmd = cmd.to_string();
334 new_cmd.push(c);
335 Transition::Mode(Mode::Command(new_cmd))
336 }
337 _ => Transition::Nop,
338 }
339 }
340
341 fn handle_search_mode(&self, input: Input, pattern: &str) -> Transition {
343 match input.key {
344 Key::Esc => Transition::Mode(Mode::Normal),
345 Key::Enter => {
346 Transition::Search(pattern.to_string())
348 }
349 Key::Backspace => {
350 if pattern.is_empty() {
351 Transition::Mode(Mode::Normal)
352 } else {
353 let mut new_pattern = pattern.to_string();
354 new_pattern.pop();
355 Transition::Mode(Mode::Search(new_pattern))
356 }
357 }
358 Key::Char(c) => {
359 let mut new_pattern = pattern.to_string();
360 new_pattern.push(c);
361 Transition::Mode(Mode::Search(new_pattern))
362 }
363 _ => Transition::Nop,
364 }
365 }
366
367 fn handle_normal_visual_operator(
369 &self,
370 input: Input,
371 textarea: &mut TextArea<'_>,
372 ) -> Transition {
373 match input {
374 Input {
376 key: Key::Char(':'),
377 ctrl: false,
378 ..
379 } if self.mode == Mode::Normal => {
380 return Transition::Mode(Mode::Command(String::new()));
381 }
382 Input {
384 key: Key::Char('/'),
385 ctrl: false,
386 ..
387 } if self.mode == Mode::Normal => {
388 return Transition::Mode(Mode::Search(String::new()));
389 }
390 Input {
392 key: Key::Char('n'),
393 ctrl: false,
394 ..
395 } if self.mode == Mode::Normal => {
396 return Transition::NextMatch;
397 }
398 Input {
400 key: Key::Char('N'),
401 ctrl: false,
402 ..
403 } if self.mode == Mode::Normal => {
404 return Transition::PrevMatch;
405 }
406 Input {
408 key: Key::Char('h'),
409 ..
410 } => textarea.move_cursor(CursorMove::Back),
411 Input {
412 key: Key::Char('j'),
413 ..
414 } => textarea.move_cursor(CursorMove::Down),
415 Input {
416 key: Key::Char('k'),
417 ..
418 } => textarea.move_cursor(CursorMove::Up),
419 Input {
420 key: Key::Char('l'),
421 ..
422 } => textarea.move_cursor(CursorMove::Forward),
423 Input {
424 key: Key::Char('w'),
425 ..
426 } => textarea.move_cursor(CursorMove::WordForward),
427 Input {
428 key: Key::Char('e'),
429 ctrl: false,
430 ..
431 } => {
432 textarea.move_cursor(CursorMove::WordEnd);
433 if matches!(self.mode, Mode::Operator(_)) {
434 textarea.move_cursor(CursorMove::Forward);
435 }
436 }
437 Input {
438 key: Key::Char('b'),
439 ctrl: false,
440 ..
441 } => textarea.move_cursor(CursorMove::WordBack),
442 Input {
443 key: Key::Char('^' | '0'),
444 ..
445 } => textarea.move_cursor(CursorMove::Head),
446 Input {
447 key: Key::Char('$'),
448 ..
449 } => textarea.move_cursor(CursorMove::End),
450 Input {
452 key: Key::Char('D'),
453 ..
454 } => {
455 textarea.delete_line_by_end();
456 return Transition::Mode(Mode::Normal);
457 }
458 Input {
459 key: Key::Char('C'),
460 ..
461 } => {
462 textarea.delete_line_by_end();
463 textarea.cancel_selection();
464 return Transition::Mode(Mode::Insert);
465 }
466 Input {
467 key: Key::Char('p'),
468 ..
469 } => {
470 textarea.paste();
471 return Transition::Mode(Mode::Normal);
472 }
473 Input {
474 key: Key::Char('u'),
475 ctrl: false,
476 ..
477 } => {
478 textarea.undo();
479 return Transition::Mode(Mode::Normal);
480 }
481 Input {
482 key: Key::Char('r'),
483 ctrl: true,
484 ..
485 } => {
486 textarea.redo();
487 return Transition::Mode(Mode::Normal);
488 }
489 Input {
490 key: Key::Char('x'),
491 ..
492 } => {
493 textarea.delete_next_char();
494 return Transition::Mode(Mode::Normal);
495 }
496 Input {
498 key: Key::Char('i'),
499 ..
500 } => {
501 textarea.cancel_selection();
502 return Transition::Mode(Mode::Insert);
503 }
504 Input {
505 key: Key::Char('a'),
506 ctrl: false,
507 ..
508 } => {
509 textarea.cancel_selection();
510 textarea.move_cursor(CursorMove::Forward);
511 return Transition::Mode(Mode::Insert);
512 }
513 Input {
514 key: Key::Char('A'),
515 ..
516 } => {
517 textarea.cancel_selection();
518 textarea.move_cursor(CursorMove::End);
519 return Transition::Mode(Mode::Insert);
520 }
521 Input {
522 key: Key::Char('o'),
523 ..
524 } => {
525 textarea.move_cursor(CursorMove::End);
526 textarea.insert_newline();
527 return Transition::Mode(Mode::Insert);
528 }
529 Input {
530 key: Key::Char('O'),
531 ..
532 } => {
533 textarea.move_cursor(CursorMove::Head);
534 textarea.insert_newline();
535 textarea.move_cursor(CursorMove::Up);
536 return Transition::Mode(Mode::Insert);
537 }
538 Input {
539 key: Key::Char('I'),
540 ..
541 } => {
542 textarea.cancel_selection();
543 textarea.move_cursor(CursorMove::Head);
544 return Transition::Mode(Mode::Insert);
545 }
546 Input {
548 key: Key::Char('e'),
549 ctrl: true,
550 ..
551 } => textarea.scroll((1, 0)),
552 Input {
553 key: Key::Char('y'),
554 ctrl: true,
555 ..
556 } => textarea.scroll((-1, 0)),
557 Input {
558 key: Key::Char('d'),
559 ctrl: true,
560 ..
561 } => textarea.scroll(tui_textarea::Scrolling::HalfPageDown),
562 Input {
563 key: Key::Char('u'),
564 ctrl: true,
565 ..
566 } => textarea.scroll(tui_textarea::Scrolling::HalfPageUp),
567 Input {
568 key: Key::Char('f'),
569 ctrl: true,
570 ..
571 } => textarea.scroll(tui_textarea::Scrolling::PageDown),
572 Input {
573 key: Key::Char('b'),
574 ctrl: true,
575 ..
576 } => textarea.scroll(tui_textarea::Scrolling::PageUp),
577 Input {
579 key: Key::Char('v'),
580 ctrl: false,
581 ..
582 } if self.mode == Mode::Normal => {
583 textarea.start_selection();
584 return Transition::Mode(Mode::Visual);
585 }
586 Input {
587 key: Key::Char('V'),
588 ctrl: false,
589 ..
590 } if self.mode == Mode::Normal => {
591 textarea.move_cursor(CursorMove::Head);
592 textarea.start_selection();
593 textarea.move_cursor(CursorMove::End);
594 return Transition::Mode(Mode::Visual);
595 }
596 Input { key: Key::Esc, .. }
597 | Input {
598 key: Key::Char('v'),
599 ctrl: false,
600 ..
601 } if self.mode == Mode::Visual => {
602 textarea.cancel_selection();
603 return Transition::Mode(Mode::Normal);
604 }
605 Input {
607 key: Key::Char('g'),
608 ctrl: false,
609 ..
610 } if matches!(
611 self.pending,
612 Input {
613 key: Key::Char('g'),
614 ctrl: false,
615 ..
616 }
617 ) =>
618 {
619 textarea.move_cursor(CursorMove::Top);
620 }
621 Input {
623 key: Key::Char('G'),
624 ctrl: false,
625 ..
626 } => textarea.move_cursor(CursorMove::Bottom),
627 Input {
629 key: Key::Char(c),
630 ctrl: false,
631 ..
632 } if self.mode == Mode::Operator(c) => {
633 textarea.move_cursor(CursorMove::Head);
634 textarea.start_selection();
635 let cursor = textarea.cursor();
636 textarea.move_cursor(CursorMove::Down);
637 if cursor == textarea.cursor() {
638 textarea.move_cursor(CursorMove::End);
639 }
640 }
641 Input {
643 key: Key::Char(op @ ('y' | 'd' | 'c')),
644 ctrl: false,
645 ..
646 } if self.mode == Mode::Normal => {
647 textarea.start_selection();
648 return Transition::Mode(Mode::Operator(op));
649 }
650 Input {
652 key: Key::Char('y'),
653 ctrl: false,
654 ..
655 } if self.mode == Mode::Visual => {
656 textarea.move_cursor(CursorMove::Forward);
657 textarea.copy();
658 return Transition::Mode(Mode::Normal);
659 }
660 Input {
661 key: Key::Char('d'),
662 ctrl: false,
663 ..
664 } if self.mode == Mode::Visual => {
665 textarea.move_cursor(CursorMove::Forward);
666 textarea.cut();
667 return Transition::Mode(Mode::Normal);
668 }
669 Input {
670 key: Key::Char('c'),
671 ctrl: false,
672 ..
673 } if self.mode == Mode::Visual => {
674 textarea.move_cursor(CursorMove::Forward);
675 textarea.cut();
676 return Transition::Mode(Mode::Insert);
677 }
678 Input { key: Key::Esc, .. } if self.mode == Mode::Normal => {
680 return Transition::Nop;
681 }
682 Input { key: Key::Esc, .. } => {
684 textarea.cancel_selection();
685 return Transition::Mode(Mode::Normal);
686 }
687 input => return Transition::Pending(input),
689 }
690
691 match self.mode {
693 Mode::Operator('y') => {
694 textarea.copy();
695 Transition::Mode(Mode::Normal)
696 }
697 Mode::Operator('d') => {
698 textarea.cut();
699 Transition::Mode(Mode::Normal)
700 }
701 Mode::Operator('c') => {
702 textarea.cut();
703 Transition::Mode(Mode::Insert)
704 }
705 _ => Transition::Nop,
706 }
707 }
708}
709
710#[allow(dead_code)]
725pub fn open_multiline_editor(title: &str) -> io::Result<Option<String>> {
726 open_editor_internal(title, &[], Mode::Insert)
727}
728
729pub fn open_multiline_editor_with_content(title: &str, initial_lines: &[String]) -> io::Result<Option<String>> {
735 open_editor_internal(title, initial_lines, Mode::Normal)
736}
737
738fn open_editor_internal(
740 title: &str,
741 initial_lines: &[String],
742 initial_mode: Mode,
743) -> io::Result<Option<String>> {
744 terminal::enable_raw_mode()?;
746 let mut stdout = io::stdout();
747 execute!(stdout, EnterAlternateScreen)?;
748
749 let backend = CrosstermBackend::new(stdout);
750 let mut terminal = Terminal::new(backend)?;
751
752 let mut textarea = if initial_lines.is_empty() {
754 TextArea::default()
755 } else {
756 TextArea::new(initial_lines.to_vec())
757 };
758 textarea.set_block(make_block(title, &initial_mode));
759 textarea.set_cursor_style(initial_mode.cursor_style());
760 textarea.set_cursor_line_style(Style::default().add_modifier(Modifier::UNDERLINED));
761 textarea.set_line_number_style(Style::default().fg(Color::DarkGray));
762
763 if !initial_lines.is_empty() {
765 textarea.move_cursor(CursorMove::Bottom);
766 textarea.move_cursor(CursorMove::End);
767 }
768
769 let initial_snapshot: Vec<String> = textarea.lines().iter().map(|l| l.to_string()).collect();
771
772 let mut vim = Vim::new(initial_mode);
773 let result = run_editor_loop(&mut terminal, &mut textarea, &mut vim, title, &initial_snapshot);
774
775 terminal::disable_raw_mode()?;
777 execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
778
779 result
780}
781
782fn make_block<'a>(title: &str, mode: &Mode) -> Block<'a> {
784 Block::default()
785 .borders(Borders::ALL)
786 .title(format!(" {} ", title))
787 .border_style(Style::default().fg(mode.border_color()))
788}
789
790fn run_editor_loop(
792 terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
793 textarea: &mut TextArea,
794 vim: &mut Vim,
795 title: &str,
796 initial_snapshot: &[String],
797) -> io::Result<Option<String>> {
798 let mut unsaved_warning = false;
800 loop {
801 let mode = &vim.mode.clone();
802
803 let cursor_row = textarea.cursor().0;
805 let current_line_text: String = textarea.lines()
806 .get(cursor_row)
807 .map(|l| l.to_string())
808 .unwrap_or_default();
809 let display_width: usize = current_line_text.chars()
812 .map(|c| if c.is_ascii() { 1 } else { 2 })
813 .sum();
814
815 terminal.draw(|frame| {
817 let area_width = frame.area().width as usize;
818 let lnum_width = format!("{}", textarea.lines().len()).len() + 2 + 2;
820 let effective_width = area_width.saturating_sub(lnum_width);
821 let needs_preview = display_width > effective_width;
822
823 let preview_inner_width = area_width.saturating_sub(2).max(1);
826 let preview_height = if needs_preview {
827 let wrapped_lines = (display_width as f64 / preview_inner_width as f64).ceil() as u16;
828 wrapped_lines.saturating_add(2).clamp(3, 8)
830 } else {
831 0
832 };
833
834 let constraints = if needs_preview {
835 vec![
836 Constraint::Min(3), Constraint::Length(preview_height), Constraint::Length(2), ]
840 } else {
841 vec![
842 Constraint::Min(3), Constraint::Length(2), ]
845 };
846
847 let chunks = Layout::default()
848 .direction(Direction::Vertical)
849 .constraints(constraints)
850 .split(frame.area());
851
852 frame.render_widget(&*textarea, chunks[0]);
854
855 if !vim.search.pattern.is_empty() {
857 apply_search_highlight(frame.buffer_mut(), chunks[0], &vim.search);
858 }
859
860 if needs_preview {
861 let preview_block = Block::default()
863 .borders(Borders::ALL)
864 .title(format!(" 📖 第 {} 行预览 ", cursor_row + 1))
865 .title_style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD))
866 .border_style(Style::default().fg(Color::Cyan));
867 let preview = Paragraph::new(current_line_text.clone())
868 .block(preview_block)
869 .style(Style::default().fg(Color::White))
870 .wrap(Wrap { trim: false });
871 frame.render_widget(preview, chunks[1]);
872
873 let status_bar = build_status_bar(mode, textarea.lines().len(), &vim.search);
875 frame.render_widget(status_bar, chunks[2]);
876 } else {
877 let status_bar = build_status_bar(mode, textarea.lines().len(), &vim.search);
879 frame.render_widget(status_bar, chunks[1]);
880 }
881 })?;
882
883 if let Event::Key(key_event) = event::read()? {
885 if unsaved_warning {
887 unsaved_warning = false;
888 textarea.set_block(make_block(title, &vim.mode));
889 }
890
891 let input = Input::from(key_event);
892 match vim.transition(input, textarea) {
893 Transition::Mode(new_mode) if vim.mode != new_mode => {
894 textarea.set_block(make_block(title, &new_mode));
895 textarea.set_cursor_style(new_mode.cursor_style());
896 *vim = Vim::new(new_mode);
897 }
898 Transition::Nop | Transition::Mode(_) => {}
899 Transition::Pending(input) => {
900 let old = std::mem::replace(vim, Vim::new(Mode::Normal));
901 *vim = old.with_pending(input);
902 }
903 Transition::Submit => {
904 let lines = textarea.lines();
905 let text = lines.join("\n");
907 if text.is_empty() {
908 return Ok(None);
909 }
910 return Ok(Some(text));
911 }
912 Transition::TryQuit => {
913 let current_lines: Vec<String> = textarea.lines().iter().map(|l| l.to_string()).collect();
915 if current_lines == initial_snapshot {
916 return Ok(None);
918 } else {
919 unsaved_warning = true;
921 textarea.set_block(
922 Block::default()
923 .borders(Borders::ALL)
924 .title(" ⚠️ 有未保存的改动!使用 :q! 强制退出,或 :wq 保存退出 ")
925 .border_style(Style::default().fg(Color::LightRed))
926 );
927 *vim = Vim::new(Mode::Normal);
928 }
929 }
930 Transition::Quit => {
931 return Ok(None);
932 }
933 Transition::Search(pattern) => {
934 let lines: Vec<String> = textarea.lines().iter().map(|l| l.to_string()).collect();
936 let count = vim.search.search(&pattern, &lines);
937
938 if count > 0 {
940 if let Some((line, col)) = vim.search.next_match() {
941 jump_to_match(textarea, line, col);
943 }
944 }
945
946 *vim = Vim::new(Mode::Normal);
947 vim.search = SearchState::new();
948 vim.search.search(&pattern, &lines);
949 }
950 Transition::NextMatch => {
951 if let Some((line, col)) = vim.search.next_match() {
952 jump_to_match(textarea, line, col);
953 }
954 }
955 Transition::PrevMatch => {
956 if let Some((line, col)) = vim.search.prev_match() {
957 jump_to_match(textarea, line, col);
958 }
959 }
960 }
961 }
962 }
963}
964
965fn build_status_bar(mode: &Mode, line_count: usize, search: &SearchState) -> Paragraph<'static> {
967 let mut spans = vec![];
968
969 let (mode_text, mode_bg) = match mode {
971 Mode::Insert => (" INSERT ", Color::LightBlue),
972 Mode::Normal => (" NORMAL ", Color::DarkGray),
973 Mode::Visual => (" VISUAL ", Color::LightYellow),
974 Mode::Operator(c) => {
975 let s: &'static str = match c {
977 'y' => " YANK ",
978 'd' => " DELETE ",
979 'c' => " CHANGE ",
980 _ => " OP ",
981 };
982 (s, Color::LightGreen)
983 }
984 Mode::Command(cmd) => {
985 let cmd_display = format!(":{}", cmd);
987 return Paragraph::new(Line::from(vec![
988 Span::styled(
989 " COMMAND ",
990 Style::default().fg(Color::Black).bg(Color::LightMagenta),
991 ),
992 Span::raw(" "),
993 Span::styled(
994 cmd_display,
995 Style::default().fg(Color::White).add_modifier(Modifier::BOLD),
996 ),
997 Span::styled("█", Style::default().fg(Color::White)),
998 ]));
999 }
1000 Mode::Search(pattern) => {
1001 let search_display = format!("/{}", pattern);
1003 return Paragraph::new(Line::from(vec![
1004 Span::styled(
1005 " SEARCH ",
1006 Style::default().fg(Color::Black).bg(Color::Magenta),
1007 ),
1008 Span::raw(" "),
1009 Span::styled(
1010 search_display,
1011 Style::default().fg(Color::White).add_modifier(Modifier::BOLD),
1012 ),
1013 Span::styled("█", Style::default().fg(Color::White)),
1014 ]));
1015 }
1016 };
1017
1018 spans.push(Span::styled(
1019 mode_text,
1020 Style::default().fg(Color::Black).bg(mode_bg),
1021 ));
1022 spans.push(Span::raw(" "));
1023
1024 match mode {
1026 Mode::Insert => {
1027 spans.push(Span::styled(
1028 " Ctrl+S ",
1029 Style::default().fg(Color::Black).bg(Color::Green),
1030 ));
1031 spans.push(Span::raw(" 提交 "));
1032 spans.push(Span::styled(
1033 " Ctrl+Q ",
1034 Style::default().fg(Color::Black).bg(Color::Red),
1035 ));
1036 spans.push(Span::raw(" 取消 "));
1037 spans.push(Span::styled(
1038 " Esc ",
1039 Style::default().fg(Color::Black).bg(Color::Yellow),
1040 ));
1041 spans.push(Span::raw(" Normal "));
1042 }
1043 Mode::Normal => {
1044 spans.push(Span::styled(
1045 " :wq ",
1046 Style::default().fg(Color::Black).bg(Color::Green),
1047 ));
1048 spans.push(Span::raw(" 提交 "));
1049 spans.push(Span::styled(
1050 " / ",
1051 Style::default().fg(Color::Black).bg(Color::Magenta),
1052 ));
1053 spans.push(Span::raw(" 搜索 "));
1054 spans.push(Span::styled(
1055 " n/N ",
1056 Style::default().fg(Color::Black).bg(Color::Cyan),
1057 ));
1058 spans.push(Span::raw(" 下/上 "));
1059 spans.push(Span::styled(
1060 " i ",
1061 Style::default().fg(Color::Black).bg(Color::Cyan),
1062 ));
1063 spans.push(Span::raw(" 编辑 "));
1064 }
1065 Mode::Visual => {
1066 spans.push(Span::styled(
1067 " y ",
1068 Style::default().fg(Color::Black).bg(Color::Green),
1069 ));
1070 spans.push(Span::raw(" 复制 "));
1071 spans.push(Span::styled(
1072 " d ",
1073 Style::default().fg(Color::Black).bg(Color::Red),
1074 ));
1075 spans.push(Span::raw(" 删除 "));
1076 spans.push(Span::styled(
1077 " Esc ",
1078 Style::default().fg(Color::Black).bg(Color::Yellow),
1079 ));
1080 spans.push(Span::raw(" 取消 "));
1081 }
1082 _ => {}
1083 }
1084
1085 spans.push(Span::styled(
1087 format!(" {} 行 ", line_count),
1088 Style::default().fg(Color::DarkGray),
1089 ));
1090
1091 if !search.pattern.is_empty() {
1093 let (current, total) = search.current_info();
1094 spans.push(Span::raw(" "));
1095 spans.push(Span::styled(
1096 format!(" [{}: {}/{}] ", search.pattern, current, total),
1097 Style::default().fg(Color::Magenta),
1098 ));
1099 }
1100
1101 Paragraph::new(Line::from(spans))
1102}