mod bracket;
mod find;
mod viewport;
mod word;
use super::{Buffer, Cursor, is_blank_line};
use crate::action::MotionKind;
use bracket::bracket_match;
use find::find_char;
use viewport::{page_target, viewport_target};
use word::{
big_word_back, big_word_forward, word_back_char_class, word_end_back, word_end_char_class,
word_forward_char_class,
};
impl Buffer {
pub fn motion_target(&self, from: Cursor, motion: MotionKind, count: u32) -> Cursor {
let mut c = from;
for _ in 0..count.max(1) {
c = self.motion_step(c, motion);
}
c
}
fn motion_step(&self, from: Cursor, motion: MotionKind) -> Cursor {
use MotionKind as M;
match motion {
M::Left => Cursor {
row: from.row,
col: from.col.saturating_sub(1),
},
M::Right => {
let max = self.lines[from.row].chars().count();
let limit = max.saturating_sub(1);
Cursor {
row: from.row,
col: (from.col + 1).min(limit),
}
}
M::Up => {
let row = from.row.saturating_sub(1);
let max = self.lines[row].chars().count();
Cursor {
row,
col: from.col.min(max.saturating_sub(1)),
}
}
M::Down => {
let row = (from.row + 1).min(self.lines.len().saturating_sub(1));
let max = self.lines[row].chars().count();
Cursor {
row,
col: from.col.min(max.saturating_sub(1)),
}
}
M::LineStart => Cursor {
row: from.row,
col: 0,
},
M::LineEnd => {
let max = self.lines[from.row].chars().count();
Cursor {
row: from.row,
col: max.saturating_sub(1),
}
}
M::LineFirstNonBlank => Cursor {
row: from.row,
col: first_non_blank(&self.lines[from.row]),
},
M::LineLastNonBlank => Cursor {
row: from.row,
col: last_non_blank(&self.lines[from.row]),
},
M::WordForward => self.peek_word_forward(from),
M::WordBack => self.peek_word_back(from),
M::WordEnd => word_end_char_class(&self.lines, from, false),
M::BigWordForward => big_word_forward(&self.lines, from),
M::BigWordBack => big_word_back(&self.lines, from),
M::BigWordEnd => word_end_char_class(&self.lines, from, true),
M::WordEndBack => word_end_back(&self.lines, from, false),
M::BigWordEndBack => word_end_back(&self.lines, from, true),
M::BracketMatch => bracket_match(&self.lines, from).unwrap_or(from),
M::SearchWordForward | M::SearchWordBack => from,
M::FindChar { ch, forward, till } => {
find_char(&self.lines[from.row], from, ch, forward, till)
}
M::RepeatFind { .. } => from,
M::FileStart => Cursor { row: 0, col: 0 },
M::FileEnd => {
let last = self.lines.len().saturating_sub(1);
let max = self.lines[last].chars().count();
Cursor {
row: last,
col: max.saturating_sub(1),
}
}
M::ParagraphForward => Cursor {
row: paragraph_forward_row(&self.lines, from.row),
col: 0,
},
M::ParagraphBack => Cursor {
row: paragraph_back_row(&self.lines, from.row),
col: 0,
},
M::ViewportTop | M::ViewportMiddle | M::ViewportBottom => {
viewport_target(self, from, motion)
}
M::HalfPageDown | M::HalfPageUp | M::PageDown | M::PageUp => {
page_target(self, from, motion)
}
M::SearchNext | M::SearchPrev => from,
}
}
pub fn move_word_forward(&mut self) {
self.cursor = self.peek_word_forward(self.cursor);
}
pub fn move_word_backward(&mut self) {
self.cursor = self.peek_word_back(self.cursor);
}
pub fn move_paragraph_forward(&mut self) {
self.cursor.row = paragraph_forward_row(&self.lines, self.cursor.row);
self.cursor.col = 0;
self.clamp_col(false);
}
pub fn move_paragraph_backward(&mut self) {
self.cursor.row = paragraph_back_row(&self.lines, self.cursor.row);
self.cursor.col = 0;
self.clamp_col(false);
}
fn peek_word_forward(&self, from: Cursor) -> Cursor {
word_forward_char_class(&self.lines, from)
}
fn peek_word_back(&self, from: Cursor) -> Cursor {
word_back_char_class(&self.lines, from)
}
}
fn first_non_blank(line: &str) -> usize {
line.chars()
.position(|c| !c.is_whitespace())
.unwrap_or(0)
}
fn last_non_blank(line: &str) -> usize {
let chars: Vec<char> = line.chars().collect();
for (i, c) in chars.iter().enumerate().rev() {
if !c.is_whitespace() {
return i;
}
}
0
}
fn paragraph_forward_row(lines: &[String], from: usize) -> usize {
let n = lines.len();
if n == 0 {
return 0;
}
let started_blank = is_blank_line(&lines[from]);
let mut row = from + 1;
if started_blank {
while row < n && is_blank_line(&lines[row]) {
row += 1;
}
}
while row < n && !is_blank_line(&lines[row]) {
row += 1;
}
row.min(n - 1)
}
fn paragraph_back_row(lines: &[String], from: usize) -> usize {
if from == 0 {
return 0;
}
let started_blank = is_blank_line(&lines[from]);
let mut row = from - 1;
if started_blank {
while row > 0 && is_blank_line(&lines[row]) {
row -= 1;
}
}
while row > 0 && !is_blank_line(&lines[row]) {
row -= 1;
}
row
}
#[cfg(test)]
mod tests {
use super::*;
fn lines(s: &str) -> Vec<String> {
s.split('\n').map(|s| s.to_string()).collect()
}
#[test]
fn first_and_last_non_blank() {
assert_eq!(first_non_blank(" hello"), 4);
assert_eq!(first_non_blank(""), 0);
assert_eq!(first_non_blank(" "), 0);
assert_eq!(last_non_blank("hello "), 4);
assert_eq!(last_non_blank("hello"), 4);
assert_eq!(last_non_blank(" "), 0);
}
#[test]
fn paragraph_motion_finds_blank_lines() {
let l = lines("foo\nbar\n\nbaz\nqux\n");
assert_eq!(paragraph_forward_row(&l, 0), 2); assert_eq!(paragraph_forward_row(&l, 1), 2); assert_eq!(paragraph_forward_row(&l, 2), 5); assert_eq!(paragraph_back_row(&l, 4), 2); assert_eq!(paragraph_back_row(&l, 3), 2); assert_eq!(paragraph_back_row(&l, 2), 0); }
}