use hjkl_buffer::{Position, Wrap, is_keyword_char, wrap};
use crate::types::{Cursor, FoldProvider, Pos, Query};
#[inline]
fn read_cursor<B: Cursor + ?Sized>(buf: &B) -> Position {
let p = Cursor::cursor(buf);
Position::new(p.line as usize, p.col as usize)
}
#[inline]
fn write_cursor<B: Cursor + ?Sized>(buf: &mut B, pos: Position) {
Cursor::set_cursor(
buf,
Pos {
line: pos.row as u32,
col: pos.col as u32,
},
);
}
#[inline]
fn read_line<B: Query + ?Sized>(buf: &B, row: usize) -> Option<&str> {
let n = Query::line_count(buf) as usize;
if row >= n {
return None;
}
Some(Query::line(buf, row as u32))
}
#[inline]
fn read_row_count<B: Query + ?Sized>(buf: &B) -> usize {
Query::line_count(buf) as usize
}
fn line_chars(line: &str) -> usize {
line.chars().count()
}
fn last_col(line: &str) -> usize {
line_chars(line).saturating_sub(1)
}
fn clamp_to_segment(start: usize, end: usize, visual_col: usize, line: &str) -> usize {
let line_max = last_col(line);
let seg_max = if end > start { end - 1 } else { start };
let want = start.saturating_add(visual_col);
want.min(seg_max).min(line_max).max(start.min(line_max))
}
pub fn move_left<B: Cursor + Query>(buf: &mut B, count: usize) {
let cursor = read_cursor(buf);
let new_col = cursor.col.saturating_sub(count.max(1));
write_cursor(buf, Position::new(cursor.row, new_col));
}
pub fn move_right_in_line<B: Cursor + Query>(buf: &mut B, count: usize) {
let cursor = read_cursor(buf);
let line = read_line(buf, cursor.row).unwrap_or("");
let limit = last_col(line);
let new_col = (cursor.col + count.max(1)).min(limit);
write_cursor(buf, Position::new(cursor.row, new_col));
}
pub fn move_right_to_end<B: Cursor + Query>(buf: &mut B, count: usize) {
let cursor = read_cursor(buf);
let line = read_line(buf, cursor.row).unwrap_or("");
let limit = line_chars(line);
let new_col = (cursor.col + count.max(1)).min(limit);
write_cursor(buf, Position::new(cursor.row, new_col));
}
pub fn move_line_start<B: Cursor + Query>(buf: &mut B) {
let row = read_cursor(buf).row;
write_cursor(buf, Position::new(row, 0));
}
pub fn move_first_non_blank<B: Cursor + Query>(buf: &mut B) {
let row = read_cursor(buf).row;
let col = read_line(buf, row)
.unwrap_or("")
.chars()
.position(|c| !c.is_whitespace())
.unwrap_or(0);
write_cursor(buf, Position::new(row, col));
}
pub fn move_line_end<B: Cursor + Query>(buf: &mut B) {
let row = read_cursor(buf).row;
let col = last_col(read_line(buf, row).unwrap_or(""));
write_cursor(buf, Position::new(row, col));
}
pub fn move_last_non_blank<B: Cursor + Query>(buf: &mut B) {
let row = read_cursor(buf).row;
let line = read_line(buf, row).unwrap_or("");
let col = line
.char_indices()
.rev()
.find(|(_, c)| !c.is_whitespace())
.map(|(byte, _)| line[..byte].chars().count())
.unwrap_or(0);
write_cursor(buf, Position::new(row, col));
}
pub fn move_paragraph_prev<B: Cursor + Query>(buf: &mut B, count: usize) {
let mut row = read_cursor(buf).row;
for _ in 0..count.max(1) {
if row == 0 {
break;
}
let mut r = row.saturating_sub(1);
while r > 0 && read_line(buf, r).is_some_and(|l| l.is_empty()) {
r -= 1;
}
while r > 0 && read_line(buf, r).is_some_and(|l| !l.is_empty()) {
r -= 1;
}
row = r;
}
write_cursor(buf, Position::new(row, 0));
}
pub fn move_paragraph_next<B: Cursor + Query>(buf: &mut B, count: usize) {
let last = read_row_count(buf).saturating_sub(1);
let mut row = read_cursor(buf).row;
for _ in 0..count.max(1) {
if row >= last {
break;
}
let mut r = row.saturating_add(1);
while r < last && read_line(buf, r).is_some_and(|l| l.is_empty()) {
r += 1;
}
while r < last && read_line(buf, r).is_some_and(|l| !l.is_empty()) {
r += 1;
}
row = r;
}
write_cursor(buf, Position::new(row, 0));
}
pub fn move_up<B: Cursor + Query>(
buf: &mut B,
folds: &dyn FoldProvider,
count: usize,
sticky_col: &mut Option<usize>,
) {
move_vertical(buf, folds, -(count.max(1) as isize), sticky_col);
}
pub fn move_down<B: Cursor + Query>(
buf: &mut B,
folds: &dyn FoldProvider,
count: usize,
sticky_col: &mut Option<usize>,
) {
move_vertical(buf, folds, count.max(1) as isize, sticky_col);
}
pub fn move_screen_up<B: Cursor + Query>(
buf: &mut B,
folds: &dyn FoldProvider,
viewport: &hjkl_buffer::Viewport,
count: usize,
sticky_col: &mut Option<usize>,
) {
move_screen_vertical(buf, folds, viewport, -(count.max(1) as isize), sticky_col);
}
pub fn move_screen_down<B: Cursor + Query>(
buf: &mut B,
folds: &dyn FoldProvider,
viewport: &hjkl_buffer::Viewport,
count: usize,
sticky_col: &mut Option<usize>,
) {
move_screen_vertical(buf, folds, viewport, count.max(1) as isize, sticky_col);
}
pub fn move_top<B: Cursor + Query>(buf: &mut B) {
write_cursor(buf, Position::new(0, 0));
move_first_non_blank(buf);
}
pub fn move_bottom<B: Cursor + Query>(buf: &mut B, count: usize) {
let last = read_row_count(buf).saturating_sub(1);
let target = if count == 0 {
last
} else {
(count - 1).min(last)
};
write_cursor(buf, Position::new(target, 0));
move_first_non_blank(buf);
}
pub fn move_word_fwd<B: Cursor + Query>(buf: &mut B, big: bool, count: usize, iskeyword: &str) {
for _ in 0..count.max(1) {
let from = read_cursor(buf);
if let Some(next) = next_word_start(buf, from, big, iskeyword) {
write_cursor(buf, next);
} else {
break;
}
}
}
pub fn move_word_back<B: Cursor + Query>(buf: &mut B, big: bool, count: usize, iskeyword: &str) {
for _ in 0..count.max(1) {
let from = read_cursor(buf);
if let Some(prev) = prev_word_start(buf, from, big, iskeyword) {
write_cursor(buf, prev);
} else {
break;
}
}
}
pub fn move_word_end<B: Cursor + Query>(buf: &mut B, big: bool, count: usize, iskeyword: &str) {
for _ in 0..count.max(1) {
let from = read_cursor(buf);
if let Some(end) = next_word_end(buf, from, big, iskeyword) {
write_cursor(buf, end);
} else {
break;
}
}
}
pub fn move_word_end_back<B: Cursor + Query>(
buf: &mut B,
big: bool,
count: usize,
iskeyword: &str,
) {
for _ in 0..count.max(1) {
let from = read_cursor(buf);
match prev_word_end(buf, from, big, iskeyword) {
Some(p) => write_cursor(buf, p),
None => break,
}
}
}
pub fn match_bracket<B: Cursor + Query>(buf: &mut B) -> bool {
let cursor = read_cursor(buf);
let line = match read_line(buf, cursor.row) {
Some(l) => l,
None => return false,
};
let ch = match line.chars().nth(cursor.col) {
Some(c) => c,
None => return false,
};
let (open, close, forward) = match ch {
'(' => ('(', ')', true),
')' => ('(', ')', false),
'[' => ('[', ']', true),
']' => ('[', ']', false),
'{' => ('{', '}', true),
'}' => ('{', '}', false),
'<' => ('<', '>', true),
'>' => ('<', '>', false),
_ => return false,
};
let mut depth: i32 = 0;
let row_count = read_row_count(buf);
if forward {
let mut r = cursor.row;
let mut c = cursor.col;
loop {
let chars: Vec<char> = read_line(buf, r).unwrap_or("").chars().collect();
while c < chars.len() {
let here = chars[c];
if here == open {
depth += 1;
} else if here == close {
depth -= 1;
if depth == 0 {
write_cursor(buf, Position::new(r, c));
return true;
}
}
c += 1;
}
if r + 1 >= row_count {
return false;
}
r += 1;
c = 0;
}
} else {
let mut r = cursor.row;
let mut c = cursor.col as isize;
loop {
let chars: Vec<char> = read_line(buf, r).unwrap_or("").chars().collect();
while c >= 0 {
let here = chars[c as usize];
if here == close {
depth += 1;
} else if here == open {
depth -= 1;
if depth == 0 {
write_cursor(buf, Position::new(r, c as usize));
return true;
}
}
c -= 1;
}
if r == 0 {
return false;
}
r -= 1;
c = read_line(buf, r).unwrap_or("").chars().count() as isize - 1;
}
}
}
pub fn find_char_on_line<B: Cursor + Query>(
buf: &mut B,
ch: char,
forward: bool,
till: bool,
) -> bool {
let cursor = read_cursor(buf);
let line = match read_line(buf, cursor.row) {
Some(l) => l,
None => return false,
};
let chars: Vec<char> = line.chars().collect();
if chars.is_empty() {
return false;
}
let target_col = if forward {
chars
.iter()
.enumerate()
.skip(cursor.col + 1)
.find(|(_, c)| **c == ch)
.map(|(i, _)| if till { i.saturating_sub(1) } else { i })
} else {
(0..cursor.col)
.rev()
.find(|&i| chars[i] == ch)
.map(|i| if till { i + 1 } else { i })
};
match target_col {
Some(col) => {
write_cursor(buf, Position::new(cursor.row, col));
true
}
None => false,
}
}
pub fn move_viewport_top<B: Cursor + Query>(
buf: &mut B,
viewport: &hjkl_buffer::Viewport,
offset: usize,
) {
let last = read_row_count(buf).saturating_sub(1);
let target = viewport.top_row.saturating_add(offset).min(last);
write_cursor(buf, Position::new(target, 0));
move_first_non_blank(buf);
}
pub fn move_viewport_middle<B: Cursor + Query>(buf: &mut B, viewport: &hjkl_buffer::Viewport) {
let last = read_row_count(buf).saturating_sub(1);
let height = viewport.height as usize;
let visible_bot = viewport
.top_row
.saturating_add(height.saturating_sub(1))
.min(last);
let mid = viewport.top_row + (visible_bot - viewport.top_row) / 2;
write_cursor(buf, Position::new(mid, 0));
move_first_non_blank(buf);
}
pub fn move_viewport_bottom<B: Cursor + Query>(
buf: &mut B,
viewport: &hjkl_buffer::Viewport,
offset: usize,
) {
let last = read_row_count(buf).saturating_sub(1);
let height = viewport.height as usize;
let visible_bot = viewport
.top_row
.saturating_add(height.saturating_sub(1))
.min(last);
let target = visible_bot.saturating_sub(offset).max(viewport.top_row);
write_cursor(buf, Position::new(target, 0));
move_first_non_blank(buf);
}
fn move_screen_vertical<B: Cursor + Query>(
buf: &mut B,
folds: &dyn FoldProvider,
viewport: &hjkl_buffer::Viewport,
delta: isize,
sticky_col: &mut Option<usize>,
) {
if matches!(viewport.wrap, Wrap::None) || viewport.text_width == 0 {
move_vertical(buf, folds, delta, sticky_col);
return;
}
let cursor = read_cursor(buf);
let line = read_line(buf, cursor.row).unwrap_or("");
let segs = wrap::wrap_segments(line, viewport.text_width, viewport.wrap);
let seg_idx = wrap::segment_for_col(&segs, cursor.col);
let visual_col = cursor.col.saturating_sub(segs[seg_idx].0);
let down = delta > 0;
for _ in 0..delta.unsigned_abs() {
if !step_screen(buf, folds, viewport, down, visual_col) {
break;
}
}
*sticky_col = Some(read_cursor(buf).col);
}
fn step_screen<B: Cursor + Query>(
buf: &mut B,
folds: &dyn FoldProvider,
viewport: &hjkl_buffer::Viewport,
down: bool,
visual_col: usize,
) -> bool {
let cursor = read_cursor(buf);
let line = read_line(buf, cursor.row).unwrap_or("");
let segs = wrap::wrap_segments(line, viewport.text_width, viewport.wrap);
let seg_idx = wrap::segment_for_col(&segs, cursor.col);
let row_count = read_row_count(buf);
if down {
if seg_idx + 1 < segs.len() {
let (s, e) = segs[seg_idx + 1];
let target = clamp_to_segment(s, e, visual_col, line);
write_cursor(buf, Position::new(cursor.row, target));
return true;
}
let Some(next_row) = folds.next_visible_row(cursor.row, row_count) else {
return false;
};
let next_line = read_line(buf, next_row).unwrap_or("");
let next_segs = wrap::wrap_segments(next_line, viewport.text_width, viewport.wrap);
let (s, e) = next_segs[0];
let target = clamp_to_segment(s, e, visual_col, next_line);
write_cursor(buf, Position::new(next_row, target));
true
} else {
if seg_idx > 0 {
let (s, e) = segs[seg_idx - 1];
let target = clamp_to_segment(s, e, visual_col, line);
write_cursor(buf, Position::new(cursor.row, target));
return true;
}
let Some(prev_row) = folds.prev_visible_row(cursor.row) else {
return false;
};
let prev_line = read_line(buf, prev_row).unwrap_or("");
let prev_segs = wrap::wrap_segments(prev_line, viewport.text_width, viewport.wrap);
let (s, e) = *prev_segs.last().unwrap_or(&(0, 0));
let target = clamp_to_segment(s, e, visual_col, prev_line);
write_cursor(buf, Position::new(prev_row, target));
true
}
}
fn move_vertical<B: Cursor + Query>(
buf: &mut B,
folds: &dyn FoldProvider,
delta: isize,
sticky_col: &mut Option<usize>,
) {
let cursor = read_cursor(buf);
let want = sticky_col.unwrap_or(cursor.col);
*sticky_col = Some(want);
let mut target_row = cursor.row;
let row_count = read_row_count(buf);
if delta < 0 {
for _ in 0..(-delta) as usize {
match folds.prev_visible_row(target_row) {
Some(r) => target_row = r,
None => break,
}
}
} else {
for _ in 0..delta as usize {
match folds.next_visible_row(target_row, row_count) {
Some(r) => target_row = r,
None => break,
}
}
}
let line = read_line(buf, target_row).unwrap_or("");
let max_col = last_col(line);
let target_col = want.min(max_col);
write_cursor(buf, Position::new(target_row, target_col));
}
fn is_word(c: char, spec: &str) -> bool {
is_keyword_char(c, spec)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CharKind {
Word,
Punct,
Space,
}
fn char_kind(c: char, big: bool, iskeyword: &str) -> CharKind {
if c.is_whitespace() {
CharKind::Space
} else if big || is_word(c, iskeyword) {
CharKind::Word
} else {
CharKind::Punct
}
}
fn step_forward<B: Query + ?Sized>(buf: &B, pos: Position) -> Option<Position> {
let line = read_line(buf, pos.row)?;
let len = line_chars(line);
if pos.col + 1 < len {
return Some(Position::new(pos.row, pos.col + 1));
}
if pos.row + 1 < read_row_count(buf) {
return Some(Position::new(pos.row + 1, 0));
}
None
}
fn step_back<B: Query + ?Sized>(buf: &B, pos: Position) -> Option<Position> {
if pos.col > 0 {
return Some(Position::new(pos.row, pos.col - 1));
}
if pos.row == 0 {
return None;
}
let prev_row = pos.row - 1;
let prev_len = line_chars(read_line(buf, prev_row).unwrap_or(""));
Some(Position::new(prev_row, prev_len.saturating_sub(1)))
}
fn char_at<B: Query + ?Sized>(buf: &B, pos: Position) -> Option<char> {
read_line(buf, pos.row)?.chars().nth(pos.col)
}
fn next_word_start<B: Query + ?Sized>(
buf: &B,
from: Position,
big: bool,
iskeyword: &str,
) -> Option<Position> {
let start_kind = char_at(buf, from).map(|c| char_kind(c, big, iskeyword));
let mut cur = from;
if let Some(kind) = start_kind {
while char_at(buf, cur).map(|c| char_kind(c, big, iskeyword)) == Some(kind) {
let prev_row = cur.row;
match step_forward(buf, cur) {
Some(next) => {
cur = next;
if next.row != prev_row {
break;
}
}
None => return Some(end_of_buffer(buf)),
}
}
}
while char_at(buf, cur).map(|c| char_kind(c, big, iskeyword)) == Some(CharKind::Space) {
match step_forward(buf, cur) {
Some(next) => cur = next,
None => return Some(end_of_buffer(buf)),
}
}
Some(cur)
}
fn end_of_buffer<B: Query + ?Sized>(buf: &B) -> Position {
let last_row = read_row_count(buf).saturating_sub(1);
let last_line = read_line(buf, last_row).unwrap_or("");
Position::new(last_row, line_chars(last_line))
}
fn prev_word_start<B: Query + ?Sized>(
buf: &B,
from: Position,
big: bool,
iskeyword: &str,
) -> Option<Position> {
let mut cur = step_back(buf, from)?;
while char_at(buf, cur).map(|c| char_kind(c, big, iskeyword)) == Some(CharKind::Space) {
cur = step_back(buf, cur)?;
}
let target_kind = char_at(buf, cur).map(|c| char_kind(c, big, iskeyword))?;
loop {
let Some(prev) = step_back(buf, cur) else {
return Some(cur);
};
if char_at(buf, prev).map(|c| char_kind(c, big, iskeyword)) == Some(target_kind) {
cur = prev;
} else {
return Some(cur);
}
}
}
fn prev_word_end<B: Query + ?Sized>(
buf: &B,
from: Position,
big: bool,
iskeyword: &str,
) -> Option<Position> {
let mut cur = step_back(buf, from)?;
loop {
if char_at(buf, cur).map(|c| char_kind(c, big, iskeyword)) == Some(CharKind::Space) {
cur = step_back(buf, cur)?;
continue;
}
let here = char_kind_or_space(buf, cur, big, iskeyword);
let next = next_char_kind_in_row(buf, cur, big, iskeyword);
let same = if big {
here != CharKind::Space && next != CharKind::Space
} else {
here == next
};
if !same {
return Some(cur);
}
cur = step_back(buf, cur)?;
}
}
fn char_kind_or_space<B: Query + ?Sized>(
buf: &B,
pos: Position,
big: bool,
iskeyword: &str,
) -> CharKind {
char_at(buf, pos)
.map(|c| char_kind(c, big, iskeyword))
.unwrap_or(CharKind::Space)
}
fn next_char_kind_in_row<B: Query + ?Sized>(
buf: &B,
pos: Position,
big: bool,
iskeyword: &str,
) -> CharKind {
let line = read_line(buf, pos.row).unwrap_or("");
let len = line_chars(line);
if pos.col + 1 < len {
char_kind_or_space(buf, Position::new(pos.row, pos.col + 1), big, iskeyword)
} else {
CharKind::Space
}
}
fn next_word_end<B: Query + ?Sized>(
buf: &B,
from: Position,
big: bool,
iskeyword: &str,
) -> Option<Position> {
let mut cur = step_forward(buf, from)?;
while char_at(buf, cur).map(|c| char_kind(c, big, iskeyword)) == Some(CharKind::Space) {
cur = step_forward(buf, cur)?;
}
let kind = char_at(buf, cur).map(|c| char_kind(c, big, iskeyword))?;
loop {
let Some(next) = step_forward(buf, cur) else {
return Some(cur);
};
if char_at(buf, next).map(|c| char_kind(c, big, iskeyword)) == Some(kind) {
cur = next;
} else {
return Some(cur);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use hjkl_buffer::Buffer;
use crate::buffer_impl::SnapshotFoldProvider;
const ISK: &str = "@,48-57,_,192-255";
fn at(b: &Buffer) -> Position {
b.cursor()
}
fn folds(b: &Buffer) -> SnapshotFoldProvider {
SnapshotFoldProvider::from_buffer(b)
}
#[test]
fn move_left_clamps_at_zero() {
let mut b = Buffer::from_str("abcd");
move_right_in_line(&mut b, 3);
assert_eq!(at(&b), Position::new(0, 3));
move_left(&mut b, 10);
assert_eq!(at(&b), Position::new(0, 0));
}
#[test]
fn move_left_does_not_wrap_to_prev_row() {
let mut b = Buffer::from_str("abc\ndef");
let mut sticky = None;
{
let f = folds(&b);
move_down(&mut b, &f, 1, &mut sticky);
}
assert_eq!(at(&b).row, 1);
move_left(&mut b, 99);
assert_eq!(at(&b), Position::new(1, 0));
}
#[test]
fn move_right_in_line_stops_at_last_char() {
let mut b = Buffer::from_str("abcd");
move_right_in_line(&mut b, 99);
assert_eq!(at(&b), Position::new(0, 3));
}
#[test]
fn move_right_to_end_allows_one_past() {
let mut b = Buffer::from_str("abcd");
move_right_to_end(&mut b, 99);
assert_eq!(at(&b), Position::new(0, 4));
}
#[test]
fn move_line_start_end() {
let mut b = Buffer::from_str(" hello");
move_line_end(&mut b);
assert_eq!(at(&b), Position::new(0, 6));
move_line_start(&mut b);
assert_eq!(at(&b), Position::new(0, 0));
move_first_non_blank(&mut b);
assert_eq!(at(&b), Position::new(0, 2));
}
#[test]
fn move_line_end_on_empty_row_stays_at_zero() {
let mut b = Buffer::from_str("");
move_line_end(&mut b);
assert_eq!(at(&b), Position::new(0, 0));
}
#[test]
fn move_down_preserves_sticky_col_across_short_row() {
let mut b = Buffer::from_str("hello world\nhi\nlong line again");
move_right_in_line(&mut b, 7);
assert_eq!(at(&b), Position::new(0, 7));
let mut sticky = None;
{
let f = folds(&b);
move_down(&mut b, &f, 1, &mut sticky);
}
assert_eq!(at(&b).row, 1);
assert_eq!(at(&b).col, 1);
{
let f = folds(&b);
move_down(&mut b, &f, 1, &mut sticky);
}
assert_eq!(at(&b), Position::new(2, 7));
}
#[test]
fn move_down_skips_closed_fold() {
let mut b = Buffer::from_str("a\nb\nc\nd\ne");
b.add_fold(1, 3, true);
let mut sticky = None;
{
let f = folds(&b);
move_down(&mut b, &f, 1, &mut sticky);
}
assert_eq!(at(&b).row, 1);
{
let f = folds(&b);
move_down(&mut b, &f, 1, &mut sticky);
}
assert_eq!(at(&b).row, 4);
}
#[test]
fn move_up_skips_closed_fold() {
let mut b = Buffer::from_str("a\nb\nc\nd\ne");
b.add_fold(1, 3, true);
b.set_cursor(Position::new(4, 0));
let mut sticky = None;
{
let f = folds(&b);
move_up(&mut b, &f, 1, &mut sticky);
}
assert_eq!(at(&b).row, 1);
{
let f = folds(&b);
move_up(&mut b, &f, 1, &mut sticky);
}
assert_eq!(at(&b).row, 0);
}
#[test]
fn open_fold_is_walked_normally() {
let mut b = Buffer::from_str("a\nb\nc\nd\ne");
b.add_fold(1, 3, false);
let mut sticky = None;
{
let f = folds(&b);
move_down(&mut b, &f, 2, &mut sticky);
}
assert_eq!(at(&b).row, 2);
}
#[test]
fn move_top_lands_on_first_non_blank() {
let mut b = Buffer::from_str(" indented\nrow2");
let mut sticky = None;
{
let f = folds(&b);
move_down(&mut b, &f, 1, &mut sticky);
}
move_top(&mut b);
assert_eq!(at(&b), Position::new(0, 4));
}
#[test]
fn move_bottom_with_count_jumps_to_line() {
let mut b = Buffer::from_str("a\n b\nc\nd");
move_bottom(&mut b, 2);
assert_eq!(at(&b), Position::new(1, 2));
}
#[test]
fn move_bottom_zero_jumps_to_last_row() {
let mut b = Buffer::from_str("a\nb\nc");
move_bottom(&mut b, 0);
assert_eq!(at(&b), Position::new(2, 0));
}
#[test]
fn move_word_fwd_skips_whitespace_runs() {
let mut b = Buffer::from_str("foo bar baz");
move_word_fwd(&mut b, false, 1, ISK);
assert_eq!(at(&b), Position::new(0, 4));
move_word_fwd(&mut b, false, 1, ISK);
assert_eq!(at(&b), Position::new(0, 9));
}
#[test]
fn move_word_fwd_separates_word_from_punct_in_small_w() {
let mut b = Buffer::from_str("foo.bar");
move_word_fwd(&mut b, false, 1, ISK);
assert_eq!(at(&b), Position::new(0, 3));
move_word_fwd(&mut b, false, 1, ISK);
assert_eq!(at(&b), Position::new(0, 4));
}
#[test]
fn move_word_fwd_big_collapses_word_and_punct() {
let mut b = Buffer::from_str("foo.bar baz");
move_word_fwd(&mut b, true, 1, ISK);
assert_eq!(at(&b), Position::new(0, 8));
}
#[test]
fn move_word_back_lands_on_word_start() {
let mut b = Buffer::from_str("foo bar baz");
move_line_end(&mut b);
assert_eq!(at(&b), Position::new(0, 10));
move_word_back(&mut b, false, 1, ISK);
assert_eq!(at(&b), Position::new(0, 8));
move_word_back(&mut b, false, 2, ISK);
assert_eq!(at(&b), Position::new(0, 0));
}
#[test]
fn move_word_end_lands_on_last_char() {
let mut b = Buffer::from_str("foo bar");
move_word_end(&mut b, false, 1, ISK);
assert_eq!(at(&b), Position::new(0, 2));
move_word_end(&mut b, false, 1, ISK);
assert_eq!(at(&b), Position::new(0, 6));
}
#[test]
fn find_char_forward_lands_on_match() {
let mut b = Buffer::from_str("foo,bar,baz");
assert!(find_char_on_line(&mut b, ',', true, false));
assert_eq!(at(&b), Position::new(0, 3));
assert!(find_char_on_line(&mut b, ',', true, false));
assert_eq!(at(&b), Position::new(0, 7));
}
#[test]
fn find_char_till_stops_one_short() {
let mut b = Buffer::from_str("foo,bar");
assert!(find_char_on_line(&mut b, ',', true, true));
assert_eq!(at(&b), Position::new(0, 2));
}
#[test]
fn find_char_backward_lands_on_match() {
let mut b = Buffer::from_str("foo,bar,baz");
b.set_cursor(Position::new(0, 10));
assert!(find_char_on_line(&mut b, ',', false, false));
assert_eq!(at(&b), Position::new(0, 7));
}
#[test]
fn find_char_no_match_returns_false() {
let mut b = Buffer::from_str("hello");
assert!(!find_char_on_line(&mut b, 'z', true, false));
assert_eq!(at(&b), Position::new(0, 0));
}
#[test]
fn move_viewport_top_with_offset() {
let mut b = Buffer::from_str("a\nb\nc\nd\ne\nf");
let v = hjkl_buffer::Viewport {
top_row: 1,
height: 4,
..Default::default()
};
move_viewport_top(&mut b, &v, 2);
assert_eq!(at(&b), Position::new(3, 0));
}
#[test]
fn move_viewport_middle_picks_center_of_visible() {
let mut b = Buffer::from_str("a\nb\nc\nd\ne");
let v = hjkl_buffer::Viewport {
top_row: 0,
height: 5,
..Default::default()
};
move_viewport_middle(&mut b, &v);
assert_eq!(at(&b), Position::new(2, 0));
}
#[test]
fn move_viewport_bottom_with_offset() {
let mut b = Buffer::from_str("a\nb\nc\nd\ne");
let v = hjkl_buffer::Viewport {
top_row: 0,
height: 5,
..Default::default()
};
move_viewport_bottom(&mut b, &v, 1);
assert_eq!(at(&b), Position::new(3, 0));
}
#[test]
fn move_word_end_back_lands_on_prev_word_end() {
let mut b = Buffer::from_str("foo bar baz");
b.set_cursor(Position::new(0, 9));
move_word_end_back(&mut b, false, 1, ISK);
assert_eq!(at(&b), Position::new(0, 6));
move_word_end_back(&mut b, false, 1, ISK);
assert_eq!(at(&b), Position::new(0, 2));
}
#[test]
fn move_word_end_back_big_skips_punct() {
let mut b = Buffer::from_str("foo-bar qux");
b.set_cursor(Position::new(0, 10));
move_word_end_back(&mut b, true, 1, ISK);
assert_eq!(at(&b), Position::new(0, 6));
}
#[test]
fn move_word_end_back_crosses_lines() {
let mut b = Buffer::from_str("abc\ndef");
b.set_cursor(Position::new(1, 2));
move_word_end_back(&mut b, false, 1, ISK);
assert_eq!(at(&b), Position::new(0, 2));
}
#[test]
fn match_bracket_pairs_within_line() {
let mut b = Buffer::from_str("if (x + y) {");
b.set_cursor(Position::new(0, 3));
assert!(match_bracket(&mut b));
assert_eq!(at(&b), Position::new(0, 9));
assert!(match_bracket(&mut b));
assert_eq!(at(&b), Position::new(0, 3));
}
#[test]
fn match_bracket_handles_nesting() {
let mut b = Buffer::from_str("((x))");
b.set_cursor(Position::new(0, 0));
assert!(match_bracket(&mut b));
assert_eq!(at(&b), Position::new(0, 4));
}
#[test]
fn match_bracket_crosses_lines() {
let mut b = Buffer::from_str("{\n x\n}");
b.set_cursor(Position::new(0, 0));
assert!(match_bracket(&mut b));
assert_eq!(at(&b), Position::new(2, 0));
}
#[test]
fn match_bracket_returns_false_off_bracket() {
let mut b = Buffer::from_str("hello");
assert!(!match_bracket(&mut b));
}
#[test]
fn motion_count_zero_treated_as_one() {
let mut b = Buffer::from_str("abcd");
move_right_in_line(&mut b, 0);
assert_eq!(at(&b), Position::new(0, 1));
}
fn make_wrap_viewport(mode: Wrap, text_width: u16) -> hjkl_buffer::Viewport {
hjkl_buffer::Viewport {
top_row: 0,
top_col: 0,
width: text_width,
height: 10,
wrap: mode,
text_width,
}
}
#[test]
fn screen_down_falls_back_to_move_down_when_wrap_off() {
let mut b = Buffer::from_str("a\nb\nc");
let v = hjkl_buffer::Viewport::default();
let mut sticky = None;
{
let f = folds(&b);
move_screen_down(&mut b, &f, &v, 1, &mut sticky);
}
assert_eq!(at(&b), Position::new(1, 0));
{
let f = folds(&b);
move_screen_down(&mut b, &f, &v, 1, &mut sticky);
}
assert_eq!(at(&b), Position::new(2, 0));
}
#[test]
fn screen_down_walks_within_wrapped_row() {
let mut b = Buffer::from_str("aaaabbbbcccc\nx");
let v = make_wrap_viewport(Wrap::Char, 4);
b.set_cursor(Position::new(0, 1));
let mut sticky = None;
{
let f = folds(&b);
move_screen_down(&mut b, &f, &v, 1, &mut sticky);
}
assert_eq!(at(&b), Position::new(0, 5));
{
let f = folds(&b);
move_screen_down(&mut b, &f, &v, 1, &mut sticky);
}
assert_eq!(at(&b), Position::new(0, 9));
{
let f = folds(&b);
move_screen_down(&mut b, &f, &v, 1, &mut sticky);
}
assert_eq!(at(&b), Position::new(1, 0));
}
#[test]
fn screen_up_walks_within_wrapped_row() {
let mut b = Buffer::from_str("aaaabbbbcccc");
let v = make_wrap_viewport(Wrap::Char, 4);
b.set_cursor(Position::new(0, 9));
let mut sticky = None;
{
let f = folds(&b);
move_screen_up(&mut b, &f, &v, 1, &mut sticky);
}
assert_eq!(at(&b), Position::new(0, 5));
{
let f = folds(&b);
move_screen_up(&mut b, &f, &v, 1, &mut sticky);
}
assert_eq!(at(&b), Position::new(0, 1));
{
let f = folds(&b);
move_screen_up(&mut b, &f, &v, 1, &mut sticky);
}
assert_eq!(at(&b), Position::new(0, 1));
}
#[test]
fn screen_down_clamps_to_short_segment() {
let mut b = Buffer::from_str("aaaaaabb\nx");
let v = make_wrap_viewport(Wrap::Char, 6);
b.set_cursor(Position::new(0, 4));
let mut sticky = None;
{
let f = folds(&b);
move_screen_down(&mut b, &f, &v, 1, &mut sticky);
}
assert_eq!(at(&b), Position::new(0, 7));
{
let f = folds(&b);
move_screen_down(&mut b, &f, &v, 1, &mut sticky);
}
assert_eq!(at(&b), Position::new(1, 0));
}
#[test]
fn screen_down_count_compounds() {
let mut b = Buffer::from_str("aaaabbbbcccc");
let v = make_wrap_viewport(Wrap::Char, 4);
b.set_cursor(Position::new(0, 0));
let mut sticky = None;
{
let f = folds(&b);
move_screen_down(&mut b, &f, &v, 2, &mut sticky);
}
assert_eq!(at(&b), Position::new(0, 8));
}
#[test]
fn motions_module_compiles_against_concrete_buffer() {
let mut b = Buffer::from_str("hello");
super::move_right_in_line(&mut b, 1);
assert_eq!(b.cursor(), Position::new(0, 1));
}
#[test]
fn motions_drive_non_canonical_cursor_query_impl() {
use std::borrow::Cow;
struct MockBuf {
lines: Vec<String>,
cursor: Pos,
}
impl crate::types::Cursor for MockBuf {
fn cursor(&self) -> Pos {
self.cursor
}
fn set_cursor(&mut self, pos: Pos) {
self.cursor = pos;
}
fn byte_offset(&self, _pos: Pos) -> usize {
0
}
fn pos_at_byte(&self, _byte: usize) -> Pos {
Pos::ORIGIN
}
}
impl crate::types::Query for MockBuf {
fn line_count(&self) -> u32 {
self.lines.len() as u32
}
fn line(&self, idx: u32) -> &str {
&self.lines[idx as usize]
}
fn len_bytes(&self) -> usize {
self.lines
.iter()
.map(|l| l.len() + 1)
.sum::<usize>()
.saturating_sub(1)
}
fn slice(&self, _range: core::ops::Range<Pos>) -> Cow<'_, str> {
Cow::Borrowed("")
}
}
let mut m = MockBuf {
lines: vec!["foo bar".into(), "baz qux".into()],
cursor: Pos::ORIGIN,
};
super::move_right_in_line(&mut m, 2);
assert_eq!(m.cursor, Pos::new(0, 2));
super::move_left(&mut m, 1);
assert_eq!(m.cursor, Pos::new(0, 1));
super::move_line_end(&mut m);
assert_eq!(m.cursor, Pos::new(0, 6));
super::move_line_start(&mut m);
assert_eq!(m.cursor, Pos::new(0, 0));
super::move_word_fwd(&mut m, false, 1, ISK);
assert_eq!(m.cursor, Pos::new(0, 4));
super::move_bottom(&mut m, 0);
assert_eq!(m.cursor.line, 1);
super::move_top(&mut m);
assert_eq!(m.cursor, Pos::new(0, 0));
}
}