From e0785aabe7618d5211f258a058bf08892ff04d06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Thu, 24 Sep 2020 19:16:35 +0900 Subject: [PATCH] Move-by-word commands: w, b, e. --- helix-core/src/state.rs | 137 +++++++++++++++++++++++++++++++++++++ helix-view/src/commands.rs | 35 ++++++++++ helix-view/src/keymap.rs | 12 ++++ 3 files changed, 184 insertions(+) diff --git a/helix-core/src/state.rs b/helix-core/src/state.rs index 4b610207b..cac52abc8 100644 --- a/helix-core/src/state.rs +++ b/helix-core/src/state.rs @@ -154,11 +154,91 @@ pub fn move_pos( (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(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(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::*; diff --git a/helix-view/src/commands.rs b/helix-view/src/commands.rs index 560167c9d..be3ea0b95 100644 --- a/helix-view/src/commands.rs +++ b/helix-view/src/commands.rs @@ -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: diff --git a/helix-view/src/keymap.rs b/helix-view/src/keymap.rs index 705357a8a..9fabf41dc 100644 --- a/helix-view/src/keymap.rs +++ b/helix-view/src/keymap.rs @@ -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