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 display_width_of(s: &str) -> usize {
792 s.chars().map(|c| if c.is_ascii() { 1 } else { 2 }).sum()
793}
794
795fn run_editor_loop(
797 terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
798 textarea: &mut TextArea,
799 vim: &mut Vim,
800 title: &str,
801 initial_snapshot: &[String],
802) -> io::Result<Option<String>> {
803 let mut unsaved_warning = false;
805 loop {
806 let mode = &vim.mode.clone();
807
808 let cursor_row = textarea.cursor().0;
810 let current_line_text: String = textarea.lines()
811 .get(cursor_row)
812 .map(|l| l.to_string())
813 .unwrap_or_default();
814 let display_width: usize = display_width_of(¤t_line_text);
816
817 terminal.draw(|frame| {
819 let area_width = frame.area().width as usize;
820 let lnum_width = format!("{}", textarea.lines().len()).len() + 2 + 2;
822 let effective_width = area_width.saturating_sub(lnum_width);
823 let needs_preview = display_width > effective_width;
824
825 let preview_inner_width = area_width.saturating_sub(2).max(1);
828 let preview_height = if needs_preview {
829 let wrapped_lines = (display_width as f64 / preview_inner_width as f64).ceil() as u16;
830 wrapped_lines.saturating_add(2).clamp(3, 8)
832 } else {
833 0
834 };
835
836 let constraints = if needs_preview {
837 vec![
838 Constraint::Min(3), Constraint::Length(preview_height), Constraint::Length(2), ]
842 } else {
843 vec![
844 Constraint::Min(3), Constraint::Length(2), ]
847 };
848
849 let chunks = Layout::default()
850 .direction(Direction::Vertical)
851 .constraints(constraints)
852 .split(frame.area());
853
854 frame.render_widget(&*textarea, chunks[0]);
856
857 if !vim.search.pattern.is_empty() {
859 apply_search_highlight(frame.buffer_mut(), chunks[0], &vim.search);
860 }
861
862 if needs_preview {
863 let preview_block = Block::default()
865 .borders(Borders::ALL)
866 .title(format!(" 📖 第 {} 行预览 ", cursor_row + 1))
867 .title_style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD))
868 .border_style(Style::default().fg(Color::Cyan));
869 let preview = Paragraph::new(current_line_text.clone())
870 .block(preview_block)
871 .style(Style::default().fg(Color::White))
872 .wrap(Wrap { trim: false });
873 frame.render_widget(preview, chunks[1]);
874
875 let status_bar = build_status_bar(mode, textarea.lines().len(), &vim.search);
877 frame.render_widget(status_bar, chunks[2]);
878 } else {
879 let status_bar = build_status_bar(mode, textarea.lines().len(), &vim.search);
881 frame.render_widget(status_bar, chunks[1]);
882 }
883 })?;
884
885 if let Event::Key(key_event) = event::read()? {
887 if unsaved_warning {
889 unsaved_warning = false;
890 textarea.set_block(make_block(title, &vim.mode));
891 }
892
893 let input = Input::from(key_event);
894 match vim.transition(input, textarea) {
895 Transition::Mode(new_mode) if vim.mode != new_mode => {
896 textarea.set_block(make_block(title, &new_mode));
897 textarea.set_cursor_style(new_mode.cursor_style());
898 *vim = Vim::new(new_mode);
899 }
900 Transition::Nop | Transition::Mode(_) => {}
901 Transition::Pending(input) => {
902 let old = std::mem::replace(vim, Vim::new(Mode::Normal));
903 *vim = old.with_pending(input);
904 }
905 Transition::Submit => {
906 let lines = textarea.lines();
907 let text = lines.join("\n");
909 if text.is_empty() {
910 return Ok(None);
911 }
912 return Ok(Some(text));
913 }
914 Transition::TryQuit => {
915 let current_lines: Vec<String> = textarea.lines().iter().map(|l| l.to_string()).collect();
917 if current_lines == initial_snapshot {
918 return Ok(None);
920 } else {
921 unsaved_warning = true;
923 textarea.set_block(
924 Block::default()
925 .borders(Borders::ALL)
926 .title(" ⚠️ 有未保存的改动!使用 :q! 强制退出,或 :wq 保存退出 ")
927 .border_style(Style::default().fg(Color::LightRed))
928 );
929 *vim = Vim::new(Mode::Normal);
930 }
931 }
932 Transition::Quit => {
933 return Ok(None);
934 }
935 Transition::Search(pattern) => {
936 let lines: Vec<String> = textarea.lines().iter().map(|l| l.to_string()).collect();
938 let count = vim.search.search(&pattern, &lines);
939
940 if count > 0 {
942 if let Some((line, col)) = vim.search.next_match() {
943 jump_to_match(textarea, line, col);
945 }
946 }
947
948 *vim = Vim::new(Mode::Normal);
949 vim.search = SearchState::new();
950 vim.search.search(&pattern, &lines);
951 }
952 Transition::NextMatch => {
953 if let Some((line, col)) = vim.search.next_match() {
954 jump_to_match(textarea, line, col);
955 }
956 }
957 Transition::PrevMatch => {
958 if let Some((line, col)) = vim.search.prev_match() {
959 jump_to_match(textarea, line, col);
960 }
961 }
962 }
963 }
964 }
965}
966
967fn build_status_bar(mode: &Mode, line_count: usize, search: &SearchState) -> Paragraph<'static> {
969 let mut spans = vec![];
970
971 let (mode_text, mode_bg) = match mode {
973 Mode::Insert => (" INSERT ", Color::LightBlue),
974 Mode::Normal => (" NORMAL ", Color::DarkGray),
975 Mode::Visual => (" VISUAL ", Color::LightYellow),
976 Mode::Operator(c) => {
977 let s: &'static str = match c {
979 'y' => " YANK ",
980 'd' => " DELETE ",
981 'c' => " CHANGE ",
982 _ => " OP ",
983 };
984 (s, Color::LightGreen)
985 }
986 Mode::Command(cmd) => {
987 let cmd_display = format!(":{}", cmd);
989 return Paragraph::new(Line::from(vec![
990 Span::styled(
991 " COMMAND ",
992 Style::default().fg(Color::Black).bg(Color::LightMagenta),
993 ),
994 Span::raw(" "),
995 Span::styled(
996 cmd_display,
997 Style::default().fg(Color::White).add_modifier(Modifier::BOLD),
998 ),
999 Span::styled("█", Style::default().fg(Color::White)),
1000 ]));
1001 }
1002 Mode::Search(pattern) => {
1003 let search_display = format!("/{}", pattern);
1005 return Paragraph::new(Line::from(vec![
1006 Span::styled(
1007 " SEARCH ",
1008 Style::default().fg(Color::Black).bg(Color::Magenta),
1009 ),
1010 Span::raw(" "),
1011 Span::styled(
1012 search_display,
1013 Style::default().fg(Color::White).add_modifier(Modifier::BOLD),
1014 ),
1015 Span::styled("█", Style::default().fg(Color::White)),
1016 ]));
1017 }
1018 };
1019
1020 spans.push(Span::styled(
1021 mode_text,
1022 Style::default().fg(Color::Black).bg(mode_bg),
1023 ));
1024 spans.push(Span::raw(" "));
1025
1026 match mode {
1028 Mode::Insert => {
1029 spans.push(Span::styled(
1030 " Ctrl+S ",
1031 Style::default().fg(Color::Black).bg(Color::Green),
1032 ));
1033 spans.push(Span::raw(" 提交 "));
1034 spans.push(Span::styled(
1035 " Ctrl+Q ",
1036 Style::default().fg(Color::Black).bg(Color::Red),
1037 ));
1038 spans.push(Span::raw(" 取消 "));
1039 spans.push(Span::styled(
1040 " Esc ",
1041 Style::default().fg(Color::Black).bg(Color::Yellow),
1042 ));
1043 spans.push(Span::raw(" Normal "));
1044 }
1045 Mode::Normal => {
1046 spans.push(Span::styled(
1047 " :wq ",
1048 Style::default().fg(Color::Black).bg(Color::Green),
1049 ));
1050 spans.push(Span::raw(" 提交 "));
1051 spans.push(Span::styled(
1052 " / ",
1053 Style::default().fg(Color::Black).bg(Color::Magenta),
1054 ));
1055 spans.push(Span::raw(" 搜索 "));
1056 spans.push(Span::styled(
1057 " n/N ",
1058 Style::default().fg(Color::Black).bg(Color::Cyan),
1059 ));
1060 spans.push(Span::raw(" 下/上 "));
1061 spans.push(Span::styled(
1062 " i ",
1063 Style::default().fg(Color::Black).bg(Color::Cyan),
1064 ));
1065 spans.push(Span::raw(" 编辑 "));
1066 }
1067 Mode::Visual => {
1068 spans.push(Span::styled(
1069 " y ",
1070 Style::default().fg(Color::Black).bg(Color::Green),
1071 ));
1072 spans.push(Span::raw(" 复制 "));
1073 spans.push(Span::styled(
1074 " d ",
1075 Style::default().fg(Color::Black).bg(Color::Red),
1076 ));
1077 spans.push(Span::raw(" 删除 "));
1078 spans.push(Span::styled(
1079 " Esc ",
1080 Style::default().fg(Color::Black).bg(Color::Yellow),
1081 ));
1082 spans.push(Span::raw(" 取消 "));
1083 }
1084 _ => {}
1085 }
1086
1087 spans.push(Span::styled(
1089 format!(" {} 行 ", line_count),
1090 Style::default().fg(Color::DarkGray),
1091 ));
1092
1093 if !search.pattern.is_empty() {
1095 let (current, total) = search.current_info();
1096 spans.push(Span::raw(" "));
1097 spans.push(Span::styled(
1098 format!(" [{}: {}/{}] ", search.pattern, current, total),
1099 Style::default().fg(Color::Magenta),
1100 ));
1101 }
1102
1103 Paragraph::new(Line::from(spans))
1104}