use hjkl_vim_types::{LastHorizontalMotion, Mode, Motion};
use hjkl_engine::input::{Input, Key};
use super::*;
use crate::vim_state::{vim, vim_mut};
use hjkl_engine::Editor;
use hjkl_engine::buf_helpers::{
buf_cursor_pos, buf_line, buf_line_chars, buf_row_count, buf_set_cursor_rc,
};
pub fn parse_motion(input: &Input) -> Option<Motion> {
if input.ctrl {
if input.key == Key::Char('h') {
return Some(Motion::BackspaceBack);
}
return None;
}
match input.key {
Key::Char('h') | Key::Left => Some(Motion::Left),
Key::Char('l') | Key::Right => Some(Motion::Right),
Key::Char(' ') => Some(Motion::SpaceFwd),
Key::Backspace => Some(Motion::BackspaceBack),
Key::Char('j') | Key::Down => Some(Motion::Down),
Key::Char('+') | Key::Enter => Some(Motion::FirstNonBlankNextLine),
Key::Char('-') => Some(Motion::FirstNonBlankPrevLine),
Key::Char('_') => Some(Motion::FirstNonBlankLine),
Key::Char('k') | Key::Up => Some(Motion::Up),
Key::Char('w') => Some(Motion::WordFwd),
Key::Char('W') => Some(Motion::BigWordFwd),
Key::Char('b') => Some(Motion::WordBack),
Key::Char('B') => Some(Motion::BigWordBack),
Key::Char('e') => Some(Motion::WordEnd),
Key::Char('E') => Some(Motion::BigWordEnd),
Key::Char('0') | Key::Home => Some(Motion::LineStart),
Key::Char('^') => Some(Motion::FirstNonBlank),
Key::Char('$') | Key::End => Some(Motion::LineEnd),
Key::Char('G') => Some(Motion::FileBottom),
Key::Char('%') => Some(Motion::MatchBracket),
Key::Char(';') => Some(Motion::FindRepeat { reverse: false }),
Key::Char(',') => Some(Motion::FindRepeat { reverse: true }),
Key::Char('*') => Some(Motion::WordAtCursor {
forward: true,
whole_word: true,
}),
Key::Char('#') => Some(Motion::WordAtCursor {
forward: false,
whole_word: true,
}),
Key::Char('n') => Some(Motion::SearchNext { reverse: false }),
Key::Char('N') => Some(Motion::SearchNext { reverse: true }),
Key::Char('H') => Some(Motion::ViewportTop),
Key::Char('M') => Some(Motion::ViewportMiddle),
Key::Char('L') => Some(Motion::ViewportBottom),
Key::Char('{') => Some(Motion::ParagraphPrev),
Key::Char('}') => Some(Motion::ParagraphNext),
Key::Char('(') => Some(Motion::SentencePrev),
Key::Char(')') => Some(Motion::SentenceNext),
Key::Char('|') => Some(Motion::GotoColumn),
_ => None,
}
}
pub(crate) fn execute_motion<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
motion: Motion,
count: usize,
) {
let count = count.clamp(1, MAX_COUNT);
if let Motion::FindRepeat { reverse } = motion
&& vim(ed).last_horizontal_motion == LastHorizontalMotion::Sneak
{
if let Some(((c1, c2), fwd)) = vim(ed).last_sneak {
let effective_fwd = if reverse { !fwd } else { fwd };
apply_sneak(ed, c1, c2, effective_fwd, count);
}
return;
}
let motion = match motion {
Motion::FindRepeat { reverse } => match vim(ed).last_find {
Some((ch, forward, till)) => {
vim_mut(ed).find_repeat_skip = true;
Motion::Find {
ch,
forward: if reverse { !forward } else { forward },
till,
}
}
None => return,
},
other => other,
};
let pre_pos = ed.cursor();
let pre_col = pre_pos.1;
apply_motion_cursor(ed, &motion, count);
let post_pos = ed.cursor();
if is_big_jump(&motion) && pre_pos != post_pos {
ed.push_jump(pre_pos);
}
apply_sticky_col(ed, &motion, pre_col);
ed.sync_buffer_from_textarea();
}
pub(crate) fn execute_motion_with_block_vcol<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
motion: Motion,
count: usize,
) {
let motion_copy = motion.clone();
execute_motion(ed, motion, count);
if vim(ed).mode == Mode::VisualBlock {
update_block_vcol(ed, &motion_copy);
}
}
pub(crate) fn apply_motion_kind<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
kind: hjkl_engine::MotionKind,
count: usize,
) {
let count = count.max(1);
match kind {
hjkl_engine::MotionKind::CharLeft => {
execute_motion_with_block_vcol(ed, Motion::Left, count);
}
hjkl_engine::MotionKind::CharRight => {
execute_motion_with_block_vcol(ed, Motion::Right, count);
}
hjkl_engine::MotionKind::LineDown => {
execute_motion_with_block_vcol(ed, Motion::Down, count);
}
hjkl_engine::MotionKind::LineUp => {
execute_motion_with_block_vcol(ed, Motion::Up, count);
}
hjkl_engine::MotionKind::FirstNonBlankDown => {
let folds = hjkl_engine::SnapshotFoldProvider::from_buffer(ed.buffer());
let mut sticky = ed.sticky_col();
hjkl_engine::motions::move_down(ed.buffer_mut(), &folds, count, &mut sticky);
ed.set_sticky_col(sticky);
hjkl_engine::motions::move_first_non_blank(ed.buffer_mut());
ed.set_sticky_col(Some(buf_cursor_pos(ed.buffer()).col));
ed.sync_buffer_from_textarea();
}
hjkl_engine::MotionKind::FirstNonBlankUp => {
let folds = hjkl_engine::SnapshotFoldProvider::from_buffer(ed.buffer());
let mut sticky = ed.sticky_col();
hjkl_engine::motions::move_up(ed.buffer_mut(), &folds, count, &mut sticky);
ed.set_sticky_col(sticky);
hjkl_engine::motions::move_first_non_blank(ed.buffer_mut());
ed.set_sticky_col(Some(buf_cursor_pos(ed.buffer()).col));
ed.sync_buffer_from_textarea();
}
hjkl_engine::MotionKind::WordForward => {
execute_motion_with_block_vcol(ed, Motion::WordFwd, count);
}
hjkl_engine::MotionKind::BigWordForward => {
execute_motion_with_block_vcol(ed, Motion::BigWordFwd, count);
}
hjkl_engine::MotionKind::WordBackward => {
execute_motion_with_block_vcol(ed, Motion::WordBack, count);
}
hjkl_engine::MotionKind::BigWordBackward => {
execute_motion_with_block_vcol(ed, Motion::BigWordBack, count);
}
hjkl_engine::MotionKind::WordEnd => {
execute_motion_with_block_vcol(ed, Motion::WordEnd, count);
}
hjkl_engine::MotionKind::BigWordEnd => {
execute_motion_with_block_vcol(ed, Motion::BigWordEnd, count);
}
hjkl_engine::MotionKind::LineStart => {
execute_motion_with_block_vcol(ed, Motion::LineStart, 1);
}
hjkl_engine::MotionKind::FirstNonBlank => {
execute_motion_with_block_vcol(ed, Motion::FirstNonBlank, 1);
}
hjkl_engine::MotionKind::GotoLine => {
execute_motion_with_block_vcol(ed, Motion::FileBottom, count);
}
hjkl_engine::MotionKind::LineEnd => {
execute_motion_with_block_vcol(ed, Motion::LineEnd, 1);
}
hjkl_engine::MotionKind::FindRepeat => {
execute_motion_with_block_vcol(ed, Motion::FindRepeat { reverse: false }, count);
}
hjkl_engine::MotionKind::FindRepeatReverse => {
execute_motion_with_block_vcol(ed, Motion::FindRepeat { reverse: true }, count);
}
hjkl_engine::MotionKind::BracketMatch => {
execute_motion_with_block_vcol(ed, Motion::MatchBracket, count);
}
hjkl_engine::MotionKind::ViewportTop => {
execute_motion_with_block_vcol(ed, Motion::ViewportTop, count);
}
hjkl_engine::MotionKind::ViewportMiddle => {
execute_motion_with_block_vcol(ed, Motion::ViewportMiddle, count);
}
hjkl_engine::MotionKind::ViewportBottom => {
execute_motion_with_block_vcol(ed, Motion::ViewportBottom, count);
}
hjkl_engine::MotionKind::HalfPageDown => {
{
let d = ed.viewport_half_rows(count) as isize;
ed.scroll_cursor_rows(d);
}
}
hjkl_engine::MotionKind::HalfPageUp => {
{
let d = -(ed.viewport_half_rows(count) as isize);
ed.scroll_cursor_rows(d);
}
}
hjkl_engine::MotionKind::FullPageDown => {
{
let d = ed.viewport_full_rows(count) as isize;
ed.scroll_cursor_rows(d);
}
}
hjkl_engine::MotionKind::FullPageUp => {
{
let d = -(ed.viewport_full_rows(count) as isize);
ed.scroll_cursor_rows(d);
}
}
hjkl_engine::MotionKind::FirstNonBlankLine => {
execute_motion_with_block_vcol(ed, Motion::FirstNonBlankLine, count);
}
hjkl_engine::MotionKind::SectionBackward => {
execute_motion_with_block_vcol(ed, Motion::SectionBackward, count);
}
hjkl_engine::MotionKind::SectionForward => {
execute_motion_with_block_vcol(ed, Motion::SectionForward, count);
}
hjkl_engine::MotionKind::SectionEndBackward => {
execute_motion_with_block_vcol(ed, Motion::SectionEndBackward, count);
}
hjkl_engine::MotionKind::SectionEndForward => {
execute_motion_with_block_vcol(ed, Motion::SectionEndForward, count);
}
_ => {}
}
}
pub(crate) fn apply_sticky_col<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
motion: &Motion,
pre_col: usize,
) {
if is_vertical_motion(motion) {
let want = ed.sticky_col().unwrap_or(pre_col);
ed.set_sticky_col(Some(want));
let (row, _) = ed.cursor();
let line_len = buf_line_chars(ed.buffer(), row);
let max_col = line_len.saturating_sub(1);
let target = want.min(max_col);
buf_set_cursor_rc(ed.buffer_mut(), row, target);
} else {
ed.set_sticky_col(Some(ed.cursor().1));
}
}
pub(crate) fn is_vertical_motion(motion: &Motion) -> bool {
matches!(
motion,
Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown
)
}
pub(crate) fn apply_motion_cursor<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
motion: &Motion,
count: usize,
) {
apply_motion_cursor_ctx(ed, motion, count, false)
}
pub(crate) fn apply_motion_cursor_ctx<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
motion: &Motion,
count: usize,
as_operator: bool,
) {
let count = count
.min(MAX_COUNT)
.min(ed.buffer().rope().len_chars().saturating_add(1));
match motion {
Motion::Left => {
hjkl_engine::motions::move_left(ed.buffer_mut(), count);
}
Motion::Right => {
if as_operator {
hjkl_engine::motions::move_right_to_end(ed.buffer_mut(), count);
} else {
hjkl_engine::motions::move_right_in_line(ed.buffer_mut(), count);
}
}
Motion::SpaceFwd => {
if as_operator {
hjkl_engine::motions::move_right_to_end(ed.buffer_mut(), count);
} else {
hjkl_engine::motions::move_space_fwd(ed.buffer_mut(), count);
}
}
Motion::BackspaceBack => {
if as_operator {
hjkl_engine::motions::move_left(ed.buffer_mut(), count);
} else {
hjkl_engine::motions::move_backspace_back(ed.buffer_mut(), count);
}
}
Motion::Up => {
let folds = hjkl_engine::SnapshotFoldProvider::from_buffer(ed.buffer());
let mut sticky = ed.sticky_col();
hjkl_engine::motions::move_up(ed.buffer_mut(), &folds, count, &mut sticky);
ed.set_sticky_col(sticky);
}
Motion::Down => {
let folds = hjkl_engine::SnapshotFoldProvider::from_buffer(ed.buffer());
let mut sticky = ed.sticky_col();
hjkl_engine::motions::move_down(ed.buffer_mut(), &folds, count, &mut sticky);
ed.set_sticky_col(sticky);
}
Motion::ScreenUp => {
let v = *ed.host().viewport();
let folds = hjkl_engine::SnapshotFoldProvider::from_buffer(ed.buffer());
let mut sticky = ed.sticky_col();
hjkl_engine::motions::move_screen_up(ed.buffer_mut(), &folds, &v, count, &mut sticky);
ed.set_sticky_col(sticky);
}
Motion::ScreenDown => {
let v = *ed.host().viewport();
let folds = hjkl_engine::SnapshotFoldProvider::from_buffer(ed.buffer());
let mut sticky = ed.sticky_col();
hjkl_engine::motions::move_screen_down(ed.buffer_mut(), &folds, &v, count, &mut sticky);
ed.set_sticky_col(sticky);
}
Motion::WordFwd => {
let iskeyword = ed.settings().iskeyword.clone();
hjkl_engine::motions::move_word_fwd(ed.buffer_mut(), false, count, &iskeyword);
}
Motion::WordBack => {
let iskeyword = ed.settings().iskeyword.clone();
hjkl_engine::motions::move_word_back(ed.buffer_mut(), false, count, &iskeyword);
}
Motion::WordEnd => {
let iskeyword = ed.settings().iskeyword.clone();
hjkl_engine::motions::move_word_end(ed.buffer_mut(), false, count, &iskeyword);
}
Motion::BigWordFwd => {
let iskeyword = ed.settings().iskeyword.clone();
hjkl_engine::motions::move_word_fwd(ed.buffer_mut(), true, count, &iskeyword);
}
Motion::BigWordBack => {
let iskeyword = ed.settings().iskeyword.clone();
hjkl_engine::motions::move_word_back(ed.buffer_mut(), true, count, &iskeyword);
}
Motion::BigWordEnd => {
let iskeyword = ed.settings().iskeyword.clone();
hjkl_engine::motions::move_word_end(ed.buffer_mut(), true, count, &iskeyword);
}
Motion::WordEndBack => {
let iskeyword = ed.settings().iskeyword.clone();
hjkl_engine::motions::move_word_end_back(ed.buffer_mut(), false, count, &iskeyword);
}
Motion::BigWordEndBack => {
let iskeyword = ed.settings().iskeyword.clone();
hjkl_engine::motions::move_word_end_back(ed.buffer_mut(), true, count, &iskeyword);
}
Motion::LineStart => {
hjkl_engine::motions::move_line_start(ed.buffer_mut());
}
Motion::FirstNonBlank => {
hjkl_engine::motions::move_first_non_blank(ed.buffer_mut());
}
Motion::LineEnd => {
hjkl_engine::motions::move_line_end(ed.buffer_mut());
}
Motion::FileTop => {
if count > 1 {
hjkl_engine::motions::move_bottom(ed.buffer_mut(), count);
} else {
hjkl_engine::motions::move_top(ed.buffer_mut());
}
}
Motion::FileBottom => {
if count > 1 {
hjkl_engine::motions::move_bottom(ed.buffer_mut(), count);
} else {
hjkl_engine::motions::move_bottom(ed.buffer_mut(), 0);
}
}
Motion::Find { ch, forward, till } => {
let repeat = std::mem::take(&mut vim_mut(ed).find_repeat_skip);
for i in 0..count {
let skip_adjacent = repeat || i > 0;
if !find_char_on_line(ed, *ch, *forward, *till, skip_adjacent) {
break;
}
}
}
Motion::FindRepeat { .. } => {} Motion::MatchBracket => {
let _ = matching_bracket(ed);
}
Motion::UnmatchedBracket { forward, open } => {
goto_unmatched_bracket(ed, *forward, *open, count);
}
Motion::WordAtCursor {
forward,
whole_word,
} => {
word_at_cursor_search(ed, *forward, *whole_word, count);
}
Motion::SearchNext { reverse } => {
if let Some(pattern) = ed.last_search_pattern() {
ed.push_search_pattern(&pattern);
}
if ed.search_state().pattern.is_none() {
return;
}
let forward = ed.last_search_forward() != *reverse;
for _ in 0..count.max(1) {
if forward {
ed.search_advance_forward(true);
} else {
ed.search_advance_backward(true);
}
}
}
Motion::ViewportTop => {
let v = *ed.host().viewport();
hjkl_engine::motions::move_viewport_top(ed.buffer_mut(), &v, count.saturating_sub(1));
}
Motion::ViewportMiddle => {
let v = *ed.host().viewport();
hjkl_engine::motions::move_viewport_middle(ed.buffer_mut(), &v);
}
Motion::ViewportBottom => {
let v = *ed.host().viewport();
hjkl_engine::motions::move_viewport_bottom(
ed.buffer_mut(),
&v,
count.saturating_sub(1),
);
}
Motion::LastNonBlank => {
hjkl_engine::motions::move_last_non_blank(ed.buffer_mut());
}
Motion::LineMiddle => {
let row = ed.cursor().0;
let line_chars = buf_line_chars(ed.buffer(), row);
let target = line_chars / 2;
ed.jump_cursor(row, target);
}
Motion::ScreenLineMiddle => {
let row = ed.cursor().0;
let width = ed.host().viewport().width as usize;
let last = buf_line_chars(ed.buffer(), row).saturating_sub(1);
let target = (width / 2).min(last);
ed.jump_cursor(row, target);
}
Motion::ParagraphPrev => {
hjkl_engine::motions::move_paragraph_prev(ed.buffer_mut(), count);
}
Motion::ParagraphNext => {
hjkl_engine::motions::move_paragraph_next(ed.buffer_mut(), count);
}
Motion::SentencePrev => {
for _ in 0..count.max(1) {
if let Some((row, col)) = sentence_boundary(ed, false) {
ed.jump_cursor(row, col);
}
}
}
Motion::SentenceNext => {
for _ in 0..count.max(1) {
if let Some((row, col)) = sentence_boundary(ed, true) {
ed.jump_cursor(row, col);
}
}
}
Motion::SectionBackward => {
hjkl_engine::motions::move_section_backward(ed.buffer_mut(), count);
}
Motion::SectionForward => {
hjkl_engine::motions::move_section_forward(ed.buffer_mut(), count);
}
Motion::SectionEndBackward => {
hjkl_engine::motions::move_section_end_backward(ed.buffer_mut(), count);
}
Motion::SectionEndForward => {
hjkl_engine::motions::move_section_end_forward(ed.buffer_mut(), count);
}
Motion::FirstNonBlankNextLine => {
hjkl_engine::motions::move_first_non_blank_next_line(ed.buffer_mut(), count);
}
Motion::FirstNonBlankPrevLine => {
hjkl_engine::motions::move_first_non_blank_prev_line(ed.buffer_mut(), count);
}
Motion::FirstNonBlankLine => {
hjkl_engine::motions::move_first_non_blank_line(ed.buffer_mut(), count);
}
Motion::GotoColumn => {
hjkl_engine::motions::move_goto_column(ed.buffer_mut(), count);
}
}
}
pub(crate) fn move_first_non_whitespace<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
) {
ed.sync_buffer_content_from_textarea();
hjkl_engine::motions::move_first_non_blank(ed.buffer_mut());
}
pub(crate) fn find_char_on_line<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
ch: char,
forward: bool,
till: bool,
skip_adjacent: bool,
) -> bool {
hjkl_engine::motions::find_char_on_line(ed.buffer_mut(), ch, forward, till, skip_adjacent)
}
pub(crate) fn matching_bracket<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
) -> bool {
hjkl_engine::motions::match_bracket(ed.buffer_mut())
}
pub(crate) fn goto_unmatched_bracket<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
forward: bool,
open: char,
count: usize,
) {
let close = match open {
'(' => ')',
'{' => '}',
_ => return,
};
let cursor = buf_cursor_pos(ed.buffer());
let rows = buf_row_count(ed.buffer());
let target = count.max(1);
let mut found = 0usize;
let mut depth = 0i32;
if forward {
let mut r = cursor.row;
let mut from_col = cursor.col + 1;
while r < rows {
let line: Vec<char> = buf_line(ed.buffer(), r)
.unwrap_or_default()
.chars()
.collect();
let mut ci = from_col;
while ci < line.len() {
let ch = line[ci];
if ch == open {
depth += 1;
} else if ch == close {
if depth == 0 {
found += 1;
if found == target {
buf_set_cursor_rc(ed.buffer_mut(), r, ci);
return;
}
} else {
depth -= 1;
}
}
ci += 1;
}
r += 1;
from_col = 0;
}
} else {
let mut r = cursor.row as isize;
let mut from_col = cursor.col as isize - 1;
while r >= 0 {
let line: Vec<char> = buf_line(ed.buffer(), r as usize)
.unwrap_or_default()
.chars()
.collect();
let mut ci = from_col.min(line.len() as isize - 1);
while ci >= 0 {
let ch = line[ci as usize];
if ch == close {
depth += 1;
} else if ch == open {
if depth == 0 {
found += 1;
if found == target {
buf_set_cursor_rc(ed.buffer_mut(), r as usize, ci as usize);
return;
}
} else {
depth -= 1;
}
}
ci -= 1;
}
r -= 1;
from_col = isize::MAX;
}
}
}
pub(crate) fn word_at_cursor_search<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
forward: bool,
whole_word: bool,
count: usize,
) {
let (row, col) = ed.cursor();
let line: String = buf_line(ed.buffer(), row).unwrap_or_default();
let chars: Vec<char> = line.chars().collect();
if chars.is_empty() {
return;
}
let spec = ed.settings().iskeyword.clone();
let is_word = |c: char| is_keyword_char(c, &spec);
let mut start = col.min(chars.len().saturating_sub(1));
if is_word(chars[start]) {
while start > 0 && is_word(chars[start - 1]) {
start -= 1;
}
} else {
while start < chars.len() && !is_word(chars[start]) {
start += 1;
}
if start >= chars.len() {
return;
}
buf_set_cursor_rc(ed.buffer_mut(), row, start);
}
let mut end = start;
while end < chars.len() && is_word(chars[end]) {
end += 1;
}
if end <= start {
return;
}
let word: String = chars[start..end].iter().collect();
let escaped = regex_escape(&word);
let pattern = if whole_word {
format!(r"\b{escaped}\b")
} else {
escaped
};
ed.push_search_pattern(&pattern);
if ed.search_state().pattern.is_none() {
return;
}
ed.set_last_search_pattern_only(Some(pattern));
ed.set_last_search_forward_only(forward);
for _ in 0..count.max(1) {
if forward {
ed.search_advance_forward(true);
} else {
ed.search_advance_backward(true);
}
}
}
pub(crate) fn regex_escape(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
if matches!(
c,
'.' | '+' | '*' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '^' | '$' | '\\'
) {
out.push('\\');
}
out.push(c);
}
out
}