use crate::{Pos, TextBuffer};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Motion {
Left,
Right,
Up,
Down,
FileStart,
FileEnd,
LineStart,
LineEnd,
WordStartBefore,
WordStartAfter,
WordEndAfter,
}
#[inline]
pub fn apply_motion(buffer: &TextBuffer, cursor: Pos, motion: Motion) -> Pos {
let cursor = buffer.clamp_pos(cursor);
match motion {
Motion::Left => buffer.move_left(cursor),
Motion::Right => buffer.move_right(cursor),
Motion::Up => buffer.move_up(cursor),
Motion::Down => buffer.move_down(cursor),
Motion::FileStart => {
let target_col = if cursor.line == 0 { 0 } else { cursor.col };
buffer.clamp_pos(Pos::new(0, target_col))
}
Motion::FileEnd => {
let last = buffer.len_lines().saturating_sub(1);
buffer.clamp_pos(Pos::new(last, cursor.col))
}
Motion::LineStart => Pos::new(cursor.line, 0),
Motion::LineEnd => {
let line = buffer.clamp_line(cursor.line);
let end_col = buffer.line_len_chars(line);
Pos::new(line, end_col)
}
Motion::WordStartBefore => buffer.word_start_before(cursor),
Motion::WordStartAfter => buffer.word_start_after(cursor),
Motion::WordEndAfter => buffer.word_end_after(cursor),
}
}
pub fn apply_motion_n(buffer: &TextBuffer, cursor: Pos, motion: Motion, count: usize) -> Pos {
let mut cur = buffer.clamp_pos(cursor);
for _ in 0..count {
let next = apply_motion(buffer, cur, motion);
if next == cur {
break;
}
cur = next;
}
cur
}
pub mod helpers {
use super::{Motion, apply_motion_n};
use crate::{Pos, TextBuffer};
#[inline]
pub fn word_forward(buffer: &TextBuffer, cursor: Pos, count: usize) -> Pos {
apply_motion_n(buffer, cursor, Motion::WordStartAfter, count)
}
#[inline]
pub fn word_backward(buffer: &TextBuffer, cursor: Pos, count: usize) -> Pos {
apply_motion_n(buffer, cursor, Motion::WordStartBefore, count)
}
#[inline]
pub fn gg(buffer: &TextBuffer, cursor: Pos) -> Pos {
super::apply_motion(buffer, cursor, Motion::FileStart)
}
#[inline]
pub fn file_end(buffer: &TextBuffer, cursor: Pos) -> Pos {
super::apply_motion(buffer, cursor, Motion::FileEnd)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn motion_count_zero_is_noop() {
let b = TextBuffer::from_str("abc\n");
let p = Pos::new(0, 2);
assert_eq!(apply_motion_n(&b, p, Motion::Left, 0), p);
assert_eq!(apply_motion_n(&b, p, Motion::WordEndAfter, 0), p);
}
#[test]
fn gg_goes_to_first_line_and_clamps_column() {
let b = TextBuffer::from_str("a\nbb\nccc\n");
let p = Pos::new(2, 2);
let p2 = apply_motion(&b, p, Motion::FileStart);
assert_eq!(p2.line, 0);
assert_eq!(p2.col, 1);
}
#[test]
fn file_end_goes_to_last_line_and_clamps_column() {
let b = TextBuffer::from_str("a\nbb\nccc\n");
let p = Pos::new(0, 10);
let p2 = apply_motion(&b, p, Motion::FileEnd);
assert_eq!(p2.line, 3);
assert_eq!(p2.col, 0);
}
#[test]
fn line_start_and_line_end_work() {
let b = TextBuffer::from_str("hello\nworld!\n");
let p = Pos::new(1, 2);
let start = apply_motion(&b, p, Motion::LineStart);
assert_eq!(start, Pos::new(1, 0));
let end = apply_motion(&b, p, Motion::LineEnd);
assert_eq!(end, Pos::new(1, 6));
}
#[test]
fn left_right_clamp_at_bounds() {
let b = TextBuffer::from_str("ab\n");
let p0 = Pos::new(0, 0);
assert_eq!(apply_motion(&b, p0, Motion::Left), Pos::new(0, 0));
let p_end = Pos::new(0, 2);
assert_eq!(apply_motion(&b, p_end, Motion::Right), Pos::new(1, 0));
}
#[test]
fn up_down_preserve_column_when_possible() {
let b = TextBuffer::from_str("aaaa\nb\ncccccc\n");
let p = Pos::new(0, 3);
let down = apply_motion(&b, p, Motion::Down);
assert_eq!(down, Pos::new(1, 1));
let down2 = apply_motion(&b, down, Motion::Down);
assert_eq!(down2, Pos::new(2, 1));
let up = apply_motion(&b, down2, Motion::Up);
assert_eq!(up, Pos::new(1, 1));
}
#[test]
fn word_motions_ascii_smoke() {
let b = TextBuffer::from_str("abc def_12!\n");
let p = Pos::new(0, 6);
let start = apply_motion(&b, p, Motion::WordStartBefore);
assert_eq!(start, Pos::new(0, 5));
let end = apply_motion(&b, start, Motion::WordEndAfter);
assert_eq!(end, Pos::new(0, 10));
}
#[test]
fn repeated_word_forward_stops_at_eof() {
let b = TextBuffer::from_str("a b c\n");
let p = Pos::new(0, 0);
let p2 = apply_motion_n(&b, p, Motion::WordEndAfter, 100);
assert_eq!(p2, Pos::new(0, 5));
}
#[test]
fn word_start_after_visits_symbol_tokens() {
let b = TextBuffer::from_str("(normal/insert/command)\n");
let mut p = Pos::new(0, 0);
p = apply_motion(&b, p, Motion::WordStartAfter);
assert_eq!(p, Pos::new(0, 1));
p = apply_motion(&b, p, Motion::WordStartAfter);
assert_eq!(p, Pos::new(0, 7));
p = apply_motion(&b, p, Motion::WordStartAfter);
assert_eq!(p, Pos::new(0, 8));
p = apply_motion(&b, p, Motion::WordStartAfter);
assert_eq!(p, Pos::new(0, 14));
p = apply_motion(&b, p, Motion::WordStartAfter);
assert_eq!(p, Pos::new(0, 15));
p = apply_motion(&b, p, Motion::WordStartAfter);
assert_eq!(p, Pos::new(0, 22)); }
#[test]
fn word_start_before_stops_on_symbol_token() {
let b = TextBuffer::from_str("(normal/insert)\n");
let p = Pos::new(0, 15); let p2 = apply_motion(&b, p, Motion::WordStartBefore);
assert_eq!(p2, Pos::new(0, 14)); }
#[test]
fn word_end_after_can_land_on_symbol_token() {
let b = TextBuffer::from_str("alpha / beta\n");
let p = Pos::new(0, 0);
let p = apply_motion(&b, p, Motion::WordEndAfter);
assert_eq!(p, Pos::new(0, 4));
let p = apply_motion(&b, p, Motion::WordEndAfter);
assert_eq!(p, Pos::new(0, 6)); }
}