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/// 计算字符串的显示宽度(中文字符占 2 列,ASCII 占 1 列)
791fn display_width_of(s: &str) -> usize {
792    s.chars().map(|c| if c.is_ascii() { 1 } else { 2 }).sum()
793}
794
795/// 编辑器主循环
796fn 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    // 是否显示 "有未保存改动" 的提示(下次按键后清除)
804    let mut unsaved_warning = false;
805    loop {
806        let mode = &vim.mode.clone();
807
808        // 获取当前光标所在行的内容(用于预览区)
809        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        // 判断当前行是否超过终端宽度,需要显示预览区
815        let display_width: usize = display_width_of(&current_line_text);
816
817        // 绘制界面
818        terminal.draw(|frame| {
819            let area_width = frame.area().width as usize;
820            // 预留行号宽度(行号位数 + 2 个边距)+ 边框宽度 2
821            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            // 动态计算预览区高度:根据文本实际需要的行数
826            // 预览区内部可用宽度 = 终端宽度 - 左右边框各 1
827            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                // 预览区高度 = wrap 后行数 + 2(边框),最少 3 行,最多 8 行
831                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),                    // 编辑区
839                    Constraint::Length(preview_height),     // 当前行预览区
840                    Constraint::Length(2),                  // 状态栏
841                ]
842            } else {
843                vec![
844                    Constraint::Min(3),   // 编辑区
845                    Constraint::Length(2), // 状态栏
846                ]
847            };
848
849            let chunks = Layout::default()
850                .direction(Direction::Vertical)
851                .constraints(constraints)
852                .split(frame.area());
853
854            // 渲染编辑区
855            frame.render_widget(&*textarea, chunks[0]);
856
857            // 在 TextArea 渲染后,应用搜索高亮
858            if !vim.search.pattern.is_empty() {
859                apply_search_highlight(frame.buffer_mut(), chunks[0], &vim.search);
860            }
861
862            if needs_preview {
863                // 渲染当前行预览区(带 wrap)
864                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                // 渲染状态栏
876                let status_bar = build_status_bar(mode, textarea.lines().len(), &vim.search);
877                frame.render_widget(status_bar, chunks[2]);
878            } else {
879                // 渲染状态栏
880                let status_bar = build_status_bar(mode, textarea.lines().len(), &vim.search);
881                frame.render_widget(status_bar, chunks[1]);
882            }
883        })?;
884
885        // 处理输入事件
886        if let Event::Key(key_event) = event::read()? {
887            // 清除上次的警告提示
888            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                    // 不使用 trim(),保留每行的原始缩进
908                    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                    // :q — 检查是否有实际改动
916                    let current_lines: Vec<String> = textarea.lines().iter().map(|l| l.to_string()).collect();
917                    if current_lines == initial_snapshot {
918                        // 无改动,直接退出
919                        return Ok(None);
920                    } else {
921                        // 有改动,拒绝退出并提示
922                        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                    // 执行搜索
937                    let lines: Vec<String> = textarea.lines().iter().map(|l| l.to_string()).collect();
938                    let count = vim.search.search(&pattern, &lines);
939                    
940                    // 跳转到第一个匹配
941                    if count > 0 {
942                        if let Some((line, col)) = vim.search.next_match() {
943                            // 移动光标到匹配位置
944                            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
967/// 构建底部状态栏
968fn build_status_bar(mode: &Mode, line_count: usize, search: &SearchState) -> Paragraph<'static> {
969    let mut spans = vec![];
970
971    // 模式标签
972    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            // 这里需要 'static,用 leaked string
978            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            // 命令模式特殊处理:直接显示命令行
988            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            // 搜索模式特殊处理:直接显示搜索词
1004            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    // 快捷键提示
1027    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    // 行数
1088    spans.push(Span::styled(
1089        format!(" {} 行 ", line_count),
1090        Style::default().fg(Color::DarkGray),
1091    ));
1092
1093    // 搜索匹配信息
1094    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}