@@ -154,11 +154,91 @@ impl State {
154154 (Direction::Forward, Granularity::Character) => {
155155 nth_next_grapheme_boundary(&text.slice(..), pos, count)
156156 }
157+ (Direction::Forward, Granularity::Word) => {
158+ Self::move_next_word_start(&text.slice(..), pos)
159+ }
160+ (Direction::Backward, Granularity::Word) => {
161+ Self::move_prev_word_start(&text.slice(..), pos)
162+ }
157163 (_, Granularity::Line) => move_vertically(&text.slice(..), dir, pos, count),
158164 _ => pos,
159165 }
160166 }
161167
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) {
173+ pos += 1;
174+ }
175+
176+ // refetch
177+ let ch = slice.char(pos);
178+
179+ if is_word(ch) {
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());
183+ }
184+
185+ // TODO: don't include newline?
186+ skip_over_next(slice, &mut pos, |ch| ch.is_ascii_whitespace());
187+
188+ pos
189+ }
190+
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
195+
196+ if categorize(ch) != categorize(prev) {
197+ pos -= 1;
198+ }
199+
200+ // TODO: skip while eol
201+
202+ // TODO: don't include newline?
203+ skip_over_prev(slice, &mut pos, |ch| ch.is_ascii_whitespace());
204+
205+ // refetch
206+ let ch = slice.char(pos);
207+
208+ if is_word(ch) {
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());
212+ }
213+
214+ pos.saturating_add(1)
215+ }
216+
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) {
222+ pos += 1;
223+ }
224+
225+ // TODO: don't include newline?
226+ skip_over_next(slice, &mut pos, |ch| ch.is_ascii_whitespace());
227+
228+ // refetch
229+ let ch = slice.char(pos);
230+
231+ if is_word(ch) {
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());
235+ }
236+
237+ // TODO: stops on spaces
238+
239+ pos.saturating_sub(1)
240+ }
241+
162242 pub fn move_selection(
163243 &self,
164244 dir: Direction,
@@ -235,6 +315,63 @@ fn move_vertically(text: &RopeSlice, dir: Direction, pos: usize, count: usize) -
235315 pos_at_coords(text, Position::new(new_line, new_col))
236316}
237317
318+// used for by-word movement
319+
320+fn is_word(ch: char) -> bool {
321+ ch.is_alphanumeric() || ch == '_'
322+}
323+
324+#[derive(Debug, Eq, PartialEq)]
325+enum Category {
326+ Whitespace,
327+ EOL,
328+ Word,
329+ Punctuation,
330+}
331+fn categorize(ch: char) -> Category {
332+ if ch == '\n' {
333+ Category::EOL
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() {
339+ Category::Word
340+ } else {
341+ unreachable!()
342+ }
343+}
344+
345+fn skip_over_next<F>(slice: &RopeSlice, pos: &mut usize, fun: F)
346+where
347+ F: Fn(char) -> bool,
348+{
349+ let mut chars = slice.chars_at(*pos);
350+
351+ while let Some(ch) = chars.next() {
352+ if !fun(ch) {
353+ break;
354+ }
355+ *pos += 1;
356+ }
357+}
358+
359+fn skip_over_prev<F>(slice: &RopeSlice, pos: &mut usize, fun: F)
360+where
361+ F: Fn(char) -> bool,
362+{
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);
366+
367+ while let Some(ch) = chars.prev() {
368+ if !fun(ch) {
369+ break;
370+ }
371+ *pos -= 1;
372+ }
373+}
374+
238375#[cfg(test)]
239376mod test {
240377 use super::*;