Extract logic for scanning over character boundaries

Nathan Sobo created

Change summary

crates/editor/src/movement.rs | 101 +++++++++++++++++++++---------------
1 file changed, 60 insertions(+), 41 deletions(-)

Detailed changes

crates/editor/src/movement.rs 🔗

@@ -132,64 +132,84 @@ pub fn line_end(
     }
 }
 
-pub fn previous_word_start(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
+pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
+    find_boundary_reversed(map, point, |left, right| {
+        char_kind(left) != char_kind(right) && !right.is_whitespace()
+    })
+}
+
+pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
+    find_boundary(map, point, |left, right| {
+        char_kind(left) != char_kind(right) && !left.is_whitespace()
+    })
+}
+
+fn find_boundary(
+    map: &DisplaySnapshot,
+    mut start: DisplayPoint,
+    is_boundary: impl Fn(char, char) -> bool,
+) -> DisplayPoint {
+    let mut prev_ch = None;
+    for ch in map.chars_at(start) {
+        if let Some(prev_ch) = prev_ch {
+            if ch == '\n' {
+                break;
+            }
+            if is_boundary(prev_ch, ch) {
+                break;
+            }
+        }
+
+        if ch == '\n' {
+            *start.row_mut() += 1;
+            *start.column_mut() = 0;
+        } else {
+            *start.column_mut() += ch.len_utf8() as u32;
+        }
+        prev_ch = Some(ch);
+    }
+    map.clip_point(start, Bias::Right)
+}
+
+fn find_boundary_reversed(
+    map: &DisplaySnapshot,
+    mut start: DisplayPoint,
+    is_boundary: impl Fn(char, char) -> bool,
+) -> DisplayPoint {
     let mut line_start = 0;
-    if point.row() > 0 {
-        if let Some(indent) = map.soft_wrap_indent(point.row() - 1) {
+    if start.row() > 0 {
+        if let Some(indent) = map.soft_wrap_indent(start.row() - 1) {
             line_start = indent;
         }
     }
 
-    if point.column() == line_start {
-        if point.row() == 0 {
+    if start.column() == line_start {
+        if start.row() == 0 {
             return DisplayPoint::new(0, 0);
         } else {
-            let row = point.row() - 1;
-            point = map.clip_point(DisplayPoint::new(row, map.line_len(row)), Bias::Left);
+            let row = start.row() - 1;
+            start = map.clip_point(DisplayPoint::new(row, map.line_len(row)), Bias::Left);
         }
     }
 
-    let mut boundary = DisplayPoint::new(point.row(), 0);
+    let mut boundary = DisplayPoint::new(start.row(), 0);
     let mut column = 0;
-    let mut prev_char_kind = CharKind::Whitespace;
-    for c in map.chars_at(DisplayPoint::new(point.row(), 0)) {
-        if column >= point.column() {
+    let mut prev_ch = None;
+    for ch in map.chars_at(DisplayPoint::new(start.row(), 0)) {
+        if column >= start.column() {
             break;
         }
 
-        let char_kind = char_kind(c);
-        if char_kind != prev_char_kind && char_kind != CharKind::Whitespace && c != '\n' {
-            *boundary.column_mut() = column;
-        }
-
-        prev_char_kind = char_kind;
-        column += c.len_utf8() as u32;
-    }
-    boundary
-}
-
-pub fn next_word_end(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
-    let mut prev_char_kind = None;
-    for c in map.chars_at(point) {
-        let char_kind = char_kind(c);
-        if let Some(prev_char_kind) = prev_char_kind {
-            if c == '\n' {
-                break;
-            }
-            if prev_char_kind != char_kind && prev_char_kind != CharKind::Whitespace {
-                break;
+        if let Some(prev_ch) = prev_ch {
+            if is_boundary(prev_ch, ch) {
+                *boundary.column_mut() = column;
             }
         }
 
-        if c == '\n' {
-            *point.row_mut() += 1;
-            *point.column_mut() = 0;
-        } else {
-            *point.column_mut() += c.len_utf8() as u32;
-        }
-        prev_char_kind = Some(char_kind);
+        prev_ch = Some(ch);
+        column += ch.len_utf8() as u32;
     }
-    map.clip_point(point, Bias::Right)
+    boundary
 }
 
 pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
@@ -226,7 +246,6 @@ mod tests {
     fn test_previous_word_start(cx: &mut gpui::MutableAppContext) {
         fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
             let (snapshot, display_points) = marked_snapshot(marked_text, cx);
-            dbg!(&display_points);
             assert_eq!(
                 previous_word_start(&snapshot, display_points[1]),
                 display_points[0]