@@ -154,11 +154,91 @@ impl State {
154 (Direction::Forward, Granularity::Character) => {
154 (Direction::Forward, Granularity::Character) => {
155 nth_next_grapheme_boundary(&text.slice(..), pos, count)
155 nth_next_grapheme_boundary(&text.slice(..), pos, count)
157+ (Direction::Forward, Granularity::Word) => {
158+ Self::move_next_word_start(&text.slice(..), pos)
160+ (Direction::Backward, Granularity::Word) => {
161+ Self::move_prev_word_start(&text.slice(..), pos)
157 (_, Granularity::Line) => move_vertically(&text.slice(..), dir, pos, count),
163 (_, Granularity::Line) => move_vertically(&text.slice(..), dir, pos, count),
158 _ => pos,
164 _ => pos,
168+ pub fn move_next_word_start(slice: &RopeSlice, mut pos: usize) -> usize {
169+ // TODO: confirm it's fine without using graphemes, I think it should be
170+ let ch = slice.char(pos);
171+ let next = slice.char(pos.saturating_add(1));
172+ if categorize(ch) != categorize(next) {
177+ let ch = slice.char(pos);
180+ skip_over_next(slice, &mut pos, is_word);
181+ } else if ch.is_ascii_punctuation() {
182+ skip_over_next(slice, &mut pos, |ch| ch.is_ascii_punctuation());
185+ // TODO: don't include newline?
186+ skip_over_next(slice, &mut pos, |ch| ch.is_ascii_whitespace());
191+ pub fn move_prev_word_start(slice: &RopeSlice, mut pos: usize) -> usize {
192+ // TODO: confirm it's fine without using graphemes, I think it should be
193+ let ch = slice.char(pos);
194+ let prev = slice.char(pos.saturating_sub(1)); // TODO: just return original pos if at start
196+ if categorize(ch) != categorize(prev) {
200+ // TODO: skip while eol
202+ // TODO: don't include newline?
203+ skip_over_prev(slice, &mut pos, |ch| ch.is_ascii_whitespace());
206+ let ch = slice.char(pos);
209+ skip_over_prev(slice, &mut pos, is_word);
210+ } else if ch.is_ascii_punctuation() {
211+ skip_over_prev(slice, &mut pos, |ch| ch.is_ascii_punctuation());
214+ pos.saturating_add(1)
217+ pub fn move_next_word_end(slice: &RopeSlice, mut pos: usize, _count: usize) -> usize {
218+ // TODO: confirm it's fine without using graphemes, I think it should be
219+ let ch = slice.char(pos);
220+ let next = slice.char(pos.saturating_add(1));
221+ if categorize(ch) != categorize(next) {
225+ // TODO: don't include newline?
226+ skip_over_next(slice, &mut pos, |ch| ch.is_ascii_whitespace());
229+ let ch = slice.char(pos);
232+ skip_over_next(slice, &mut pos, is_word);
233+ } else if ch.is_ascii_punctuation() {
234+ skip_over_next(slice, &mut pos, |ch| ch.is_ascii_punctuation());
237+ // TODO: stops on spaces
239+ pos.saturating_sub(1)
162 pub fn move_selection(
242 pub fn move_selection(
164 dir: Direction,
244 dir: Direction,
@@ -235,6 +315,63 @@ fn move_vertically(text: &RopeSlice, dir: Direction, pos: usize, count: usize) -
235 pos_at_coords(text, Position::new(new_line, new_col))
315 pos_at_coords(text, Position::new(new_line, new_col))
318+// used for by-word movement
320+fn is_word(ch: char) -> bool {
321+ ch.is_alphanumeric() || ch == '_'
324+#[derive(Debug, Eq, PartialEq)]
331+fn categorize(ch: char) -> Category {
334+ } else if ch.is_ascii_whitespace() {
335+ Category::Whitespace
336+ } else if ch.is_ascii_punctuation() {
337+ Category::Punctuation
338+ } else if ch.is_ascii_alphanumeric() {
345+fn skip_over_next<F>(slice: &RopeSlice, pos: &mut usize, fun: F)
347+ F: Fn(char) -> bool,
349+ let mut chars = slice.chars_at(*pos);
351+ while let Some(ch) = chars.next() {
359+fn skip_over_prev<F>(slice: &RopeSlice, pos: &mut usize, fun: F)
361+ F: Fn(char) -> bool,
363+ // need to +1 so that prev() includes current char
364+ let mut chars = slice.chars_at(*pos + 1);
365+ let mut chars = slice.chars_at(*pos + 1);
367+ while let Some(ch) = chars.prev() {
238#[cfg(test)]
375#[cfg(test)]
239mod test {
376mod test {
240 use super::*;
377 use super::*;