use super::history::{History, Snapshot};
use super::text_buffer::TextBuffer;
use std::fmt;
#[derive(Debug, Clone)]
pub struct CmdItem {
pub name: &'static str,
pub desc: &'static str,
}
pub const COMMANDS: &[CmdItem] = &[
CmdItem {
name: "save",
desc: "保存/提交",
},
CmdItem {
name: "quit",
desc: "取消退出",
},
CmdItem {
name: "search",
desc: "搜索",
},
CmdItem {
name: "wrap",
desc: "开启折行",
},
CmdItem {
name: "nowrap",
desc: "关闭折行",
},
CmdItem {
name: "jump",
desc: "跳转到指定行 (如 /jump 10)",
},
CmdItem {
name: "undo",
desc: "撤销",
},
CmdItem {
name: "redo",
desc: "重做",
},
CmdItem {
name: "tohead",
desc: "跳到文件开头",
},
CmdItem {
name: "toend",
desc: "跳到文件末尾",
},
CmdItem {
name: "theme",
desc: "切换主题",
},
CmdItem {
name: "line-number",
desc: "显示行号",
},
CmdItem {
name: "no-line-number",
desc: "隐藏行号",
},
];
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Mode {
Normal,
Insert,
Visual,
Operator(char),
Command(String),
Search(String),
CommandPanel(String),
ThemeSelect,
}
impl fmt::Display for Mode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Normal => write!(f, "NORMAL"),
Self::Insert => write!(f, "INSERT"),
Self::Visual => write!(f, "VISUAL"),
Self::Operator(c) => write!(f, "OPERATOR({})", c),
Self::Command(_) => write!(f, "COMMAND"),
Self::Search(_) => write!(f, "SEARCH"),
Self::CommandPanel(_) => write!(f, "CMD"),
Self::ThemeSelect => write!(f, "THEME"),
}
}
}
impl Mode {
pub fn border_color(&self) -> ratatui::style::Color {
use ratatui::style::Color;
match self {
Self::Normal => Color::DarkGray,
Self::Insert => Color::Cyan,
Self::Visual => Color::LightYellow,
Self::Operator(_) => Color::LightGreen,
Self::Command(_) => Color::DarkGray,
Self::Search(_) => Color::Magenta,
Self::CommandPanel(_) => Color::Magenta,
Self::ThemeSelect => Color::Magenta,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Key {
Char(char),
Enter,
Backspace,
Esc,
Left,
Right,
Up,
Down,
PageUp,
PageDown,
Home,
End,
Tab,
Delete,
F(u8),
Null,
}
#[derive(Debug, Clone)]
pub struct Input {
pub key: Key,
pub ctrl: bool,
}
impl Input {
pub fn from_keycode(
code: crossterm::event::KeyCode,
modifiers: crossterm::event::KeyModifiers,
) -> Self {
use crossterm::event::{KeyCode, KeyModifiers};
let key = match code {
KeyCode::Char(c) => Key::Char(c),
KeyCode::Enter => Key::Enter,
KeyCode::Backspace => Key::Backspace,
KeyCode::Esc => Key::Esc,
KeyCode::Left => Key::Left,
KeyCode::Right => Key::Right,
KeyCode::Up => Key::Up,
KeyCode::Down => Key::Down,
KeyCode::PageUp => Key::PageUp,
KeyCode::PageDown => Key::PageDown,
KeyCode::Home => Key::Home,
KeyCode::End => Key::End,
KeyCode::Tab => Key::Tab,
KeyCode::Delete => Key::Delete,
KeyCode::F(n) => Key::F(n),
_ => Key::Null,
};
Self {
key,
ctrl: modifiers.contains(KeyModifiers::CONTROL),
}
}
}
#[derive(Debug)]
pub enum Transition {
Nop,
Mode(Mode),
Submit,
Cancel,
NeedRebuild,
ToggleWrap(bool),
ExecuteCommand(String),
}
#[derive(Debug)]
pub struct Vim {
mode: Mode,
yank_register: String,
visual_start: (usize, usize),
history: History,
}
impl Vim {
pub fn new(initial_mode: Mode) -> Self {
Self {
mode: initial_mode,
yank_register: String::new(),
visual_start: (0, 0),
history: History::new(),
}
}
pub fn mode(&self) -> &Mode {
&self.mode
}
pub fn set_mode(&mut self, mode: Mode) {
self.mode = mode;
}
pub fn handle_input(&mut self, input: &Input, buffer: &mut TextBuffer) -> Transition {
let mode = self.mode.clone();
match &mode {
Mode::Insert => self.handle_insert_mode(input, buffer),
Mode::Normal => self.handle_normal_mode(input, buffer),
Mode::Command(cmd) => self.handle_command_mode(input, cmd.clone()),
Mode::Search(pattern) => self.handle_search_mode(input, pattern.clone()),
Mode::CommandPanel(filter) => self.handle_command_panel_mode(input, filter.clone()),
Mode::Visual => self.handle_visual_mode(input, buffer),
Mode::Operator(c) => self.handle_operator_mode(input, *c, buffer),
Mode::ThemeSelect => Transition::Nop, }
}
fn handle_insert_mode(&mut self, input: &Input, buffer: &mut TextBuffer) -> Transition {
match input.key {
Key::Esc => Transition::Mode(Mode::Normal),
Key::Enter => {
buffer.insert_newline();
Transition::NeedRebuild
}
Key::Backspace => {
buffer.backspace();
Transition::NeedRebuild
}
Key::Delete => {
buffer.delete_char();
Transition::NeedRebuild
}
Key::Left => {
buffer.move_cursor_back();
Transition::Nop
}
Key::Right => {
buffer.move_cursor_forward();
Transition::Nop
}
Key::Up => {
buffer.move_cursor_up();
Transition::Nop
}
Key::Down => {
buffer.move_cursor_down();
Transition::Nop
}
Key::Char(c) => {
buffer.insert_char(c);
Transition::NeedRebuild
}
Key::Tab => {
buffer.insert_str(" ");
Transition::NeedRebuild
}
_ => Transition::Nop,
}
}
fn handle_normal_mode(&mut self, input: &Input, buffer: &mut TextBuffer) -> Transition {
match input.key {
Key::Char('i') => Transition::Mode(Mode::Insert),
Key::Char('a') => {
buffer.move_cursor_forward();
Transition::Mode(Mode::Insert)
}
Key::Char('A') => {
buffer.move_cursor_end();
Transition::Mode(Mode::Insert)
}
Key::Char('I') => {
buffer.move_cursor_head();
Transition::Mode(Mode::Insert)
}
Key::Char('o') => {
buffer.insert_line_below();
Transition::Mode(Mode::Insert)
}
Key::Char('O') => {
buffer.insert_line_above();
Transition::Mode(Mode::Insert)
}
Key::Char('h') | Key::Left => {
buffer.move_cursor_back();
Transition::Nop
}
Key::Char('j') | Key::Down => {
buffer.move_cursor_down();
Transition::Nop
}
Key::Char('k') | Key::Up => {
buffer.move_cursor_up();
Transition::Nop
}
Key::Char('l') | Key::Right => {
buffer.move_cursor_forward();
Transition::Nop
}
Key::Char('w') => {
buffer.move_cursor_word_forward();
Transition::Nop
}
Key::Char('b') => {
buffer.move_cursor_word_back();
Transition::Nop
}
Key::Char('e') => {
buffer.move_cursor_word_end();
Transition::Nop
}
Key::Char('0') => {
buffer.move_cursor_head();
Transition::Nop
}
Key::Char('$') => {
buffer.move_cursor_end();
Transition::Nop
}
Key::Char('g') => {
buffer.move_cursor_top();
Transition::Nop
}
Key::Char('G') => {
buffer.move_cursor_bottom();
Transition::Nop
}
Key::Char('x') => {
buffer.delete_char();
Transition::NeedRebuild
}
Key::Char('X') => {
buffer.move_cursor_back();
buffer.delete_char();
Transition::NeedRebuild
}
Key::Char('d') => Transition::Mode(Mode::Operator('d')),
Key::Char('c') => Transition::Mode(Mode::Operator('c')),
Key::Char('y') => Transition::Mode(Mode::Operator('y')),
Key::Char('p') => {
if !self.yank_register.is_empty() {
buffer.move_cursor_end();
buffer.insert_newline();
buffer.insert_str(&self.yank_register);
}
Transition::NeedRebuild
}
Key::Char('v') => {
self.visual_start = buffer.cursor();
Transition::Mode(Mode::Visual)
}
Key::Char(':') | Key::Char(':') => Transition::Mode(Mode::Command(String::new())),
Key::Char('/') => Transition::Mode(Mode::CommandPanel(String::new())),
Key::PageDown => {
for _ in 0..10 {
buffer.move_cursor_down();
}
Transition::Nop
}
Key::PageUp => {
for _ in 0..10 {
buffer.move_cursor_up();
}
Transition::Nop
}
_ => Transition::Nop,
}
}
fn handle_command_mode(&mut self, input: &Input, cmd: String) -> Transition {
match input.key {
Key::Esc => Transition::Mode(Mode::Normal),
Key::Enter => {
let trimmed = cmd.trim();
match trimmed {
"w" | "wq" | "x" => Transition::Submit,
"q" | "q!" => Transition::Cancel,
"set wrap" => Transition::ToggleWrap(true),
"set nowrap" => Transition::ToggleWrap(false),
_ => Transition::Mode(Mode::Normal),
}
}
_ => Transition::Nop,
}
}
fn handle_search_mode(&mut self, input: &Input, _pattern: String) -> Transition {
match input.key {
Key::Esc => Transition::Mode(Mode::Normal),
Key::Enter => Transition::Mode(Mode::Normal),
_ => Transition::Nop,
}
}
fn handle_command_panel_mode(&mut self, input: &Input, filter: String) -> Transition {
match input.key {
Key::Esc => Transition::Mode(Mode::Normal),
Key::Enter => {
let matched = filter_commands(&filter);
if let Some(cmd) = matched.first() {
let cmd_name = cmd.name.to_string();
let full_cmd = if cmd_name == "jump" {
filter.clone()
} else {
cmd_name
};
Transition::ExecuteCommand(full_cmd)
} else {
Transition::Mode(Mode::Normal)
}
}
_ => Transition::Nop,
}
}
fn handle_visual_mode(&mut self, input: &Input, buffer: &mut TextBuffer) -> Transition {
match input.key {
Key::Esc => Transition::Mode(Mode::Normal),
Key::Char('y') => {
let (start_row, start_col) = self.visual_start;
let (end_row, end_col) = buffer.cursor();
let (start_row, start_col, end_row, end_col) =
if start_row > end_row || (start_row == end_row && start_col > end_col) {
(end_row, end_col, start_row, start_col)
} else {
(start_row, start_col, end_row, end_col)
};
let lines = buffer.lines();
if start_row == end_row {
if let Some(line) = lines.get(start_row) {
let chars: Vec<char> = line.chars().collect();
self.yank_register = chars[start_col..end_col].iter().collect();
}
} else {
let mut yanked = String::new();
for (i, line) in lines.iter().enumerate() {
let chars: Vec<char> = line.chars().collect();
if i == start_row {
yanked.push_str(&chars[start_col..].iter().collect::<String>());
yanked.push('\n');
} else if i == end_row {
yanked.push_str(&chars[..end_col].iter().collect::<String>());
} else if i > start_row && i < end_row {
yanked.push_str(line);
yanked.push('\n');
}
}
self.yank_register = yanked;
}
Transition::Mode(Mode::Normal)
}
Key::Char('h') | Key::Left => {
buffer.move_cursor_back();
Transition::Nop
}
Key::Char('j') | Key::Down => {
buffer.move_cursor_down();
Transition::Nop
}
Key::Char('k') | Key::Up => {
buffer.move_cursor_up();
Transition::Nop
}
Key::Char('l') | Key::Right => {
buffer.move_cursor_forward();
Transition::Nop
}
_ => Transition::Nop,
}
}
fn handle_operator_mode(
&mut self,
input: &Input,
op: char,
buffer: &mut TextBuffer,
) -> Transition {
match input.key {
Key::Esc => Transition::Mode(Mode::Normal),
Key::Char('d') if op == 'd' => {
let (row, _) = buffer.cursor();
self.yank_register = buffer.line(row).cloned().unwrap_or_default();
buffer.delete_line();
Transition::NeedRebuild
}
Key::Char('w') => match op {
'd' => {
buffer.delete_word();
Transition::NeedRebuild
}
'c' => {
buffer.delete_word();
Transition::Mode(Mode::Insert)
}
_ => Transition::Mode(Mode::Normal),
},
Key::Char('$') => match op {
'd' => {
let (row, col) = buffer.cursor();
if let Some(line) = buffer.line(row) {
let chars: Vec<char> = line.chars().collect();
self.yank_register = chars[col..].iter().collect();
}
buffer.delete_line_by_end();
Transition::NeedRebuild
}
'c' => {
let (row, col) = buffer.cursor();
if let Some(line) = buffer.line(row) {
let chars: Vec<char> = line.chars().collect();
self.yank_register = chars[col..].iter().collect();
}
buffer.delete_line_by_end();
Transition::Mode(Mode::Insert)
}
_ => Transition::Mode(Mode::Normal),
},
Key::Char('c') if op == 'c' => {
let (row, _) = buffer.cursor();
self.yank_register = buffer.line(row).cloned().unwrap_or_default();
buffer.delete_line_by_end();
buffer.move_cursor_head();
Transition::Mode(Mode::Insert)
}
_ => Transition::Mode(Mode::Normal),
}
}
}
impl Vim {
pub fn push_snapshot(&mut self, snapshot: Snapshot, cursor: (usize, usize)) {
let snap = Snapshot::with_cursor(snapshot.lines, cursor);
self.history.push(snap);
}
pub fn undo(&mut self) -> Option<Snapshot> {
self.history.undo().cloned()
}
pub fn redo(&mut self) -> Option<Snapshot> {
self.history.redo().cloned()
}
}
pub fn filter_commands(filter: &str) -> Vec<&'static CmdItem> {
if filter.is_empty() {
COMMANDS.iter().collect()
} else {
let filter_lower = filter.to_lowercase();
COMMANDS
.iter()
.filter(|cmd| {
cmd.name.contains(&filter_lower)
|| cmd.name.starts_with(&filter_lower)
|| cmd.desc.contains(&filter_lower)
})
.collect()
}
}
pub fn parse_command(input: &str) -> (&str, &str) {
if let Some(space_pos) = input.find(' ') {
(&input[..space_pos], input[space_pos + 1..].trim())
} else {
(input, "")
}
}