aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBlaž Hrastnik2020-09-24 10:16:35 +0000
committerBlaž Hrastnik2020-09-24 10:16:35 +0000
commite0785aabe7618d5211f258a058bf08892ff04d06 (patch)
treed3ad697e560790c7ecdf5eb983a36f2502ab2636
parenteb639eb2e4610ed2b440c8d95217f125005288fd (diff)
Move-by-word commands: w, b, e.
-rw-r--r--helix-core/src/state.rs137
-rw-r--r--helix-view/src/commands.rs35
-rw-r--r--helix-view/src/keymap.rs12
3 files changed, 184 insertions, 0 deletions
diff --git a/helix-core/src/state.rs b/helix-core/src/state.rs
index 4b610207..cac52abc 100644
--- a/helix-core/src/state.rs
+++ b/helix-core/src/state.rs
@@ -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::*;
diff --git a/helix-view/src/commands.rs b/helix-view/src/commands.rs
index 560167c9..be3ea0b9 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 705357a8..9fabf41d 100644
--- a/helix-view/src/keymap.rs
+++ b/helix-view/src/keymap.rs
@@ -103,6 +103,18 @@ pub fn default() -> Keymaps {
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
}] => commands::insert_mode as Command,