Skip to main content

j_cli/tui/
editor.rs

1use crossterm::{
2    event::{self, Event},
3    execute,
4    terminal::{self, EnterAlternateScreen, LeaveAlternateScreen},
5};
6use ratatui::{
7    Terminal,
8    backend::CrosstermBackend,
9    layout::{Constraint, Direction, Layout, Rect},
10    style::{Color, Modifier, Style},
11    text::{Line, Span},
12    widgets::{Block, Borders, Paragraph, Wrap},
13};
14use std::fmt;
15use std::io;
16use tui_textarea::{CursorMove, Input, Key, TextArea};
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(buf: &mut ratatui::buffer::Buffer, area: Rect, search: &SearchState) {
165    if search.pattern.is_empty() || search.matches.is_empty() {
166        return;
167    }
168
169    let pattern = &search.pattern;
170
171    // 遍历 buffer 查找 pattern
172    for row in area.top()..area.bottom() {
173        let content_start = area.left() + 3; // 跳过行号
174        let mut chars_with_pos: Vec<(char, u16)> = Vec::new();
175
176        for col in content_start..area.right() {
177            if let Some(cell) = buf.cell((col, row)) {
178                let symbol = cell.symbol();
179                for c in symbol.chars() {
180                    chars_with_pos.push((c, col));
181                }
182            }
183        }
184
185        let pattern_chars: Vec<char> = pattern.chars().collect();
186        if pattern_chars.is_empty() {
187            continue;
188        }
189
190        let mut i = 0;
191        while i + pattern_chars.len() <= chars_with_pos.len() {
192            let is_match = pattern_chars.iter().enumerate().all(|(j, pc)| {
193                chars_with_pos
194                    .get(i + j)
195                    .map(|(c, _)| c == pc)
196                    .unwrap_or(false)
197            });
198
199            if is_match {
200                // 匹配文字用红色显示
201                let style = Style::default().fg(Color::Red).add_modifier(Modifier::BOLD);
202
203                for j in 0..pattern_chars.len() {
204                    if let Some((_, col)) = chars_with_pos.get(i + j) {
205                        buf[(*col, row)].set_style(style);
206                    }
207                }
208
209                i += pattern_chars.len();
210            } else {
211                i += 1;
212            }
213        }
214    }
215}
216
217/// 跳转到指定行和列
218fn jump_to_match(textarea: &mut TextArea, line: usize, col: usize) {
219    textarea.move_cursor(CursorMove::Jump(
220        line.try_into().unwrap_or(0),
221        col.try_into().unwrap_or(0),
222    ));
223}
224
225// ========== 编辑器状态转换 ==========
226
227enum Transition {
228    Nop,
229    Mode(Mode),
230    Pending(Input),
231    Submit,         // 提交内容
232    Quit,           // 强制取消退出(:q! / Ctrl+Q)
233    TryQuit,        // 尝试退出,若有改动则拒绝(:q)
234    Search(String), // 执行搜索
235    NextMatch,      // 跳转到下一个匹配
236    PrevMatch,      // 跳转到上一个匹配
237}
238
239/// Vim 状态机
240struct Vim {
241    mode: Mode,
242    pending: Input,
243    search: SearchState,
244}
245
246impl Vim {
247    fn new(mode: Mode) -> Self {
248        Self {
249            mode,
250            pending: Input::default(),
251            search: SearchState::new(),
252        }
253    }
254
255    fn with_pending(self, pending: Input) -> Self {
256        Self {
257            mode: self.mode,
258            pending,
259            search: self.search,
260        }
261    }
262
263    /// 处理 Normal/Visual/Operator 模式的按键
264    fn transition(&self, input: Input, textarea: &mut TextArea<'_>) -> Transition {
265        if input.key == Key::Null {
266            return Transition::Nop;
267        }
268
269        // 任何模式下 Ctrl+S → 提交
270        if input.ctrl && input.key == Key::Char('s') {
271            return Transition::Submit;
272        }
273
274        // 任何模式下 Ctrl+Q → 强制取消退出
275        if input.ctrl && input.key == Key::Char('q') {
276            return Transition::Quit;
277        }
278
279        match &self.mode {
280            Mode::Command(cmd) => self.handle_command_mode(input, cmd),
281            Mode::Search(pattern) => self.handle_search_mode(input, pattern),
282            Mode::Insert => self.handle_insert_mode(input, textarea),
283            Mode::Normal | Mode::Visual | Mode::Operator(_) => {
284                self.handle_normal_visual_operator(input, textarea)
285            }
286        }
287    }
288
289    /// Insert 模式:Esc 回到 Normal,其他交给 textarea 默认处理
290    fn handle_insert_mode(&self, input: Input, textarea: &mut TextArea<'_>) -> Transition {
291        match input {
292            Input { key: Key::Esc, .. }
293            | Input {
294                key: Key::Char('c'),
295                ctrl: true,
296                ..
297            } => Transition::Mode(Mode::Normal),
298            input => {
299                textarea.input(input);
300                Transition::Mode(Mode::Insert)
301            }
302        }
303    }
304
305    /// Command 模式:处理 :wq, :w, :q, :q! 等命令
306    fn handle_command_mode(&self, input: Input, cmd: &str) -> Transition {
307        match input.key {
308            Key::Esc => Transition::Mode(Mode::Normal),
309            Key::Enter => {
310                // 执行命令
311                let cmd = cmd.trim();
312                match cmd {
313                    "wq" | "x" => Transition::Submit,
314                    "w" => Transition::Submit,
315                    "q" => Transition::TryQuit, // 有改动时拒绝退出
316                    "q!" => Transition::Quit,   // 强制退出
317                    _ => Transition::Mode(Mode::Normal), // 未知命令,回到 Normal
318                }
319            }
320            Key::Backspace => {
321                if cmd.is_empty() {
322                    Transition::Mode(Mode::Normal)
323                } else {
324                    let mut new_cmd = cmd.to_string();
325                    new_cmd.pop();
326                    Transition::Mode(Mode::Command(new_cmd))
327                }
328            }
329            Key::Char(c) => {
330                let mut new_cmd = cmd.to_string();
331                new_cmd.push(c);
332                Transition::Mode(Mode::Command(new_cmd))
333            }
334            _ => Transition::Nop,
335        }
336    }
337
338    /// Search 模式:处理 /pattern 输入
339    fn handle_search_mode(&self, input: Input, pattern: &str) -> Transition {
340        match input.key {
341            Key::Esc => Transition::Mode(Mode::Normal),
342            Key::Enter => {
343                // 执行搜索
344                Transition::Search(pattern.to_string())
345            }
346            Key::Backspace => {
347                if pattern.is_empty() {
348                    Transition::Mode(Mode::Normal)
349                } else {
350                    let mut new_pattern = pattern.to_string();
351                    new_pattern.pop();
352                    Transition::Mode(Mode::Search(new_pattern))
353                }
354            }
355            Key::Char(c) => {
356                let mut new_pattern = pattern.to_string();
357                new_pattern.push(c);
358                Transition::Mode(Mode::Search(new_pattern))
359            }
360            _ => Transition::Nop,
361        }
362    }
363
364    /// Normal / Visual / Operator 模式的 vim 按键处理
365    fn handle_normal_visual_operator(
366        &self,
367        input: Input,
368        textarea: &mut TextArea<'_>,
369    ) -> Transition {
370        match input {
371            // : 进入命令模式
372            Input {
373                key: Key::Char(':'),
374                ctrl: false,
375                ..
376            } if self.mode == Mode::Normal => {
377                return Transition::Mode(Mode::Command(String::new()));
378            }
379            // / 进入搜索模式
380            Input {
381                key: Key::Char('/'),
382                ctrl: false,
383                ..
384            } if self.mode == Mode::Normal => {
385                return Transition::Mode(Mode::Search(String::new()));
386            }
387            // n 跳转到下一个匹配
388            Input {
389                key: Key::Char('n'),
390                ctrl: false,
391                ..
392            } if self.mode == Mode::Normal => {
393                return Transition::NextMatch;
394            }
395            // N 跳转到上一个匹配
396            Input {
397                key: Key::Char('N'),
398                ctrl: false,
399                ..
400            } if self.mode == Mode::Normal => {
401                return Transition::PrevMatch;
402            }
403            // 移动
404            Input {
405                key: Key::Char('h'),
406                ..
407            } => textarea.move_cursor(CursorMove::Back),
408            Input {
409                key: Key::Char('j'),
410                ..
411            } => textarea.move_cursor(CursorMove::Down),
412            Input {
413                key: Key::Char('k'),
414                ..
415            } => textarea.move_cursor(CursorMove::Up),
416            Input {
417                key: Key::Char('l'),
418                ..
419            } => textarea.move_cursor(CursorMove::Forward),
420            Input {
421                key: Key::Char('w'),
422                ..
423            } => textarea.move_cursor(CursorMove::WordForward),
424            Input {
425                key: Key::Char('e'),
426                ctrl: false,
427                ..
428            } => {
429                textarea.move_cursor(CursorMove::WordEnd);
430                if matches!(self.mode, Mode::Operator(_)) {
431                    textarea.move_cursor(CursorMove::Forward);
432                }
433            }
434            Input {
435                key: Key::Char('b'),
436                ctrl: false,
437                ..
438            } => textarea.move_cursor(CursorMove::WordBack),
439            Input {
440                key: Key::Char('^' | '0'),
441                ..
442            } => textarea.move_cursor(CursorMove::Head),
443            Input {
444                key: Key::Char('$'),
445                ..
446            } => textarea.move_cursor(CursorMove::End),
447            // 删除 / 修改
448            Input {
449                key: Key::Char('D'),
450                ..
451            } => {
452                textarea.delete_line_by_end();
453                return Transition::Mode(Mode::Normal);
454            }
455            Input {
456                key: Key::Char('C'),
457                ..
458            } => {
459                textarea.delete_line_by_end();
460                textarea.cancel_selection();
461                return Transition::Mode(Mode::Insert);
462            }
463            Input {
464                key: Key::Char('p'),
465                ..
466            } => {
467                textarea.paste();
468                return Transition::Mode(Mode::Normal);
469            }
470            Input {
471                key: Key::Char('u'),
472                ctrl: false,
473                ..
474            } => {
475                textarea.undo();
476                return Transition::Mode(Mode::Normal);
477            }
478            Input {
479                key: Key::Char('r'),
480                ctrl: true,
481                ..
482            } => {
483                textarea.redo();
484                return Transition::Mode(Mode::Normal);
485            }
486            Input {
487                key: Key::Char('x'),
488                ..
489            } => {
490                textarea.delete_next_char();
491                return Transition::Mode(Mode::Normal);
492            }
493            // 进入 Insert 模式
494            Input {
495                key: Key::Char('i'),
496                ..
497            } => {
498                textarea.cancel_selection();
499                return Transition::Mode(Mode::Insert);
500            }
501            Input {
502                key: Key::Char('a'),
503                ctrl: false,
504                ..
505            } => {
506                textarea.cancel_selection();
507                textarea.move_cursor(CursorMove::Forward);
508                return Transition::Mode(Mode::Insert);
509            }
510            Input {
511                key: Key::Char('A'),
512                ..
513            } => {
514                textarea.cancel_selection();
515                textarea.move_cursor(CursorMove::End);
516                return Transition::Mode(Mode::Insert);
517            }
518            Input {
519                key: Key::Char('o'),
520                ..
521            } => {
522                textarea.move_cursor(CursorMove::End);
523                textarea.insert_newline();
524                return Transition::Mode(Mode::Insert);
525            }
526            Input {
527                key: Key::Char('O'),
528                ..
529            } => {
530                textarea.move_cursor(CursorMove::Head);
531                textarea.insert_newline();
532                textarea.move_cursor(CursorMove::Up);
533                return Transition::Mode(Mode::Insert);
534            }
535            Input {
536                key: Key::Char('I'),
537                ..
538            } => {
539                textarea.cancel_selection();
540                textarea.move_cursor(CursorMove::Head);
541                return Transition::Mode(Mode::Insert);
542            }
543            // 滚动
544            Input {
545                key: Key::Char('e'),
546                ctrl: true,
547                ..
548            } => textarea.scroll((1, 0)),
549            Input {
550                key: Key::Char('y'),
551                ctrl: true,
552                ..
553            } => textarea.scroll((-1, 0)),
554            Input {
555                key: Key::Char('d'),
556                ctrl: true,
557                ..
558            } => textarea.scroll(tui_textarea::Scrolling::HalfPageDown),
559            Input {
560                key: Key::Char('u'),
561                ctrl: true,
562                ..
563            } => textarea.scroll(tui_textarea::Scrolling::HalfPageUp),
564            Input {
565                key: Key::Char('f'),
566                ctrl: true,
567                ..
568            } => textarea.scroll(tui_textarea::Scrolling::PageDown),
569            Input {
570                key: Key::Char('b'),
571                ctrl: true,
572                ..
573            } => textarea.scroll(tui_textarea::Scrolling::PageUp),
574            // Visual 模式
575            Input {
576                key: Key::Char('v'),
577                ctrl: false,
578                ..
579            } if self.mode == Mode::Normal => {
580                textarea.start_selection();
581                return Transition::Mode(Mode::Visual);
582            }
583            Input {
584                key: Key::Char('V'),
585                ctrl: false,
586                ..
587            } if self.mode == Mode::Normal => {
588                textarea.move_cursor(CursorMove::Head);
589                textarea.start_selection();
590                textarea.move_cursor(CursorMove::End);
591                return Transition::Mode(Mode::Visual);
592            }
593            Input { key: Key::Esc, .. }
594            | Input {
595                key: Key::Char('v'),
596                ctrl: false,
597                ..
598            } if self.mode == Mode::Visual => {
599                textarea.cancel_selection();
600                return Transition::Mode(Mode::Normal);
601            }
602            // gg → 跳到开头
603            Input {
604                key: Key::Char('g'),
605                ctrl: false,
606                ..
607            } if matches!(
608                self.pending,
609                Input {
610                    key: Key::Char('g'),
611                    ctrl: false,
612                    ..
613                }
614            ) =>
615            {
616                textarea.move_cursor(CursorMove::Top);
617            }
618            // G → 跳到结尾
619            Input {
620                key: Key::Char('G'),
621                ctrl: false,
622                ..
623            } => textarea.move_cursor(CursorMove::Bottom),
624            // Operator 重复(yy, dd, cc)
625            Input {
626                key: Key::Char(c),
627                ctrl: false,
628                ..
629            } if self.mode == Mode::Operator(c) => {
630                textarea.move_cursor(CursorMove::Head);
631                textarea.start_selection();
632                let cursor = textarea.cursor();
633                textarea.move_cursor(CursorMove::Down);
634                if cursor == textarea.cursor() {
635                    textarea.move_cursor(CursorMove::End);
636                }
637            }
638            // 进入 Operator 模式(y/d/c)
639            Input {
640                key: Key::Char(op @ ('y' | 'd' | 'c')),
641                ctrl: false,
642                ..
643            } if self.mode == Mode::Normal => {
644                textarea.start_selection();
645                return Transition::Mode(Mode::Operator(op));
646            }
647            // Visual 模式下的 y/d/c
648            Input {
649                key: Key::Char('y'),
650                ctrl: false,
651                ..
652            } if self.mode == Mode::Visual => {
653                textarea.move_cursor(CursorMove::Forward);
654                textarea.copy();
655                return Transition::Mode(Mode::Normal);
656            }
657            Input {
658                key: Key::Char('d'),
659                ctrl: false,
660                ..
661            } if self.mode == Mode::Visual => {
662                textarea.move_cursor(CursorMove::Forward);
663                textarea.cut();
664                return Transition::Mode(Mode::Normal);
665            }
666            Input {
667                key: Key::Char('c'),
668                ctrl: false,
669                ..
670            } if self.mode == Mode::Visual => {
671                textarea.move_cursor(CursorMove::Forward);
672                textarea.cut();
673                return Transition::Mode(Mode::Insert);
674            }
675            // Esc 在 Normal 模式下不退出(vim 标准行为)
676            Input { key: Key::Esc, .. } if self.mode == Mode::Normal => {
677                return Transition::Nop;
678            }
679            // Esc 在 Operator 模式取消
680            Input { key: Key::Esc, .. } => {
681                textarea.cancel_selection();
682                return Transition::Mode(Mode::Normal);
683            }
684            // 其他未匹配的按键 → pending(用于 gg 等序列)
685            input => return Transition::Pending(input),
686        }
687
688        // 处理 pending operator
689        match self.mode {
690            Mode::Operator('y') => {
691                textarea.copy();
692                Transition::Mode(Mode::Normal)
693            }
694            Mode::Operator('d') => {
695                textarea.cut();
696                Transition::Mode(Mode::Normal)
697            }
698            Mode::Operator('c') => {
699                textarea.cut();
700                Transition::Mode(Mode::Insert)
701            }
702            _ => Transition::Nop,
703        }
704    }
705}
706
707// ========== 公共 API ==========
708
709/// 打开全屏多行编辑器(vim 模式),返回用户输入的文本内容
710///
711/// 操作方式:
712/// - 默认进入 INSERT 模式,可直接输入
713/// - Esc: 回到 NORMAL 模式
714/// - `:wq` / `:w` / `:x`: 提交内容
715/// - `:q` / `:q!`: 取消退出
716/// - `/pattern`: 搜索 pattern
717/// - `n` / `N`: 跳转到下一个/上一个匹配
718/// - Ctrl+S: 任何模式下快速提交
719///
720/// 返回 Some(text) 表示提交,None 表示取消
721#[allow(dead_code)]
722pub fn open_multiline_editor(title: &str) -> io::Result<Option<String>> {
723    open_editor_internal(title, &[], Mode::Insert)
724}
725
726/// 打开全屏多行编辑器,带有预填充内容,默认 NORMAL 模式
727///
728/// - `initial_lines`: 预填充到编辑区的行(如历史日报 + 日期前缀)
729///
730/// 返回 Some(text) 表示提交,None 表示取消
731pub fn open_multiline_editor_with_content(
732    title: &str,
733    initial_lines: &[String],
734) -> 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(
774        &mut terminal,
775        &mut textarea,
776        &mut vim,
777        title,
778        &initial_snapshot,
779    );
780
781    // 恢复终端状态
782    terminal::disable_raw_mode()?;
783    execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
784
785    result
786}
787
788/// 构造编辑区的 Block(根据模式变色)
789fn make_block<'a>(title: &str, mode: &Mode) -> Block<'a> {
790    Block::default()
791        .borders(Borders::ALL)
792        .title(format!(" {} ", title))
793        .border_style(Style::default().fg(mode.border_color()))
794}
795
796/// 计算字符串的显示宽度(中文字符占 2 列,ASCII 占 1 列)
797fn display_width_of(s: &str) -> usize {
798    s.chars().map(|c| if c.is_ascii() { 1 } else { 2 }).sum()
799}
800
801/// 编辑器主循环
802fn run_editor_loop(
803    terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
804    textarea: &mut TextArea,
805    vim: &mut Vim,
806    title: &str,
807    initial_snapshot: &[String],
808) -> io::Result<Option<String>> {
809    // 是否显示 "有未保存改动" 的提示(下次按键后清除)
810    let mut unsaved_warning = false;
811    loop {
812        let mode = &vim.mode.clone();
813
814        // 获取当前光标所在行的内容(用于预览区)
815        let cursor_row = textarea.cursor().0;
816        let current_line_text: String = textarea
817            .lines()
818            .get(cursor_row)
819            .map(|l| l.to_string())
820            .unwrap_or_default();
821        // 判断当前行是否超过终端宽度,需要显示预览区
822        let display_width: usize = display_width_of(&current_line_text);
823
824        // 绘制界面
825        terminal.draw(|frame| {
826            let area_width = frame.area().width as usize;
827            // 预留行号宽度(行号位数 + 2 个边距)+ 边框宽度 2
828            let lnum_width = format!("{}", textarea.lines().len()).len() + 2 + 2;
829            let effective_width = area_width.saturating_sub(lnum_width);
830            let needs_preview = display_width > effective_width;
831
832            // 动态计算预览区高度:根据文本实际需要的行数
833            // 预览区内部可用宽度 = 终端宽度 - 左右边框各 1
834            let preview_inner_width = area_width.saturating_sub(2).max(1);
835            let preview_height = if needs_preview {
836                let wrapped_lines =
837                    (display_width as f64 / preview_inner_width as f64).ceil() as u16;
838                // 预览区高度 = wrap 后行数 + 2(边框),最少 3 行,最多 8 行
839                wrapped_lines.saturating_add(2).clamp(3, 8)
840            } else {
841                0
842            };
843
844            let constraints = if needs_preview {
845                vec![
846                    Constraint::Min(3),                 // 编辑区
847                    Constraint::Length(preview_height), // 当前行预览区
848                    Constraint::Length(2),              // 状态栏
849                ]
850            } else {
851                vec![
852                    Constraint::Min(3),    // 编辑区
853                    Constraint::Length(2), // 状态栏
854                ]
855            };
856
857            let chunks = Layout::default()
858                .direction(Direction::Vertical)
859                .constraints(constraints)
860                .split(frame.area());
861
862            // 渲染编辑区
863            frame.render_widget(&*textarea, chunks[0]);
864
865            // 在 TextArea 渲染后,应用搜索高亮
866            if !vim.search.pattern.is_empty() {
867                apply_search_highlight(frame.buffer_mut(), chunks[0], &vim.search);
868            }
869
870            if needs_preview {
871                // 渲染当前行预览区(带 wrap)
872                let preview_block = Block::default()
873                    .borders(Borders::ALL)
874                    .title(format!(" 📖 第 {} 行预览 ", cursor_row + 1))
875                    .title_style(
876                        Style::default()
877                            .fg(Color::Cyan)
878                            .add_modifier(Modifier::BOLD),
879                    )
880                    .border_style(Style::default().fg(Color::Cyan));
881                let preview = Paragraph::new(current_line_text.clone())
882                    .block(preview_block)
883                    .style(Style::default().fg(Color::White))
884                    .wrap(Wrap { trim: false });
885                frame.render_widget(preview, chunks[1]);
886
887                // 渲染状态栏
888                let status_bar = build_status_bar(mode, textarea.lines().len(), &vim.search);
889                frame.render_widget(status_bar, chunks[2]);
890            } else {
891                // 渲染状态栏
892                let status_bar = build_status_bar(mode, textarea.lines().len(), &vim.search);
893                frame.render_widget(status_bar, chunks[1]);
894            }
895        })?;
896
897        // 处理输入事件
898        if let Event::Key(key_event) = event::read()? {
899            // 清除上次的警告提示
900            if unsaved_warning {
901                unsaved_warning = false;
902                textarea.set_block(make_block(title, &vim.mode));
903            }
904
905            let input = Input::from(key_event);
906            match vim.transition(input, textarea) {
907                Transition::Mode(new_mode) if vim.mode != new_mode => {
908                    textarea.set_block(make_block(title, &new_mode));
909                    textarea.set_cursor_style(new_mode.cursor_style());
910                    *vim = Vim::new(new_mode);
911                }
912                Transition::Nop | Transition::Mode(_) => {}
913                Transition::Pending(input) => {
914                    let old = std::mem::replace(vim, Vim::new(Mode::Normal));
915                    *vim = old.with_pending(input);
916                }
917                Transition::Submit => {
918                    let lines = textarea.lines();
919                    // 不使用 trim(),保留每行的原始缩进
920                    let text = lines.join("\n");
921                    if text.is_empty() {
922                        return Ok(None);
923                    }
924                    return Ok(Some(text));
925                }
926                Transition::TryQuit => {
927                    // :q — 检查是否有实际改动
928                    let current_lines: Vec<String> =
929                        textarea.lines().iter().map(|l| l.to_string()).collect();
930                    if current_lines == initial_snapshot {
931                        // 无改动,直接退出
932                        return Ok(None);
933                    } else {
934                        // 有改动,拒绝退出并提示
935                        unsaved_warning = true;
936                        textarea.set_block(
937                            Block::default()
938                                .borders(Borders::ALL)
939                                .title(" ⚠️ 有未保存的改动!使用 :q! 强制退出,或 :wq 保存退出 ")
940                                .border_style(Style::default().fg(Color::LightRed)),
941                        );
942                        *vim = Vim::new(Mode::Normal);
943                    }
944                }
945                Transition::Quit => {
946                    return Ok(None);
947                }
948                Transition::Search(pattern) => {
949                    // 执行搜索
950                    let lines: Vec<String> =
951                        textarea.lines().iter().map(|l| l.to_string()).collect();
952                    let count = vim.search.search(&pattern, &lines);
953
954                    // 跳转到第一个匹配
955                    if count > 0 {
956                        if let Some((line, col)) = vim.search.next_match() {
957                            // 移动光标到匹配位置
958                            jump_to_match(textarea, line, col);
959                        }
960                    }
961
962                    *vim = Vim::new(Mode::Normal);
963                    vim.search = SearchState::new();
964                    vim.search.search(&pattern, &lines);
965                }
966                Transition::NextMatch => {
967                    if let Some((line, col)) = vim.search.next_match() {
968                        jump_to_match(textarea, line, col);
969                    }
970                }
971                Transition::PrevMatch => {
972                    if let Some((line, col)) = vim.search.prev_match() {
973                        jump_to_match(textarea, line, col);
974                    }
975                }
976            }
977        }
978    }
979}
980
981/// 构建底部状态栏
982fn build_status_bar(mode: &Mode, line_count: usize, search: &SearchState) -> Paragraph<'static> {
983    let mut spans = vec![];
984
985    // 模式标签
986    let (mode_text, mode_bg) = match mode {
987        Mode::Insert => (" INSERT ", Color::LightBlue),
988        Mode::Normal => (" NORMAL ", Color::DarkGray),
989        Mode::Visual => (" VISUAL ", Color::LightYellow),
990        Mode::Operator(c) => {
991            // 这里需要 'static,用 leaked string
992            let s: &'static str = match c {
993                'y' => " YANK ",
994                'd' => " DELETE ",
995                'c' => " CHANGE ",
996                _ => " OP ",
997            };
998            (s, Color::LightGreen)
999        }
1000        Mode::Command(cmd) => {
1001            // 命令模式特殊处理:直接显示命令行
1002            let cmd_display = format!(":{}", cmd);
1003            return Paragraph::new(Line::from(vec![
1004                Span::styled(
1005                    " COMMAND ",
1006                    Style::default().fg(Color::Black).bg(Color::LightMagenta),
1007                ),
1008                Span::raw(" "),
1009                Span::styled(
1010                    cmd_display,
1011                    Style::default()
1012                        .fg(Color::White)
1013                        .add_modifier(Modifier::BOLD),
1014                ),
1015                Span::styled("█", Style::default().fg(Color::White)),
1016            ]));
1017        }
1018        Mode::Search(pattern) => {
1019            // 搜索模式特殊处理:直接显示搜索词
1020            let search_display = format!("/{}", pattern);
1021            return Paragraph::new(Line::from(vec![
1022                Span::styled(
1023                    " SEARCH ",
1024                    Style::default().fg(Color::Black).bg(Color::Magenta),
1025                ),
1026                Span::raw(" "),
1027                Span::styled(
1028                    search_display,
1029                    Style::default()
1030                        .fg(Color::White)
1031                        .add_modifier(Modifier::BOLD),
1032                ),
1033                Span::styled("█", Style::default().fg(Color::White)),
1034            ]));
1035        }
1036    };
1037
1038    spans.push(Span::styled(
1039        mode_text,
1040        Style::default().fg(Color::Black).bg(mode_bg),
1041    ));
1042    spans.push(Span::raw("  "));
1043
1044    // 快捷键提示
1045    match mode {
1046        Mode::Insert => {
1047            spans.push(Span::styled(
1048                " Ctrl+S ",
1049                Style::default().fg(Color::Black).bg(Color::Green),
1050            ));
1051            spans.push(Span::raw(" 提交  "));
1052            spans.push(Span::styled(
1053                " Ctrl+Q ",
1054                Style::default().fg(Color::Black).bg(Color::Red),
1055            ));
1056            spans.push(Span::raw(" 取消  "));
1057            spans.push(Span::styled(
1058                " Esc ",
1059                Style::default().fg(Color::Black).bg(Color::Yellow),
1060            ));
1061            spans.push(Span::raw(" Normal  "));
1062        }
1063        Mode::Normal => {
1064            spans.push(Span::styled(
1065                " :wq ",
1066                Style::default().fg(Color::Black).bg(Color::Green),
1067            ));
1068            spans.push(Span::raw(" 提交  "));
1069            spans.push(Span::styled(
1070                " / ",
1071                Style::default().fg(Color::Black).bg(Color::Magenta),
1072            ));
1073            spans.push(Span::raw(" 搜索  "));
1074            spans.push(Span::styled(
1075                " n/N ",
1076                Style::default().fg(Color::Black).bg(Color::Cyan),
1077            ));
1078            spans.push(Span::raw(" 下/上  "));
1079            spans.push(Span::styled(
1080                " i ",
1081                Style::default().fg(Color::Black).bg(Color::Cyan),
1082            ));
1083            spans.push(Span::raw(" 编辑  "));
1084        }
1085        Mode::Visual => {
1086            spans.push(Span::styled(
1087                " y ",
1088                Style::default().fg(Color::Black).bg(Color::Green),
1089            ));
1090            spans.push(Span::raw(" 复制  "));
1091            spans.push(Span::styled(
1092                " d ",
1093                Style::default().fg(Color::Black).bg(Color::Red),
1094            ));
1095            spans.push(Span::raw(" 删除  "));
1096            spans.push(Span::styled(
1097                " Esc ",
1098                Style::default().fg(Color::Black).bg(Color::Yellow),
1099            ));
1100            spans.push(Span::raw(" 取消  "));
1101        }
1102        _ => {}
1103    }
1104
1105    // 行数
1106    spans.push(Span::styled(
1107        format!(" {} 行 ", line_count),
1108        Style::default().fg(Color::DarkGray),
1109    ));
1110
1111    // 搜索匹配信息
1112    if !search.pattern.is_empty() {
1113        let (current, total) = search.current_info();
1114        spans.push(Span::raw("  "));
1115        spans.push(Span::styled(
1116            format!(" [{}: {}/{}] ", search.pattern, current, total),
1117            Style::default().fg(Color::Magenta),
1118        ));
1119    }
1120
1121    Paragraph::new(Line::from(spans))
1122}