aboutsummaryrefslogtreecommitdiff
path: root/helix-core/src/movement.rs
diff options
context:
space:
mode:
Diffstat (limited to 'helix-core/src/movement.rs')
-rw-r--r--helix-core/src/movement.rs157
1 files changed, 157 insertions, 0 deletions
diff --git a/helix-core/src/movement.rs b/helix-core/src/movement.rs
new file mode 100644
index 00000000..57abc498
--- /dev/null
+++ b/helix-core/src/movement.rs
@@ -0,0 +1,157 @@
+use crate::graphemes::{nth_next_grapheme_boundary, nth_prev_grapheme_boundary, RopeGraphemes};
+use crate::{coords_at_pos, pos_at_coords, ChangeSet, Position, Range, Rope, RopeSlice, Selection};
+
+pub fn move_next_word_start(slice: RopeSlice, mut pos: usize, count: usize) -> usize {
+ for _ in 0..count {
+ if pos + 1 == slice.len_chars() {
+ return pos;
+ }
+
+ let mut ch = slice.char(pos);
+ let next = slice.char(pos + 1);
+
+ // if we're at the end of a word, or on whitespce right before new one
+ if categorize(ch) != categorize(next) {
+ pos += 1;
+ ch = next;
+ }
+
+ 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, count: usize) -> usize {
+ for _ in 0..count {
+ if pos == 0 {
+ return pos;
+ }
+
+ let ch = slice.char(pos);
+ let prev = slice.char(pos - 1);
+
+ if categorize(ch) != categorize(prev) {
+ pos -= 1;
+ }
+
+ // match (category c1, category c2) => {
+ // if c1 != c2 {
+ // }
+ // }
+
+ // 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 = pos.saturating_add(1)
+ }
+
+ pos
+}
+
+pub fn move_next_word_end(slice: RopeSlice, mut pos: usize, count: usize) -> usize {
+ for _ in 0..count {
+ if pos + 1 == slice.len_chars() {
+ return pos;
+ }
+
+ let ch = slice.char(pos);
+ let next = slice.char(pos + 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());
+ }
+ pos -= 1
+ }
+
+ pos
+}
+
+// ---- util ------------
+
+// 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!()
+ }
+}
+
+#[inline]
+pub fn skip_over_next<F>(slice: RopeSlice, pos: &mut usize, fun: F)
+where
+ F: Fn(char) -> bool,
+{
+ let mut chars = slice.chars_at(*pos);
+
+ for ch in chars {
+ if !fun(ch) {
+ break;
+ }
+ *pos += 1;
+ }
+}
+
+#[inline]
+pub 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);
+
+ while let Some(ch) = chars.prev() {
+ if !fun(ch) {
+ break;
+ }
+ *pos = pos.saturating_sub(1);
+ }
+}