Skip to main content

j_cli/tui/
editor.rs

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},
12    Terminal,
13};
14use tui_textarea::{CursorMove, Input, Key, TextArea};
15use std::io;
16use std::fmt;
17
18// ========== Vim 模式定义 ==========
19
20#[derive(Debug, Clone, PartialEq, Eq)]
21enum Mode {
22    Normal,
23    Insert,
24    Visual,
25    Operator(char),
26    Command(String), // 命令行模式,存储输入的命令字符串
27    Search(String),  // 搜索模式,存储输入的搜索词
28}
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// ========== 搜索状态 ==========
69
70/// 搜索匹配结果
71#[derive(Debug, Clone)]
72struct SearchMatch {
73    line: usize,
74    start: usize,
75    #[allow(dead_code)]
76    end: usize, // 保留用于将来可能的高亮增强
77}
78
79/// 搜索状态管理
80#[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    /// 执行搜索,返回匹配数量
93    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    /// 跳转到下一个匹配,返回 (line, column)
122    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    /// 跳转到上一个匹配,返回 (line, column)
133    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    /// 获取当前匹配信息(用于状态栏显示)
147    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
161// ========== 搜索高亮函数 ==========
162
163/// 应用搜索高亮到 buffer(直接修改 buffer 样式)
164fn 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    // 遍历 buffer 查找 pattern
176    for row in area.top()..area.bottom() {
177        let content_start = area.left() + 3; // 跳过行号
178        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                // 匹配文字用红色显示
202                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
220/// 跳转到指定行和列
221fn 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
228// ========== 编辑器状态转换 ==========
229
230enum Transition {
231    Nop,
232    Mode(Mode),
233    Pending(Input),
234    Submit,     // 提交内容
235    Quit,       // 强制取消退出(:q! / Ctrl+Q)
236    TryQuit,    // 尝试退出,若有改动则拒绝(:q)
237    Search(String), // 执行搜索
238    NextMatch,  // 跳转到下一个匹配
239    PrevMatch,  // 跳转到上一个匹配
240}
241
242/// Vim 状态机
243struct 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    /// 处理 Normal/Visual/Operator 模式的按键
267    fn transition(&self, input: Input, textarea: &mut TextArea<'_>) -> Transition {
268        if input.key == Key::Null {
269            return Transition::Nop;
270        }
271
272        // 任何模式下 Ctrl+S → 提交
273        if input.ctrl && input.key == Key::Char('s') {
274            return Transition::Submit;
275        }
276
277        // 任何模式下 Ctrl+Q → 强制取消退出
278        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    /// Insert 模式:Esc 回到 Normal,其他交给 textarea 默认处理
293    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    /// Command 模式:处理 :wq, :w, :q, :q! 等命令
309    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                // 执行命令
314                let cmd = cmd.trim();
315                match cmd {
316                    "wq" | "x" => Transition::Submit,
317                    "w" => Transition::Submit,
318                    "q" => Transition::TryQuit,   // 有改动时拒绝退出
319                    "q!" => Transition::Quit,      // 强制退出
320                    _ => Transition::Mode(Mode::Normal), // 未知命令,回到 Normal
321                }
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    /// Search 模式:处理 /pattern 输入
342    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                // 执行搜索
347                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    /// Normal / Visual / Operator 模式的 vim 按键处理
368    fn handle_normal_visual_operator(
369        &self,
370        input: Input,
371        textarea: &mut TextArea<'_>,
372    ) -> Transition {
373        match input {
374            // : 进入命令模式
375            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            // / 进入搜索模式
383            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            // n 跳转到下一个匹配
391            Input {
392                key: Key::Char('n'),
393                ctrl: false,
394                ..
395            } if self.mode == Mode::Normal => {
396                return Transition::NextMatch;
397            }
398            // N 跳转到上一个匹配
399            Input {
400                key: Key::Char('N'),
401                ctrl: false,
402                ..
403            } if self.mode == Mode::Normal => {
404                return Transition::PrevMatch;
405            }
406            // 移动
407            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            // 删除 / 修改
451            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            // 进入 Insert 模式
497            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            // 滚动
547            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            // Visual 模式
578            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            // gg → 跳到开头
606            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            // G → 跳到结尾
622            Input {
623                key: Key::Char('G'),
624                ctrl: false,
625                ..
626            } => textarea.move_cursor(CursorMove::Bottom),
627            // Operator 重复(yy, dd, cc)
628            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            // 进入 Operator 模式(y/d/c)
642            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            // Visual 模式下的 y/d/c
651            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            // Esc 在 Normal 模式下不退出(vim 标准行为)
679            Input { key: Key::Esc, .. } if self.mode == Mode::Normal => {
680                return Transition::Nop;
681            }
682            // Esc 在 Operator 模式取消
683            Input { key: Key::Esc, .. } => {
684                textarea.cancel_selection();
685                return Transition::Mode(Mode::Normal);
686            }
687            // 其他未匹配的按键 → pending(用于 gg 等序列)
688            input => return Transition::Pending(input),
689        }
690
691        // 处理 pending operator
692        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// ========== 公共 API ==========
711
712/// 打开全屏多行编辑器(vim 模式),返回用户输入的文本内容
713///
714/// 操作方式:
715/// - 默认进入 INSERT 模式,可直接输入
716/// - Esc: 回到 NORMAL 模式
717/// - `:wq` / `:w` / `:x`: 提交内容
718/// - `:q` / `:q!`: 取消退出
719/// - `/pattern`: 搜索 pattern
720/// - `n` / `N`: 跳转到下一个/上一个匹配
721/// - Ctrl+S: 任何模式下快速提交
722///
723/// 返回 Some(text) 表示提交,None 表示取消
724#[allow(dead_code)]
725pub fn open_multiline_editor(title: &str) -> io::Result<Option<String>> {
726    open_editor_internal(title, &[], Mode::Insert)
727}
728
729/// 打开全屏多行编辑器,带有预填充内容,默认 NORMAL 模式
730///
731/// - `initial_lines`: 预填充到编辑区的行(如历史日报 + 日期前缀)
732///
733/// 返回 Some(text) 表示提交,None 表示取消
734pub 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
738/// 内部统一入口:初始化终端 + 编辑区 + 主循环
739fn open_editor_internal(
740    title: &str,
741    initial_lines: &[String],
742    initial_mode: Mode,
743) -> io::Result<Option<String>> {
744    // 进入终端原始模式
745    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    // 初始化文本编辑区
753    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    // 如果有预填内容,光标跳到最后一行末尾
764    if !initial_lines.is_empty() {
765        textarea.move_cursor(CursorMove::Bottom);
766        textarea.move_cursor(CursorMove::End);
767    }
768
769    // 记录初始内容的快照,用于判断是否有实际改动
770    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    // 恢复终端状态
776    terminal::disable_raw_mode()?;
777    execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
778
779    result
780}
781
782/// 构造编辑区的 Block(根据模式变色)
783fn 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
790/// 编辑器主循环
791fn 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    // 是否显示 "有未保存改动" 的提示(下次按键后清除)
799    let mut unsaved_warning = false;
800    loop {
801        let mode = &vim.mode.clone();
802
803        // 绘制界面
804        terminal.draw(|frame| {
805            let chunks = Layout::default()
806                .direction(Direction::Vertical)
807                .constraints([
808                    Constraint::Min(3),   // 编辑区
809                    Constraint::Length(2), // 状态栏
810                ])
811                .split(frame.area());
812
813            // 渲染编辑区
814            frame.render_widget(&*textarea, chunks[0]);
815
816            // 在 TextArea 渲染后,应用搜索高亮
817            if !vim.search.pattern.is_empty() {
818                apply_search_highlight(frame.buffer_mut(), chunks[0], &vim.search);
819            }
820
821            // 渲染状态栏
822            let status_bar = build_status_bar(mode, textarea.lines().len(), &vim.search);
823            frame.render_widget(status_bar, chunks[1]);
824        })?;
825
826        // 处理输入事件
827        if let Event::Key(key_event) = event::read()? {
828            // 清除上次的警告提示
829            if unsaved_warning {
830                unsaved_warning = false;
831                textarea.set_block(make_block(title, &vim.mode));
832            }
833
834            let input = Input::from(key_event);
835            match vim.transition(input, textarea) {
836                Transition::Mode(new_mode) if vim.mode != new_mode => {
837                    textarea.set_block(make_block(title, &new_mode));
838                    textarea.set_cursor_style(new_mode.cursor_style());
839                    *vim = Vim::new(new_mode);
840                }
841                Transition::Nop | Transition::Mode(_) => {}
842                Transition::Pending(input) => {
843                    let old = std::mem::replace(vim, Vim::new(Mode::Normal));
844                    *vim = old.with_pending(input);
845                }
846                Transition::Submit => {
847                    let lines = textarea.lines();
848                    // 不使用 trim(),保留每行的原始缩进
849                    let text = lines.join("\n");
850                    if text.is_empty() {
851                        return Ok(None);
852                    }
853                    return Ok(Some(text));
854                }
855                Transition::TryQuit => {
856                    // :q — 检查是否有实际改动
857                    let current_lines: Vec<String> = textarea.lines().iter().map(|l| l.to_string()).collect();
858                    if current_lines == initial_snapshot {
859                        // 无改动,直接退出
860                        return Ok(None);
861                    } else {
862                        // 有改动,拒绝退出并提示
863                        unsaved_warning = true;
864                        textarea.set_block(
865                            Block::default()
866                                .borders(Borders::ALL)
867                                .title(" ⚠️ 有未保存的改动!使用 :q! 强制退出,或 :wq 保存退出 ")
868                                .border_style(Style::default().fg(Color::LightRed))
869                        );
870                        *vim = Vim::new(Mode::Normal);
871                    }
872                }
873                Transition::Quit => {
874                    return Ok(None);
875                }
876                Transition::Search(pattern) => {
877                    // 执行搜索
878                    let lines: Vec<String> = textarea.lines().iter().map(|l| l.to_string()).collect();
879                    let count = vim.search.search(&pattern, &lines);
880                    
881                    // 跳转到第一个匹配
882                    if count > 0 {
883                        if let Some((line, col)) = vim.search.next_match() {
884                            // 移动光标到匹配位置
885                            jump_to_match(textarea, line, col);
886                        }
887                    }
888                    
889                    *vim = Vim::new(Mode::Normal);
890                    vim.search = SearchState::new();
891                    vim.search.search(&pattern, &lines);
892                }
893                Transition::NextMatch => {
894                    if let Some((line, col)) = vim.search.next_match() {
895                        jump_to_match(textarea, line, col);
896                    }
897                }
898                Transition::PrevMatch => {
899                    if let Some((line, col)) = vim.search.prev_match() {
900                        jump_to_match(textarea, line, col);
901                    }
902                }
903            }
904        }
905    }
906}
907
908/// 构建底部状态栏
909fn build_status_bar(mode: &Mode, line_count: usize, search: &SearchState) -> Paragraph<'static> {
910    let mut spans = vec![];
911
912    // 模式标签
913    let (mode_text, mode_bg) = match mode {
914        Mode::Insert => (" INSERT ", Color::LightBlue),
915        Mode::Normal => (" NORMAL ", Color::DarkGray),
916        Mode::Visual => (" VISUAL ", Color::LightYellow),
917        Mode::Operator(c) => {
918            // 这里需要 'static,用 leaked string
919            let s: &'static str = match c {
920                'y' => " YANK ",
921                'd' => " DELETE ",
922                'c' => " CHANGE ",
923                _ => " OP ",
924            };
925            (s, Color::LightGreen)
926        }
927        Mode::Command(cmd) => {
928            // 命令模式特殊处理:直接显示命令行
929            let cmd_display = format!(":{}", cmd);
930            return Paragraph::new(Line::from(vec![
931                Span::styled(
932                    " COMMAND ",
933                    Style::default().fg(Color::Black).bg(Color::LightMagenta),
934                ),
935                Span::raw(" "),
936                Span::styled(
937                    cmd_display,
938                    Style::default().fg(Color::White).add_modifier(Modifier::BOLD),
939                ),
940                Span::styled("█", Style::default().fg(Color::White)),
941            ]));
942        }
943        Mode::Search(pattern) => {
944            // 搜索模式特殊处理:直接显示搜索词
945            let search_display = format!("/{}", pattern);
946            return Paragraph::new(Line::from(vec![
947                Span::styled(
948                    " SEARCH ",
949                    Style::default().fg(Color::Black).bg(Color::Magenta),
950                ),
951                Span::raw(" "),
952                Span::styled(
953                    search_display,
954                    Style::default().fg(Color::White).add_modifier(Modifier::BOLD),
955                ),
956                Span::styled("█", Style::default().fg(Color::White)),
957            ]));
958        }
959    };
960
961    spans.push(Span::styled(
962        mode_text,
963        Style::default().fg(Color::Black).bg(mode_bg),
964    ));
965    spans.push(Span::raw("  "));
966
967    // 快捷键提示
968    match mode {
969        Mode::Insert => {
970            spans.push(Span::styled(
971                " Ctrl+S ",
972                Style::default().fg(Color::Black).bg(Color::Green),
973            ));
974            spans.push(Span::raw(" 提交  "));
975            spans.push(Span::styled(
976                " Ctrl+Q ",
977                Style::default().fg(Color::Black).bg(Color::Red),
978            ));
979            spans.push(Span::raw(" 取消  "));
980            spans.push(Span::styled(
981                " Esc ",
982                Style::default().fg(Color::Black).bg(Color::Yellow),
983            ));
984            spans.push(Span::raw(" Normal  "));
985        }
986        Mode::Normal => {
987            spans.push(Span::styled(
988                " :wq ",
989                Style::default().fg(Color::Black).bg(Color::Green),
990            ));
991            spans.push(Span::raw(" 提交  "));
992            spans.push(Span::styled(
993                " / ",
994                Style::default().fg(Color::Black).bg(Color::Magenta),
995            ));
996            spans.push(Span::raw(" 搜索  "));
997            spans.push(Span::styled(
998                " n/N ",
999                Style::default().fg(Color::Black).bg(Color::Cyan),
1000            ));
1001            spans.push(Span::raw(" 下/上  "));
1002            spans.push(Span::styled(
1003                " i ",
1004                Style::default().fg(Color::Black).bg(Color::Cyan),
1005            ));
1006            spans.push(Span::raw(" 编辑  "));
1007        }
1008        Mode::Visual => {
1009            spans.push(Span::styled(
1010                " y ",
1011                Style::default().fg(Color::Black).bg(Color::Green),
1012            ));
1013            spans.push(Span::raw(" 复制  "));
1014            spans.push(Span::styled(
1015                " d ",
1016                Style::default().fg(Color::Black).bg(Color::Red),
1017            ));
1018            spans.push(Span::raw(" 删除  "));
1019            spans.push(Span::styled(
1020                " Esc ",
1021                Style::default().fg(Color::Black).bg(Color::Yellow),
1022            ));
1023            spans.push(Span::raw(" 取消  "));
1024        }
1025        _ => {}
1026    }
1027
1028    // 行数
1029    spans.push(Span::styled(
1030        format!(" {} 行 ", line_count),
1031        Style::default().fg(Color::DarkGray),
1032    ));
1033
1034    // 搜索匹配信息
1035    if !search.pattern.is_empty() {
1036        let (current, total) = search.current_info();
1037        spans.push(Span::raw("  "));
1038        spans.push(Span::styled(
1039            format!(" [{}: {}/{}] ", search.pattern, current, total),
1040            Style::default().fg(Color::Magenta),
1041        ));
1042    }
1043
1044    Paragraph::new(Line::from(spans))
1045}