From d013f383a5cd5fd8bbc95fb6301b390b073dbd3c Mon Sep 17 00:00:00 2001 From: VagnoDev Date: Sun, 1 Nov 2026 19:24:10 +0100 Subject: [PATCH] feat: implement Vim cursor navigation - Implemented `gg` to move to the beginning of the buffer. - Implemented `G` to move to the last line. - Implemented `ge` to move to the end of the previous word. - Implemented `e` to move to the end of the current word. --- src/app/actions.rs | 4 ++ src/editor/movement.rs | 122 +++++++++++++++++++++++++++++++++++++++++ src/input/keyboard.rs | 107 +++++++++++++++++++----------------- src/main.rs | 23 +++----- 4 files changed, 193 insertions(+), 63 deletions(-) diff --git a/src/app/actions.rs b/src/app/actions.rs index c67e4f0..b1292ca 100644 --- a/src/app/actions.rs +++ b/src/app/actions.rs @@ -21,5 +21,9 @@ pub enum Action { EndLine, MoveNext, MovePrev, + MoveToWord, + MoveToPrevWordEnd, + MoveToStart, + MoveToEnd, Quit, } \ No newline at end of file diff --git a/src/editor/movement.rs b/src/editor/movement.rs index 41c01ff..840451a 100644 --- a/src/editor/movement.rs +++ b/src/editor/movement.rs @@ -158,6 +158,128 @@ pub fn move_toprev(buffer: &mut Buffer) { } } +pub fn move_to_next_word(buffer: &mut Buffer) { + if buffer.lines.is_empty() { + return; + } + + if buffer.column >= buffer.lines.len() { + return; + } + + let mut col = buffer.column; + let mut pos = buffer.row; + + while col < buffer.lines.len() { + let line: Vec = buffer.lines[col].chars().collect(); + + if line.is_empty() || pos >= line.len() { + col += 1; + pos = 0; + continue; + } + + if char_type(line[pos]) == CharType::Word { + let mut end = pos; + + while end + 1 < line.len() && char_type(line[end + 1]) == CharType::Word { + end += 1; + } + + if end > pos { + buffer.column = col; + buffer.row = end; + return; + } + + pos += 1; + } + + while pos < line.len() && char_type(line[pos]) != CharType::Word { + pos += 1; + } + + if pos < line.len() { + while pos + 1 < line.len() && char_type(line[pos + 1]) == CharType::Word { + pos += 1; + } + + buffer.column = col; + buffer.row = pos; + return; + } + + col += 1; + pos = 0; + } +} + +pub fn moveto_prev_word_end(buffer: &mut Buffer) { + if buffer.lines.is_empty() || buffer.column >= buffer.lines.len() { + return; + } + + let original_row = buffer.row; + let original_col = buffer.column; + + let mut col = original_col; + let mut pos = original_row; + + let current_line: Vec = buffer.lines[col].chars().collect(); + + if pos < current_line.len() && char_type(current_line[pos]) == CharType::Word { + while pos > 0 && char_type(current_line[pos - 1]) == CharType::Word { + pos -= 1; + } + } + + loop { + if pos == 0 { + if col == 0 { + break; + } + + col -= 1; + + let line: Vec = buffer.lines[col].chars().collect(); + pos = line.len(); + + if pos == 0 { + continue; + } + } + + let line: Vec = buffer.lines[col].chars().collect(); + + while pos > 0 && char_type(line[pos - 1]) != CharType::Word { + pos -= 1; + } + + if pos > 0 { + buffer.column = col; + buffer.row = pos - 1; + return; + } + } + + buffer.column = original_col; + buffer.row = original_row; +} + +pub fn move_to_start(buffer: &mut Buffer) { + buffer.row = 0; + buffer.column = 0; +} + +pub fn move_to_end(buffer: &mut Buffer) { + if buffer.lines.is_empty() { + return; + } else { + buffer.row = 0; + buffer.column = buffer.lines.len().saturating_sub(1); + } +} + fn char_type(c: char) -> CharType { if c.is_whitespace() { CharType::Whitespace diff --git a/src/input/keyboard.rs b/src/input/keyboard.rs index 3461ec1..37ee8cf 100644 --- a/src/input/keyboard.rs +++ b/src/input/keyboard.rs @@ -1,73 +1,82 @@ -use crate::app::actions::{Action}; +use crate::app::actions::Action; use crate::app::mode::Mode; -use crossterm::{ - event::{KeyCode, KeyEvent}, -}; - - +use crossterm::event::{KeyCode, KeyEvent}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum PendingKey { None, - Space + Space, + G, } pub fn keybinding(key: KeyEvent, mode: &Mode, pending_key: &mut PendingKey) -> Option { match mode { Mode::Normal => { - match key.code { - KeyCode::Char('h') => Some(Action::MoveLeft), - KeyCode::Char('l') => Some(Action::MoveRight), - KeyCode::Char('j') => Some(Action::MoveDown), - KeyCode::Char('k') => Some(Action::MoveUp), - KeyCode::Char('a') => Some(Action::InsertForward), - KeyCode::Char('i') => Some(Action::InserBackward), - KeyCode::Char(':') => Some(Action::SetCommand), - KeyCode::Char('0') => Some(Action::StartLine), - KeyCode::Char('$') => Some(Action::EndLine), - KeyCode::Char('w') => Some(Action::MoveNext), - KeyCode::Char('b') => Some(Action::MovePrev), - KeyCode::Char(' ') => { - *pending_key = PendingKey::Space; - None - } - KeyCode::Char('e') if *pending_key == PendingKey::Space => { + if *pending_key != PendingKey::None { + let pending = *pending_key; *pending_key = PendingKey::None; - Some(Action::SetExplorer) + + match (pending, key.code) { + (PendingKey::G, KeyCode::Char('e')) => { + return Some(Action::MoveToPrevWordEnd); + } + (PendingKey::G, KeyCode::Char('g')) => { + return Some(Action::MoveToStart); + } + (PendingKey::Space, KeyCode::Char('e')) => { + return Some(Action::SetExplorer); + } + _ => {} + } } - _ => None, - } - } - Mode::Insert => { match key.code { - KeyCode::Esc => Some(Action::SetNormal), - KeyCode::Char(c) => Some(Action::InsertChar(c)), - KeyCode::Backspace => Some(Action::RemoveChar), - KeyCode::Enter => Some(Action::InserNewLine), - _ => None + KeyCode::Char('h') => Some(Action::MoveLeft), + KeyCode::Char('l') => Some(Action::MoveRight), + KeyCode::Char('j') => Some(Action::MoveDown), + KeyCode::Char('k') => Some(Action::MoveUp), + KeyCode::Char('a') => Some(Action::InsertForward), + KeyCode::Char('i') => Some(Action::InserBackward), + KeyCode::Char(':') => Some(Action::SetCommand), + KeyCode::Char('0') => Some(Action::StartLine), + KeyCode::Char('$') => Some(Action::EndLine), + KeyCode::Char('w') => Some(Action::MoveNext), + KeyCode::Char('b') => Some(Action::MovePrev), + KeyCode::Char('e') => Some(Action::MoveToWord), + KeyCode::Char('G') => Some(Action::MoveToEnd), + KeyCode::Char(' ') => { + *pending_key = PendingKey::Space; + None + } + KeyCode::Char('g') => { + *pending_key = PendingKey::G; + None + }, + _ => None, } } - Mode::Command => { - match key.code { - KeyCode::Esc => Some(Action::EscCommand), - KeyCode::Char(char) => Some(Action::InsertChar(char)), - KeyCode::Backspace => Some(Action::RemoveChar), - KeyCode::Enter => Some(Action::SendCommand), - _ => None - } - } - Mode::Explorer => { - match key.code { + Mode::Insert => match key.code { + KeyCode::Esc => Some(Action::SetNormal), + KeyCode::Char(c) => Some(Action::InsertChar(c)), + KeyCode::Backspace => Some(Action::RemoveChar), + KeyCode::Enter => Some(Action::InserNewLine), + _ => None, + }, + Mode::Command => match key.code { + KeyCode::Esc => Some(Action::EscCommand), + KeyCode::Char(char) => Some(Action::InsertChar(char)), + KeyCode::Backspace => Some(Action::RemoveChar), + KeyCode::Enter => Some(Action::SendCommand), + _ => None, + }, + Mode::Explorer => match key.code { KeyCode::Up => Some(Action::ExplorerUp), KeyCode::Down => Some(Action::ExplorerDown), KeyCode::Enter => Some(Action::EnterFile), KeyCode::Esc => Some(Action::SetNormal), KeyCode::Char('j') => Some(Action::ExplorerDown), KeyCode::Char('k') => Some(Action::ExplorerUp), - _ => None - } - } + _ => None, + }, } - } diff --git a/src/main.rs b/src/main.rs index 063e1bb..ff86885 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,23 +1,14 @@ use std::{io::stdout, path::PathBuf, time::Instant}; use crate::{ - Status::Continue, - app::{ + Status::Continue, app::{ messages::MessageLevel, mode::{Mode, set_command, set_normal}, - }, - commands::{executor, parser}, - editor::{ - editing::{insert_backward, insert_newline, remove_char}, - movement::{ - move_down, move_left, move_right, move_toend, move_tonext, move_toprev, move_tostart, - move_up, + }, commands::{executor, parser}, editor::{ + editing::{insert_backward, insert_newline, remove_char}, movement::{ + move_down, move_left, move_right, move_to_end, move_to_next_word, move_to_start, move_toend, move_tonext, move_toprev, move_tostart, move_up, moveto_prev_word_end, }, - }, - explorer::tree::{Explorer, read_dir}, - gui::{display::Display, status_bar::send_message}, - input::keyboard::{PendingKey, keybinding}, - io::save::save, + }, explorer::tree::{Explorer, read_dir}, gui::{display::Display, status_bar::send_message}, input::keyboard::{PendingKey, keybinding}, io::save::save, }; use crossterm::{ event::{ @@ -215,9 +206,13 @@ fn manager( Action::EndLine => move_toend(lines), Action::MoveNext => move_tonext(lines), Action::MovePrev => move_toprev(lines), + Action::MoveToPrevWordEnd => moveto_prev_word_end(lines), Action::SetExplorer => { return Ok(Status::OpenExplorer); } + Action::MoveToWord => move_to_next_word(lines), + Action::MoveToStart => move_to_start(lines), + Action::MoveToEnd => move_to_end(lines), _ => {} }, Mode::Insert => match first {