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};
17use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
18
19// ========== Vim 模式定义 ==========
20
21#[derive(Debug, Clone, PartialEq, Eq)]
22enum Mode {
23    Normal,
24    Insert,
25    Visual,
26    Operator(char),
27    Command(String), // 命令行模式,存储输入的命令字符串
28    Search(String),  // 搜索模式,存储输入的搜索词
29}
30
31impl fmt::Display for Mode {
32    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33        match self {
34            Self::Normal => write!(f, "NORMAL"),
35            Self::Insert => write!(f, "INSERT"),
36            Self::Visual => write!(f, "VISUAL"),
37            Self::Operator(c) => write!(f, "OPERATOR({})", c),
38            Self::Command(_) => write!(f, "COMMAND"),
39            Self::Search(_) => write!(f, "SEARCH"),
40        }
41    }
42}
43
44impl Mode {
45    fn cursor_style(&self) -> Style {
46        let color = match self {
47            Self::Normal => Color::Reset,
48            Self::Insert => Color::LightBlue,
49            Self::Visual => Color::LightYellow,
50            Self::Operator(_) => Color::LightGreen,
51            Self::Command(_) => Color::Reset,
52            Self::Search(_) => Color::Reset,
53        };
54        Style::default().fg(color).add_modifier(Modifier::REVERSED)
55    }
56
57    fn border_color(&self) -> Color {
58        match self {
59            Self::Normal => Color::DarkGray,
60            Self::Insert => Color::Cyan,
61            Self::Visual => Color::LightYellow,
62            Self::Operator(_) => Color::LightGreen,
63            Self::Command(_) => Color::DarkGray,
64            Self::Search(_) => Color::Magenta,
65        }
66    }
67}
68
69// ========== 搜索状态 ==========
70
71/// 搜索匹配结果
72#[derive(Debug, Clone)]
73struct SearchMatch {
74    line: usize,
75    start: usize,
76    #[allow(dead_code)]
77    end: usize, // 保留用于将来可能的高亮增强
78}
79
80/// 搜索状态管理
81#[derive(Debug, Clone, Default)]
82struct SearchState {
83    pattern: String,
84    matches: Vec<SearchMatch>,
85    current_index: usize,
86}
87
88impl SearchState {
89    fn new() -> Self {
90        Self::default()
91    }
92
93    /// 执行搜索,返回匹配数量
94    fn search(&mut self, pattern: &str, lines: &[String]) -> usize {
95        self.pattern = pattern.to_string();
96        self.matches.clear();
97        self.current_index = 0;
98
99        if pattern.is_empty() {
100            return 0;
101        }
102
103        for (line_idx, line) in lines.iter().enumerate() {
104            let mut start = 0;
105            while let Some(pos) = line[start..].find(pattern) {
106                let abs_start = start + pos;
107                self.matches.push(SearchMatch {
108                    line: line_idx,
109                    start: abs_start,
110                    end: abs_start + pattern.len(),
111                });
112                start = abs_start + pattern.len();
113                if start >= line.len() {
114                    break;
115                }
116            }
117        }
118
119        self.matches.len()
120    }
121
122    /// 跳转到下一个匹配,返回 (line, column)
123    fn next_match(&mut self) -> Option<(usize, usize)> {
124        if self.matches.is_empty() {
125            return None;
126        }
127        let m = &self.matches[self.current_index];
128        let result = Some((m.line, m.start));
129        self.current_index = (self.current_index + 1) % self.matches.len();
130        result
131    }
132
133    /// 跳转到上一个匹配,返回 (line, column)
134    fn prev_match(&mut self) -> Option<(usize, usize)> {
135        if self.matches.is_empty() {
136            return None;
137        }
138        if self.current_index == 0 {
139            self.current_index = self.matches.len() - 1;
140        } else {
141            self.current_index -= 1;
142        }
143        let m = &self.matches[self.current_index];
144        Some((m.line, m.start))
145    }
146
147    /// 获取当前匹配信息(用于状态栏显示)
148    fn current_info(&self) -> (usize, usize) {
149        if self.matches.is_empty() {
150            (0, 0)
151        } else {
152            let display_idx = if self.current_index == 0 {
153                self.matches.len()
154            } else {
155                self.current_index
156            };
157            (display_idx, self.matches.len())
158        }
159    }
160}
161
162// ========== 搜索高亮函数 ==========
163
164/// 应用搜索高亮到 buffer(直接修改 buffer 样式)
165fn apply_search_highlight(buf: &mut ratatui::buffer::Buffer, area: Rect, search: &SearchState) {
166    if search.pattern.is_empty() || search.matches.is_empty() {
167        return;
168    }
169
170    let pattern = &search.pattern;
171
172    // 遍历 buffer 查找 pattern
173    for row in area.top()..area.bottom() {
174        let content_start = area.left() + 3; // 跳过行号
175        let mut chars_with_pos: Vec<(char, u16)> = Vec::new();
176
177        for col in content_start..area.right() {
178            if let Some(cell) = buf.cell((col, row)) {
179                let symbol = cell.symbol();
180                for c in symbol.chars() {
181                    chars_with_pos.push((c, col));
182                }
183            }
184        }
185
186        let pattern_chars: Vec<char> = pattern.chars().collect();
187        if pattern_chars.is_empty() {
188            continue;
189        }
190
191        let mut i = 0;
192        while i + pattern_chars.len() <= chars_with_pos.len() {
193            let is_match = pattern_chars.iter().enumerate().all(|(j, pc)| {
194                chars_with_pos
195                    .get(i + j)
196                    .map(|(c, _)| c == pc)
197                    .unwrap_or(false)
198            });
199
200            if is_match {
201                // 匹配文字用红色显示
202                let style = Style::default().fg(Color::Red).add_modifier(Modifier::BOLD);
203
204                for j in 0..pattern_chars.len() {
205                    if let Some((_, col)) = chars_with_pos.get(i + j) {
206                        buf[(*col, row)].set_style(style);
207                    }
208                }
209
210                i += pattern_chars.len();
211            } else {
212                i += 1;
213            }
214        }
215    }
216}
217
218/// 跳转到指定行和列
219fn jump_to_match(textarea: &mut TextArea, line: usize, col: usize) {
220    textarea.move_cursor(CursorMove::Jump(
221        line.try_into().unwrap_or(0),
222        col.try_into().unwrap_or(0),
223    ));
224}
225
226// ========== 编辑器状态转换 ==========
227
228enum Transition {
229    Nop,
230    Mode(Mode),
231    Pending(Input),
232    Submit,         // 提交内容
233    Quit,           // 强制取消退出(:q! / Ctrl+Q)
234    TryQuit,        // 尝试退出,若有改动则拒绝(:q)
235    Search(String), // 执行搜索
236    NextMatch,      // 跳转到下一个匹配
237    PrevMatch,      // 跳转到上一个匹配
238}
239
240/// Vim 状态机
241struct Vim {
242    mode: Mode,
243    pending: Input,
244    search: SearchState,
245}
246
247impl Vim {
248    fn new(mode: Mode) -> Self {
249        Self {
250            mode,
251            pending: Input::default(),
252            search: SearchState::new(),
253        }
254    }
255
256    fn with_pending(self, pending: Input) -> Self {
257        Self {
258            mode: self.mode,
259            pending,
260            search: self.search,
261        }
262    }
263
264    /// 处理 Normal/Visual/Operator 模式的按键
265    fn transition(&self, input: Input, textarea: &mut TextArea<'_>) -> Transition {
266        if input.key == Key::Null {
267            return Transition::Nop;
268        }
269
270        // 任何模式下 Ctrl+S → 提交
271        if input.ctrl && input.key == Key::Char('s') {
272            return Transition::Submit;
273        }
274
275        // 任何模式下 Ctrl+Q → 强制取消退出
276        if input.ctrl && input.key == Key::Char('q') {
277            return Transition::Quit;
278        }
279
280        match &self.mode {
281            Mode::Command(cmd) => self.handle_command_mode(input, cmd),
282            Mode::Search(pattern) => self.handle_search_mode(input, pattern),
283            Mode::Insert => self.handle_insert_mode(input, textarea),
284            Mode::Normal | Mode::Visual | Mode::Operator(_) => {
285                self.handle_normal_visual_operator(input, textarea)
286            }
287        }
288    }
289
290    /// Insert 模式:Esc 回到 Normal,其他交给 textarea 默认处理
291    fn handle_insert_mode(&self, input: Input, textarea: &mut TextArea<'_>) -> Transition {
292        match input {
293            Input { key: Key::Esc, .. }
294            | Input {
295                key: Key::Char('c'),
296                ctrl: true,
297                ..
298            } => Transition::Mode(Mode::Normal),
299            input => {
300                textarea.input(input);
301                Transition::Mode(Mode::Insert)
302            }
303        }
304    }
305
306    /// Command 模式:处理 :wq, :w, :q, :q! 等命令
307    fn handle_command_mode(&self, input: Input, cmd: &str) -> Transition {
308        match input.key {
309            Key::Esc => Transition::Mode(Mode::Normal),
310            Key::Enter => {
311                // 执行命令
312                let cmd = cmd.trim();
313                match cmd {
314                    "wq" | "x" => Transition::Submit,
315                    "w" => Transition::Submit,
316                    "q" => Transition::TryQuit, // 有改动时拒绝退出
317                    "q!" => Transition::Quit,   // 强制退出
318                    _ => Transition::Mode(Mode::Normal), // 未知命令,回到 Normal
319                }
320            }
321            Key::Backspace => {
322                if cmd.is_empty() {
323                    Transition::Mode(Mode::Normal)
324                } else {
325                    let mut new_cmd = cmd.to_string();
326                    new_cmd.pop();
327                    Transition::Mode(Mode::Command(new_cmd))
328                }
329            }
330            Key::Char(c) => {
331                let mut new_cmd = cmd.to_string();
332                new_cmd.push(c);
333                Transition::Mode(Mode::Command(new_cmd))
334            }
335            _ => Transition::Nop,
336        }
337    }
338
339    /// Search 模式:处理 /pattern 输入
340    fn handle_search_mode(&self, input: Input, pattern: &str) -> Transition {
341        match input.key {
342            Key::Esc => Transition::Mode(Mode::Normal),
343            Key::Enter => {
344                // 执行搜索
345                Transition::Search(pattern.to_string())
346            }
347            Key::Backspace => {
348                if pattern.is_empty() {
349                    Transition::Mode(Mode::Normal)
350                } else {
351                    let mut new_pattern = pattern.to_string();
352                    new_pattern.pop();
353                    Transition::Mode(Mode::Search(new_pattern))
354                }
355            }
356            Key::Char(c) => {
357                let mut new_pattern = pattern.to_string();
358                new_pattern.push(c);
359                Transition::Mode(Mode::Search(new_pattern))
360            }
361            _ => Transition::Nop,
362        }
363    }
364
365    /// Normal / Visual / Operator 模式的 vim 按键处理
366    fn handle_normal_visual_operator(
367        &self,
368        input: Input,
369        textarea: &mut TextArea<'_>,
370    ) -> Transition {
371        match input {
372            // : 进入命令模式
373            Input {
374                key: Key::Char(':'),
375                ctrl: false,
376                ..
377            } if self.mode == Mode::Normal => {
378                return Transition::Mode(Mode::Command(String::new()));
379            }
380            // / 进入搜索模式
381            Input {
382                key: Key::Char('/'),
383                ctrl: false,
384                ..
385            } if self.mode == Mode::Normal => {
386                return Transition::Mode(Mode::Search(String::new()));
387            }
388            // n 跳转到下一个匹配
389            Input {
390                key: Key::Char('n'),
391                ctrl: false,
392                ..
393            } if self.mode == Mode::Normal => {
394                return Transition::NextMatch;
395            }
396            // N 跳转到上一个匹配
397            Input {
398                key: Key::Char('N'),
399                ctrl: false,
400                ..
401            } if self.mode == Mode::Normal => {
402                return Transition::PrevMatch;
403            }
404            // 移动
405            Input {
406                key: Key::Char('h'),
407                ..
408            } => textarea.move_cursor(CursorMove::Back),
409            Input {
410                key: Key::Char('j'),
411                ..
412            } => textarea.move_cursor(CursorMove::Down),
413            Input {
414                key: Key::Char('k'),
415                ..
416            } => textarea.move_cursor(CursorMove::Up),
417            Input {
418                key: Key::Char('l'),
419                ..
420            } => textarea.move_cursor(CursorMove::Forward),
421            Input {
422                key: Key::Char('w'),
423                ..
424            } => textarea.move_cursor(CursorMove::WordForward),
425            Input {
426                key: Key::Char('e'),
427                ctrl: false,
428                ..
429            } => {
430                textarea.move_cursor(CursorMove::WordEnd);
431                if matches!(self.mode, Mode::Operator(_)) {
432                    textarea.move_cursor(CursorMove::Forward);
433                }
434            }
435            Input {
436                key: Key::Char('b'),
437                ctrl: false,
438                ..
439            } => textarea.move_cursor(CursorMove::WordBack),
440            Input {
441                key: Key::Char('^' | '0'),
442                ..
443            } => textarea.move_cursor(CursorMove::Head),
444            Input {
445                key: Key::Char('$'),
446                ..
447            } => textarea.move_cursor(CursorMove::End),
448            // 删除 / 修改
449            Input {
450                key: Key::Char('D'),
451                ..
452            } => {
453                textarea.delete_line_by_end();
454                return Transition::Mode(Mode::Normal);
455            }
456            Input {
457                key: Key::Char('C'),
458                ..
459            } => {
460                textarea.delete_line_by_end();
461                textarea.cancel_selection();
462                return Transition::Mode(Mode::Insert);
463            }
464            Input {
465                key: Key::Char('p'),
466                ..
467            } => {
468                textarea.paste();
469                return Transition::Mode(Mode::Normal);
470            }
471            Input {
472                key: Key::Char('u'),
473                ctrl: false,
474                ..
475            } => {
476                textarea.undo();
477                return Transition::Mode(Mode::Normal);
478            }
479            Input {
480                key: Key::Char('r'),
481                ctrl: true,
482                ..
483            } => {
484                textarea.redo();
485                return Transition::Mode(Mode::Normal);
486            }
487            Input {
488                key: Key::Char('x'),
489                ..
490            } => {
491                textarea.delete_next_char();
492                return Transition::Mode(Mode::Normal);
493            }
494            // 进入 Insert 模式
495            Input {
496                key: Key::Char('i'),
497                ..
498            } => {
499                textarea.cancel_selection();
500                return Transition::Mode(Mode::Insert);
501            }
502            Input {
503                key: Key::Char('a'),
504                ctrl: false,
505                ..
506            } => {
507                textarea.cancel_selection();
508                textarea.move_cursor(CursorMove::Forward);
509                return Transition::Mode(Mode::Insert);
510            }
511            Input {
512                key: Key::Char('A'),
513                ..
514            } => {
515                textarea.cancel_selection();
516                textarea.move_cursor(CursorMove::End);
517                return Transition::Mode(Mode::Insert);
518            }
519            Input {
520                key: Key::Char('o'),
521                ..
522            } => {
523                textarea.move_cursor(CursorMove::End);
524                textarea.insert_newline();
525                return Transition::Mode(Mode::Insert);
526            }
527            Input {
528                key: Key::Char('O'),
529                ..
530            } => {
531                textarea.move_cursor(CursorMove::Head);
532                textarea.insert_newline();
533                textarea.move_cursor(CursorMove::Up);
534                return Transition::Mode(Mode::Insert);
535            }
536            Input {
537                key: Key::Char('I'),
538                ..
539            } => {
540                textarea.cancel_selection();
541                textarea.move_cursor(CursorMove::Head);
542                return Transition::Mode(Mode::Insert);
543            }
544            // 滚动
545            Input {
546                key: Key::Char('e'),
547                ctrl: true,
548                ..
549            } => textarea.scroll((1, 0)),
550            Input {
551                key: Key::Char('y'),
552                ctrl: true,
553                ..
554            } => textarea.scroll((-1, 0)),
555            Input {
556                key: Key::Char('d'),
557                ctrl: true,
558                ..
559            } => textarea.scroll(tui_textarea::Scrolling::HalfPageDown),
560            Input {
561                key: Key::Char('u'),
562                ctrl: true,
563                ..
564            } => textarea.scroll(tui_textarea::Scrolling::HalfPageUp),
565            Input {
566                key: Key::Char('f'),
567                ctrl: true,
568                ..
569            } => textarea.scroll(tui_textarea::Scrolling::PageDown),
570            Input {
571                key: Key::Char('b'),
572                ctrl: true,
573                ..
574            } => textarea.scroll(tui_textarea::Scrolling::PageUp),
575            // Visual 模式
576            Input {
577                key: Key::Char('v'),
578                ctrl: false,
579                ..
580            } if self.mode == Mode::Normal => {
581                textarea.start_selection();
582                return Transition::Mode(Mode::Visual);
583            }
584            Input {
585                key: Key::Char('V'),
586                ctrl: false,
587                ..
588            } if self.mode == Mode::Normal => {
589                textarea.move_cursor(CursorMove::Head);
590                textarea.start_selection();
591                textarea.move_cursor(CursorMove::End);
592                return Transition::Mode(Mode::Visual);
593            }
594            Input { key: Key::Esc, .. }
595            | Input {
596                key: Key::Char('v'),
597                ctrl: false,
598                ..
599            } if self.mode == Mode::Visual => {
600                textarea.cancel_selection();
601                return Transition::Mode(Mode::Normal);
602            }
603            // gg → 跳到开头
604            Input {
605                key: Key::Char('g'),
606                ctrl: false,
607                ..
608            } if matches!(
609                self.pending,
610                Input {
611                    key: Key::Char('g'),
612                    ctrl: false,
613                    ..
614                }
615            ) =>
616            {
617                textarea.move_cursor(CursorMove::Top);
618            }
619            // G → 跳到结尾
620            Input {
621                key: Key::Char('G'),
622                ctrl: false,
623                ..
624            } => textarea.move_cursor(CursorMove::Bottom),
625            // Operator 重复(yy, dd, cc)
626            Input {
627                key: Key::Char(c),
628                ctrl: false,
629                ..
630            } if self.mode == Mode::Operator(c) => {
631                textarea.move_cursor(CursorMove::Head);
632                textarea.start_selection();
633                let cursor = textarea.cursor();
634                textarea.move_cursor(CursorMove::Down);
635                if cursor == textarea.cursor() {
636                    textarea.move_cursor(CursorMove::End);
637                }
638            }
639            // 进入 Operator 模式(y/d/c)
640            Input {
641                key: Key::Char(op @ ('y' | 'd' | 'c')),
642                ctrl: false,
643                ..
644            } if self.mode == Mode::Normal => {
645                textarea.start_selection();
646                return Transition::Mode(Mode::Operator(op));
647            }
648            // Visual 模式下的 y/d/c
649            Input {
650                key: Key::Char('y'),
651                ctrl: false,
652                ..
653            } if self.mode == Mode::Visual => {
654                textarea.move_cursor(CursorMove::Forward);
655                textarea.copy();
656                return Transition::Mode(Mode::Normal);
657            }
658            Input {
659                key: Key::Char('d'),
660                ctrl: false,
661                ..
662            } if self.mode == Mode::Visual => {
663                textarea.move_cursor(CursorMove::Forward);
664                textarea.cut();
665                return Transition::Mode(Mode::Normal);
666            }
667            Input {
668                key: Key::Char('c'),
669                ctrl: false,
670                ..
671            } if self.mode == Mode::Visual => {
672                textarea.move_cursor(CursorMove::Forward);
673                textarea.cut();
674                return Transition::Mode(Mode::Insert);
675            }
676            // Esc 在 Normal 模式下不退出(vim 标准行为)
677            Input { key: Key::Esc, .. } if self.mode == Mode::Normal => {
678                return Transition::Nop;
679            }
680            // Esc 在 Operator 模式取消
681            Input { key: Key::Esc, .. } => {
682                textarea.cancel_selection();
683                return Transition::Mode(Mode::Normal);
684            }
685            // 其他未匹配的按键 → pending(用于 gg 等序列)
686            input => return Transition::Pending(input),
687        }
688
689        // 处理 pending operator
690        match self.mode {
691            Mode::Operator('y') => {
692                textarea.copy();
693                Transition::Mode(Mode::Normal)
694            }
695            Mode::Operator('d') => {
696                textarea.cut();
697                Transition::Mode(Mode::Normal)
698            }
699            Mode::Operator('c') => {
700                textarea.cut();
701                Transition::Mode(Mode::Insert)
702            }
703            _ => Transition::Nop,
704        }
705    }
706}
707
708// ========== 公共 API ==========
709
710/// 打开全屏多行编辑器(vim 模式),返回用户输入的文本内容
711///
712/// 操作方式:
713/// - 默认进入 INSERT 模式,可直接输入
714/// - Esc: 回到 NORMAL 模式
715/// - `:wq` / `:w` / `:x`: 提交内容
716/// - `:q` / `:q!`: 取消退出
717/// - `/pattern`: 搜索 pattern
718/// - `n` / `N`: 跳转到下一个/上一个匹配
719/// - Ctrl+S: 任何模式下快速提交
720///
721/// 返回 Some(text) 表示提交,None 表示取消
722#[allow(dead_code)]
723pub fn open_multiline_editor(title: &str) -> io::Result<Option<String>> {
724    open_editor_internal(title, &[], Mode::Insert)
725}
726
727/// 打开全屏多行编辑器,带有预填充内容,默认 NORMAL 模式
728///
729/// - `initial_lines`: 预填充到编辑区的行(如历史日报 + 日期前缀)
730///
731/// 返回 Some(text) 表示提交,None 表示取消
732pub fn open_multiline_editor_with_content(
733    title: &str,
734    initial_lines: &[String],
735) -> io::Result<Option<String>> {
736    open_editor_internal(title, initial_lines, Mode::Normal)
737}
738
739/// 在已有终端上打开全屏编辑器(不管理终端状态)
740///
741/// 适用于 Chat TUI 内部弹出编辑器的场景(如编辑 system_prompt),
742/// 调用方已经持有 terminal 并处于 raw mode + AlternateScreen,
743/// 编辑完毕后直接返回,不做终端状态切换。
744///
745/// 返回 Some(text) 表示提交,None 表示取消
746pub fn open_editor_on_terminal(
747    terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
748    title: &str,
749    content: &str,
750) -> io::Result<Option<String>> {
751    let initial_lines: Vec<String> = if content.is_empty() {
752        vec![]
753    } else {
754        content.lines().map(|l| l.to_string()).collect()
755    };
756    let initial_mode = if initial_lines.is_empty() {
757        Mode::Insert
758    } else {
759        Mode::Normal
760    };
761
762    let mut textarea = if initial_lines.is_empty() {
763        TextArea::default()
764    } else {
765        TextArea::new(initial_lines.clone())
766    };
767    textarea.set_block(make_block(title, &initial_mode));
768    textarea.set_cursor_style(initial_mode.cursor_style());
769    textarea.set_cursor_line_style(Style::default().add_modifier(Modifier::UNDERLINED));
770    textarea.set_line_number_style(Style::default().fg(Color::DarkGray));
771
772    if !initial_lines.is_empty() {
773        textarea.move_cursor(CursorMove::Bottom);
774        textarea.move_cursor(CursorMove::End);
775    }
776
777    let initial_snapshot: Vec<String> = textarea.lines().iter().map(|l| l.to_string()).collect();
778
779    let mut vim = Vim::new(initial_mode);
780    run_editor_loop(terminal, &mut textarea, &mut vim, title, &initial_snapshot)
781}
782
783/// 内部统一入口:初始化终端 + 编辑区 + 主循环
784fn open_editor_internal(
785    title: &str,
786    initial_lines: &[String],
787    initial_mode: Mode,
788) -> io::Result<Option<String>> {
789    // 进入终端原始模式
790    terminal::enable_raw_mode()?;
791    let mut stdout = io::stdout();
792    execute!(stdout, EnterAlternateScreen)?;
793
794    let backend = CrosstermBackend::new(stdout);
795    let mut terminal = Terminal::new(backend)?;
796
797    // 初始化文本编辑区
798    let mut textarea = if initial_lines.is_empty() {
799        TextArea::default()
800    } else {
801        TextArea::new(initial_lines.to_vec())
802    };
803    textarea.set_block(make_block(title, &initial_mode));
804    textarea.set_cursor_style(initial_mode.cursor_style());
805    textarea.set_cursor_line_style(Style::default().add_modifier(Modifier::UNDERLINED));
806    textarea.set_line_number_style(Style::default().fg(Color::DarkGray));
807
808    // 如果有预填内容,光标跳到最后一行末尾
809    if !initial_lines.is_empty() {
810        textarea.move_cursor(CursorMove::Bottom);
811        textarea.move_cursor(CursorMove::End);
812    }
813
814    // 记录初始内容的快照,用于判断是否有实际改动
815    let initial_snapshot: Vec<String> = textarea.lines().iter().map(|l| l.to_string()).collect();
816
817    let mut vim = Vim::new(initial_mode);
818    let result = run_editor_loop(
819        &mut terminal,
820        &mut textarea,
821        &mut vim,
822        title,
823        &initial_snapshot,
824    );
825
826    // 恢复终端状态
827    terminal::disable_raw_mode()?;
828    execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
829
830    result
831}
832
833/// 构造编辑区的 Block(根据模式变色)
834fn make_block<'a>(title: &str, mode: &Mode) -> Block<'a> {
835    Block::default()
836        .borders(Borders::ALL)
837        .title(format!(" {} ", title))
838        .border_style(Style::default().fg(mode.border_color()))
839}
840
841/// 计算字符串的显示宽度(使用 unicode_width,与 ratatui 内部一致)
842fn display_width_of(s: &str) -> usize {
843    UnicodeWidthStr::width(s)
844}
845
846/// 精确计算字符串在给定列宽下 wrap 后的行数(用于预览区滚动进度显示)
847fn count_wrapped_lines_unicode(s: &str, col_width: usize) -> usize {
848    if col_width == 0 || s.is_empty() {
849        return 1;
850    }
851    let mut lines = 1usize;
852    let mut current_width = 0usize;
853    for c in s.chars() {
854        let char_width = UnicodeWidthChar::width(c).unwrap_or(0);
855        if char_width == 0 {
856            continue;
857        }
858        if current_width + char_width > col_width {
859            lines += 1;
860            current_width = char_width;
861        } else {
862            current_width += char_width;
863        }
864    }
865    lines
866}
867
868/// 计算光标在指定列宽下 wrap 后所在的行号(0-based,基于 unicode_width)
869fn cursor_wrapped_line_unicode(s: &str, cursor_pos: usize, col_width: usize) -> u16 {
870    if col_width == 0 {
871        return 0;
872    }
873    let mut line: u16 = 0;
874    let mut current_width: usize = 0;
875    for (i, c) in s.chars().enumerate() {
876        if i == cursor_pos {
877            return line;
878        }
879        let char_width = UnicodeWidthChar::width(c).unwrap_or(0);
880        if char_width == 0 {
881            continue;
882        }
883        if current_width + char_width > col_width {
884            line += 1;
885            current_width = char_width;
886        } else {
887            current_width += char_width;
888        }
889    }
890    // cursor_pos == chars.len() (cursor at end)
891    line
892}
893
894/// 将字符串按光标位置(字符索引)拆分为三部分:光标前、光标字符、光标后
895fn split_line_at_cursor(line: &str, cursor_col: usize) -> (String, String, String) {
896    let chars: Vec<char> = line.chars().collect();
897    let before: String = chars[..cursor_col.min(chars.len())].iter().collect();
898    let cursor_ch = if cursor_col < chars.len() {
899        chars[cursor_col].to_string()
900    } else {
901        " ".to_string()
902    };
903    let after: String = if cursor_col < chars.len() {
904        chars[cursor_col + 1..].iter().collect()
905    } else {
906        String::new()
907    };
908    (before, cursor_ch, after)
909}
910
911/// 编辑器主循环
912fn run_editor_loop(
913    terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
914    textarea: &mut TextArea,
915    vim: &mut Vim,
916    title: &str,
917    initial_snapshot: &[String],
918) -> io::Result<Option<String>> {
919    // 是否显示 "有未保存改动" 的提示(下次按键后清除)
920    let mut unsaved_warning = false;
921    // 预览区滚动偏移(向下滚动的行数)
922    let mut preview_scroll: u16 = 0;
923    // 上一次预览的行索引,切换行时重置滚动
924    let mut last_preview_row: usize = usize::MAX;
925
926    loop {
927        let mode = &vim.mode.clone();
928
929        // 获取当前光标所在行的内容和列位置(用于预览区)
930        let (cursor_row, cursor_col) = textarea.cursor();
931        let current_line_text: String = textarea
932            .lines()
933            .get(cursor_row)
934            .map(|l| l.to_string())
935            .unwrap_or_default();
936
937        // 切换到新行时重置预览滚动
938        if cursor_row != last_preview_row {
939            preview_scroll = 0;
940            last_preview_row = cursor_row;
941        }
942
943        // 判断当前行是否超过终端宽度,需要显示预览区
944        // 用终端宽度粗略判断(不减行号宽度,保守估计)
945        let display_width: usize = display_width_of(&current_line_text);
946
947        // 绘制界面
948        terminal.draw(|frame| {
949            let area_width = frame.area().width as usize;
950            let _area_height = frame.area().height;
951            // 预留行号宽度(行号位数 + 2 个边距)+ 边框宽度 2
952            let lnum_width = format!("{}", textarea.lines().len()).len() + 2 + 2;
953            let effective_width = area_width.saturating_sub(lnum_width);
954            let needs_preview = display_width > effective_width;
955
956            let constraints = if needs_preview {
957                vec![
958                    // 编辑区占 55%,预览区占 40%,状态栏固定 2 行
959                    Constraint::Percentage(55),
960                    Constraint::Min(5),
961                    Constraint::Length(2),
962                ]
963            } else {
964                vec![
965                    Constraint::Min(3),    // 编辑区
966                    Constraint::Length(2), // 状态栏
967                ]
968            };
969
970            let chunks = Layout::default()
971                .direction(Direction::Vertical)
972                .constraints(constraints)
973                .split(frame.area());
974
975            // 渲染编辑区
976            frame.render_widget(&*textarea, chunks[0]);
977
978            // 在 TextArea 渲染后,应用搜索高亮
979            if !vim.search.pattern.is_empty() {
980                apply_search_highlight(frame.buffer_mut(), chunks[0], &vim.search);
981            }
982
983            if needs_preview {
984                // 预览区内部可用高度(去掉上下边框各 1 行)
985                let preview_inner_h = chunks[1].height.saturating_sub(2) as u16;
986                // 预览区内部可用宽度(去掉左右边框各 1 列)
987                let preview_inner_w = (chunks[1].width.saturating_sub(2)) as usize;
988
989                // 计算总 wrap 行数(用于显示滚动进度)
990                let total_wrapped =
991                    count_wrapped_lines_unicode(&current_line_text, preview_inner_w) as u16;
992                let max_scroll = total_wrapped.saturating_sub(preview_inner_h);
993
994                // 自动滚动到光标所在的 wrap 行可见
995                let cursor_wrap_line =
996                    cursor_wrapped_line_unicode(&current_line_text, cursor_col, preview_inner_w);
997                let auto_scroll = if cursor_wrap_line < preview_scroll {
998                    cursor_wrap_line
999                } else if cursor_wrap_line >= preview_scroll + preview_inner_h {
1000                    cursor_wrap_line.saturating_sub(preview_inner_h - 1)
1001                } else {
1002                    preview_scroll
1003                };
1004                let clamped_scroll = auto_scroll.min(max_scroll);
1005
1006                let scroll_hint = if total_wrapped > preview_inner_h {
1007                    format!(
1008                        " 📖 第 {} 行预览  [{}/{}行]  Alt+↓/↑滚动 ",
1009                        cursor_row + 1,
1010                        clamped_scroll + preview_inner_h,
1011                        total_wrapped
1012                    )
1013                } else {
1014                    format!(" 📖 第 {} 行预览 ", cursor_row + 1)
1015                };
1016
1017                let preview_block = Block::default()
1018                    .borders(Borders::ALL)
1019                    .title(scroll_hint)
1020                    .title_style(
1021                        Style::default()
1022                            .fg(Color::Cyan)
1023                            .add_modifier(Modifier::BOLD),
1024                    )
1025                    .border_style(Style::default().fg(Color::Cyan));
1026
1027                // 构建带光标高亮的预览文本
1028                let (before, cursor_ch, after) =
1029                    split_line_at_cursor(&current_line_text, cursor_col);
1030                let cursor_style = Style::default().fg(Color::Black).bg(Color::White);
1031                let preview_text = vec![Line::from(vec![
1032                    Span::styled(before, Style::default().fg(Color::White)),
1033                    Span::styled(cursor_ch, cursor_style),
1034                    Span::styled(after, Style::default().fg(Color::White)),
1035                ])];
1036
1037                let preview = Paragraph::new(preview_text)
1038                    .block(preview_block)
1039                    .wrap(Wrap { trim: false })
1040                    .scroll((clamped_scroll, 0));
1041                frame.render_widget(preview, chunks[1]);
1042
1043                // 渲染状态栏
1044                let status_bar = build_status_bar(mode, textarea.lines().len(), &vim.search);
1045                frame.render_widget(status_bar, chunks[2]);
1046            } else {
1047                // 渲染状态栏
1048                let status_bar = build_status_bar(mode, textarea.lines().len(), &vim.search);
1049                frame.render_widget(status_bar, chunks[1]);
1050            }
1051        })?;
1052
1053        // 处理输入事件
1054        if let Event::Key(key_event) = event::read()? {
1055            // 清除上次的警告提示
1056            if unsaved_warning {
1057                unsaved_warning = false;
1058                textarea.set_block(make_block(title, &vim.mode));
1059            }
1060
1061            let input = Input::from(key_event);
1062
1063            // Alt+↓ / Alt+↑:预览区滚动(不影响编辑区)
1064            use crossterm::event::{KeyCode, KeyModifiers};
1065            if key_event.modifiers == KeyModifiers::ALT {
1066                match key_event.code {
1067                    KeyCode::Down => {
1068                        preview_scroll = preview_scroll.saturating_add(1);
1069                        continue;
1070                    }
1071                    KeyCode::Up => {
1072                        preview_scroll = preview_scroll.saturating_sub(1);
1073                        continue;
1074                    }
1075                    _ => {}
1076                }
1077            }
1078
1079            match vim.transition(input, textarea) {
1080                Transition::Mode(new_mode) if vim.mode != new_mode => {
1081                    textarea.set_block(make_block(title, &new_mode));
1082                    textarea.set_cursor_style(new_mode.cursor_style());
1083                    *vim = Vim::new(new_mode);
1084                }
1085                Transition::Nop | Transition::Mode(_) => {}
1086                Transition::Pending(input) => {
1087                    let old = std::mem::replace(vim, Vim::new(Mode::Normal));
1088                    *vim = old.with_pending(input);
1089                }
1090                Transition::Submit => {
1091                    let lines = textarea.lines();
1092                    // 不使用 trim(),保留每行的原始缩进
1093                    let text = lines.join("\n");
1094                    if text.is_empty() {
1095                        return Ok(None);
1096                    }
1097                    return Ok(Some(text));
1098                }
1099                Transition::TryQuit => {
1100                    // :q — 检查是否有实际改动
1101                    let current_lines: Vec<String> =
1102                        textarea.lines().iter().map(|l| l.to_string()).collect();
1103                    if current_lines == initial_snapshot {
1104                        // 无改动,直接退出
1105                        return Ok(None);
1106                    } else {
1107                        // 有改动,拒绝退出并提示
1108                        unsaved_warning = true;
1109                        textarea.set_block(
1110                            Block::default()
1111                                .borders(Borders::ALL)
1112                                .title(" ⚠️ 有未保存的改动!使用 :q! 强制退出,或 :wq 保存退出 ")
1113                                .border_style(Style::default().fg(Color::LightRed)),
1114                        );
1115                        *vim = Vim::new(Mode::Normal);
1116                    }
1117                }
1118                Transition::Quit => {
1119                    return Ok(None);
1120                }
1121                Transition::Search(pattern) => {
1122                    // 执行搜索
1123                    let lines: Vec<String> =
1124                        textarea.lines().iter().map(|l| l.to_string()).collect();
1125                    let count = vim.search.search(&pattern, &lines);
1126
1127                    // 跳转到第一个匹配
1128                    if count > 0 {
1129                        if let Some((line, col)) = vim.search.next_match() {
1130                            // 移动光标到匹配位置
1131                            jump_to_match(textarea, line, col);
1132                        }
1133                    }
1134
1135                    *vim = Vim::new(Mode::Normal);
1136                    vim.search = SearchState::new();
1137                    vim.search.search(&pattern, &lines);
1138                }
1139                Transition::NextMatch => {
1140                    if let Some((line, col)) = vim.search.next_match() {
1141                        jump_to_match(textarea, line, col);
1142                    }
1143                }
1144                Transition::PrevMatch => {
1145                    if let Some((line, col)) = vim.search.prev_match() {
1146                        jump_to_match(textarea, line, col);
1147                    }
1148                }
1149            }
1150        }
1151    }
1152}
1153
1154/// 构建底部状态栏
1155fn build_status_bar(mode: &Mode, line_count: usize, search: &SearchState) -> Paragraph<'static> {
1156    let mut spans = vec![];
1157
1158    // 模式标签
1159    let (mode_text, mode_bg) = match mode {
1160        Mode::Insert => (" INSERT ", Color::LightBlue),
1161        Mode::Normal => (" NORMAL ", Color::DarkGray),
1162        Mode::Visual => (" VISUAL ", Color::LightYellow),
1163        Mode::Operator(c) => {
1164            // 这里需要 'static,用 leaked string
1165            let s: &'static str = match c {
1166                'y' => " YANK ",
1167                'd' => " DELETE ",
1168                'c' => " CHANGE ",
1169                _ => " OP ",
1170            };
1171            (s, Color::LightGreen)
1172        }
1173        Mode::Command(cmd) => {
1174            // 命令模式特殊处理:直接显示命令行
1175            let cmd_display = format!(":{}", cmd);
1176            return Paragraph::new(Line::from(vec![
1177                Span::styled(
1178                    " COMMAND ",
1179                    Style::default().fg(Color::Black).bg(Color::LightMagenta),
1180                ),
1181                Span::raw(" "),
1182                Span::styled(
1183                    cmd_display,
1184                    Style::default()
1185                        .fg(Color::White)
1186                        .add_modifier(Modifier::BOLD),
1187                ),
1188                Span::styled("█", Style::default().fg(Color::White)),
1189            ]));
1190        }
1191        Mode::Search(pattern) => {
1192            // 搜索模式特殊处理:直接显示搜索词
1193            let search_display = format!("/{}", pattern);
1194            return Paragraph::new(Line::from(vec![
1195                Span::styled(
1196                    " SEARCH ",
1197                    Style::default().fg(Color::Black).bg(Color::Magenta),
1198                ),
1199                Span::raw(" "),
1200                Span::styled(
1201                    search_display,
1202                    Style::default()
1203                        .fg(Color::White)
1204                        .add_modifier(Modifier::BOLD),
1205                ),
1206                Span::styled("█", Style::default().fg(Color::White)),
1207            ]));
1208        }
1209    };
1210
1211    spans.push(Span::styled(
1212        mode_text,
1213        Style::default().fg(Color::Black).bg(mode_bg),
1214    ));
1215    spans.push(Span::raw("  "));
1216
1217    // 快捷键提示
1218    match mode {
1219        Mode::Insert => {
1220            spans.push(Span::styled(
1221                " Ctrl+S ",
1222                Style::default().fg(Color::Black).bg(Color::Green),
1223            ));
1224            spans.push(Span::raw(" 提交  "));
1225            spans.push(Span::styled(
1226                " Ctrl+Q ",
1227                Style::default().fg(Color::Black).bg(Color::Red),
1228            ));
1229            spans.push(Span::raw(" 取消  "));
1230            spans.push(Span::styled(
1231                " Esc ",
1232                Style::default().fg(Color::Black).bg(Color::Yellow),
1233            ));
1234            spans.push(Span::raw(" Normal  "));
1235        }
1236        Mode::Normal => {
1237            spans.push(Span::styled(
1238                " :wq ",
1239                Style::default().fg(Color::Black).bg(Color::Green),
1240            ));
1241            spans.push(Span::raw(" 提交  "));
1242            spans.push(Span::styled(
1243                " / ",
1244                Style::default().fg(Color::Black).bg(Color::Magenta),
1245            ));
1246            spans.push(Span::raw(" 搜索  "));
1247            spans.push(Span::styled(
1248                " n/N ",
1249                Style::default().fg(Color::Black).bg(Color::Cyan),
1250            ));
1251            spans.push(Span::raw(" 下/上  "));
1252            spans.push(Span::styled(
1253                " i ",
1254                Style::default().fg(Color::Black).bg(Color::Cyan),
1255            ));
1256            spans.push(Span::raw(" 编辑  "));
1257        }
1258        Mode::Visual => {
1259            spans.push(Span::styled(
1260                " y ",
1261                Style::default().fg(Color::Black).bg(Color::Green),
1262            ));
1263            spans.push(Span::raw(" 复制  "));
1264            spans.push(Span::styled(
1265                " d ",
1266                Style::default().fg(Color::Black).bg(Color::Red),
1267            ));
1268            spans.push(Span::raw(" 删除  "));
1269            spans.push(Span::styled(
1270                " Esc ",
1271                Style::default().fg(Color::Black).bg(Color::Yellow),
1272            ));
1273            spans.push(Span::raw(" 取消  "));
1274        }
1275        _ => {}
1276    }
1277
1278    // 行数
1279    spans.push(Span::styled(
1280        format!(" {} 行 ", line_count),
1281        Style::default().fg(Color::DarkGray),
1282    ));
1283
1284    // 搜索匹配信息
1285    if !search.pattern.is_empty() {
1286        let (current, total) = search.current_info();
1287        spans.push(Span::raw("  "));
1288        spans.push(Span::styled(
1289            format!(" [{}: {}/{}] ", search.pattern, current, total),
1290            Style::default().fg(Color::Magenta),
1291        ));
1292    }
1293
1294    Paragraph::new(Line::from(spans))
1295}