Move-by-word commands: w, b, e.

This commit is contained in:
Blaž Hrastnik 2020-09-24 19:16:35 +09:00
parent eb639eb2e4
commit e0785aabe7
3 changed files with 184 additions and 0 deletions

View file

@ -154,11 +154,91 @@ impl State {
(Direction::Forward, Granularity::Character) => {
nth_next_grapheme_boundary(&text.slice(..), pos, count)
}
(Direction::Forward, Granularity::Word) => {
Self::move_next_word_start(&text.slice(..), pos)
}
(Direction::Backward, Granularity::Word) => {
Self::move_prev_word_start(&text.slice(..), pos)
}
(_, Granularity::Line) => move_vertically(&text.slice(..), dir, pos, count),
_ => pos,
}
}
pub fn move_next_word_start(slice: &RopeSlice, mut pos: usize) -> usize {
// TODO: confirm it's fine without using graphemes, I think it should be
let ch = slice.char(pos);
let next = slice.char(pos.saturating_add(1));
if categorize(ch) != categorize(next) {
pos += 1;
}
// refetch
let ch = slice.char(pos);
if is_word(ch) {
skip_over_next(slice, &mut pos, is_word);
} else if ch.is_ascii_punctuation() {
skip_over_next(slice, &mut pos, |ch| ch.is_ascii_punctuation());
}
// TODO: don't include newline?
skip_over_next(slice, &mut pos, |ch| ch.is_ascii_whitespace());
pos
}
pub fn move_prev_word_start(slice: &RopeSlice, mut pos: usize) -> usize {
// TODO: confirm it's fine without using graphemes, I think it should be
let ch = slice.char(pos);
let prev = slice.char(pos.saturating_sub(1)); // TODO: just return original pos if at start
if categorize(ch) != categorize(prev) {
pos -= 1;
}
// TODO: skip while eol
// TODO: don't include newline?
skip_over_prev(slice, &mut pos, |ch| ch.is_ascii_whitespace());
// refetch
let ch = slice.char(pos);
if is_word(ch) {
skip_over_prev(slice, &mut pos, is_word);
} else if ch.is_ascii_punctuation() {
skip_over_prev(slice, &mut pos, |ch| ch.is_ascii_punctuation());
}
pos.saturating_add(1)
}
pub fn move_next_word_end(slice: &RopeSlice, mut pos: usize, _count: usize) -> usize {
// TODO: confirm it's fine without using graphemes, I think it should be
let ch = slice.char(pos);
let next = slice.char(pos.saturating_add(1));
if categorize(ch) != categorize(next) {
pos += 1;
}
// TODO: don't include newline?
skip_over_next(slice, &mut pos, |ch| ch.is_ascii_whitespace());
// refetch
let ch = slice.char(pos);
if is_word(ch) {
skip_over_next(slice, &mut pos, is_word);
} else if ch.is_ascii_punctuation() {
skip_over_next(slice, &mut pos, |ch| ch.is_ascii_punctuation());
}
// TODO: stops on spaces
pos.saturating_sub(1)
}
pub fn move_selection(
&self,
dir: Direction,
@ -235,6 +315,63 @@ fn move_vertically(text: &RopeSlice, dir: Direction, pos: usize, count: usize) -
pos_at_coords(text, Position::new(new_line, new_col))
}
// used for by-word movement
fn is_word(ch: char) -> bool {
ch.is_alphanumeric() || ch == '_'
}
#[derive(Debug, Eq, PartialEq)]
enum Category {
Whitespace,
EOL,
Word,
Punctuation,
}
fn categorize(ch: char) -> Category {
if ch == '\n' {
Category::EOL
} else if ch.is_ascii_whitespace() {
Category::Whitespace
} else if ch.is_ascii_punctuation() {
Category::Punctuation
} else if ch.is_ascii_alphanumeric() {
Category::Word
} else {
unreachable!()
}
}
fn skip_over_next<F>(slice: &RopeSlice, pos: &mut usize, fun: F)
where
F: Fn(char) -> bool,
{
let mut chars = slice.chars_at(*pos);
while let Some(ch) = chars.next() {
if !fun(ch) {
break;
}
*pos += 1;
}
}
fn skip_over_prev<F>(slice: &RopeSlice, pos: &mut usize, fun: F)
where
F: Fn(char) -> bool,
{
// need to +1 so that prev() includes current char
let mut chars = slice.chars_at(*pos + 1);
let mut chars = slice.chars_at(*pos + 1);
while let Some(ch) = chars.prev() {
if !fun(ch) {
break;
}
*pos -= 1;
}
}
#[cfg(test)]
mod test {
use super::*;

View file

@ -39,6 +39,41 @@ pub fn move_line_down(view: &mut View, count: usize) {
.move_selection(Direction::Forward, Granularity::Line, count);
}
pub fn move_next_word_start(view: &mut View, count: usize) {
let pos = view.state.move_pos(
view.state.selection.cursor(),
Direction::Forward,
Granularity::Word,
count,
);
// TODO: use a transaction
view.state.selection = Selection::single(pos, pos);
}
pub fn move_prev_word_start(view: &mut View, count: usize) {
let pos = view.state.move_pos(
view.state.selection.cursor(),
Direction::Backward,
Granularity::Word,
count,
);
// TODO: use a transaction
view.state.selection = Selection::single(pos, pos);
}
pub fn move_next_word_end(view: &mut View, count: usize) {
let pos = State::move_next_word_end(
&view.state.doc().slice(..),
view.state.selection.cursor(),
count,
);
// TODO: use a transaction
view.state.selection = Selection::single(pos, pos);
}
// avoid select by default by having a visual mode switch that makes movements into selects
// insert mode:

View file

@ -102,6 +102,18 @@ pub fn default() -> Keymaps {
code: KeyCode::Char('l'),
modifiers: Modifiers::NONE
}] => commands::move_char_right as Command,
vec![Key {
code: KeyCode::Char('w'),
modifiers: Modifiers::NONE
}] => commands::move_next_word_start as Command,
vec![Key {
code: KeyCode::Char('b'),
modifiers: Modifiers::NONE
}] => commands::move_prev_word_start as Command,
vec![Key {
code: KeyCode::Char('e'),
modifiers: Modifiers::NONE
}] => commands::move_next_word_end as Command,
vec![Key {
code: KeyCode::Char('i'),
modifiers: Modifiers::NONE