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, Wrap},
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        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        // 判断当前行是否超过终端宽度,需要显示预览区
810        // 使用 unicode 字符宽度来准确计算(中文字符占 2 列)
811        let display_width: usize = current_line_text.chars()
812            .map(|c| if c.is_ascii() { 1 } else { 2 })
813            .sum();
814
815        // 绘制界面
816        terminal.draw(|frame| {
817            let area_width = frame.area().width as usize;
818            // 预留行号宽度(行号位数 + 2 个边距)+ 边框宽度 2
819            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            // 动态计算预览区高度:根据文本实际需要的行数
824            // 预览区内部可用宽度 = 终端宽度 - 左右边框各 1
825            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                // 预览区高度 = wrap 后行数 + 2(边框),最少 3 行,最多 8 行
829                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),                    // 编辑区
837                    Constraint::Length(preview_height),     // 当前行预览区
838                    Constraint::Length(2),                  // 状态栏
839                ]
840            } else {
841                vec![
842                    Constraint::Min(3),   // 编辑区
843                    Constraint::Length(2), // 状态栏
844                ]
845            };
846
847            let chunks = Layout::default()
848                .direction(Direction::Vertical)
849                .constraints(constraints)
850                .split(frame.area());
851
852            // 渲染编辑区
853            frame.render_widget(&*textarea, chunks[0]);
854
855            // 在 TextArea 渲染后,应用搜索高亮
856            if !vim.search.pattern.is_empty() {
857                apply_search_highlight(frame.buffer_mut(), chunks[0], &vim.search);
858            }
859
860            if needs_preview {
861                // 渲染当前行预览区(带 wrap)
862                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                // 渲染状态栏
874                let status_bar = build_status_bar(mode, textarea.lines().len(), &vim.search);
875                frame.render_widget(status_bar, chunks[2]);
876            } else {
877                // 渲染状态栏
878                let status_bar = build_status_bar(mode, textarea.lines().len(), &vim.search);
879                frame.render_widget(status_bar, chunks[1]);
880            }
881        })?;
882
883        // 处理输入事件
884        if let Event::Key(key_event) = event::read()? {
885            // 清除上次的警告提示
886            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                    // 不使用 trim(),保留每行的原始缩进
906                    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                    // :q — 检查是否有实际改动
914                    let current_lines: Vec<String> = textarea.lines().iter().map(|l| l.to_string()).collect();
915                    if current_lines == initial_snapshot {
916                        // 无改动,直接退出
917                        return Ok(None);
918                    } else {
919                        // 有改动,拒绝退出并提示
920                        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                    // 执行搜索
935                    let lines: Vec<String> = textarea.lines().iter().map(|l| l.to_string()).collect();
936                    let count = vim.search.search(&pattern, &lines);
937                    
938                    // 跳转到第一个匹配
939                    if count > 0 {
940                        if let Some((line, col)) = vim.search.next_match() {
941                            // 移动光标到匹配位置
942                            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
965/// 构建底部状态栏
966fn build_status_bar(mode: &Mode, line_count: usize, search: &SearchState) -> Paragraph<'static> {
967    let mut spans = vec![];
968
969    // 模式标签
970    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            // 这里需要 'static,用 leaked string
976            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            // 命令模式特殊处理:直接显示命令行
986            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            // 搜索模式特殊处理:直接显示搜索词
1002            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    // 快捷键提示
1025    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    // 行数
1086    spans.push(Span::styled(
1087        format!(" {} 行 ", line_count),
1088        Style::default().fg(Color::DarkGray),
1089    ));
1090
1091    // 搜索匹配信息
1092    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}