Add a new `movement::surrounding_word` function

Antonio Scandurra created

Change summary

crates/editor/src/display_map.rs |   2 
crates/editor/src/movement.rs    | 148 ++++++++++++++++++++++++++++-----
2 files changed, 125 insertions(+), 25 deletions(-)

Detailed changes

crates/editor/src/display_map.rs 🔗

@@ -169,7 +169,7 @@ impl DisplayMap {
 }
 
 pub struct DisplayMapSnapshot {
-    buffer_snapshot: language::Snapshot,
+    pub buffer_snapshot: language::Snapshot,
     folds_snapshot: fold_map::Snapshot,
     tabs_snapshot: tab_map::Snapshot,
     wraps_snapshot: wrap_map::Snapshot,

crates/editor/src/movement.rs 🔗

@@ -1,5 +1,7 @@
-use super::{Bias, DisplayMapSnapshot, DisplayPoint, SelectionGoal};
+use super::{Bias, DisplayMapSnapshot, DisplayPoint, SelectionGoal, ToDisplayPoint};
 use anyhow::Result;
+use buffer::ToPoint;
+use std::{cmp, ops::Range};
 
 pub fn left(map: &DisplayMapSnapshot, mut point: DisplayPoint) -> Result<DisplayPoint> {
     if point.column() > 0 {
@@ -113,10 +115,7 @@ pub fn line_end(map: &DisplayMapSnapshot, point: DisplayPoint) -> Result<Display
     Ok(map.clip_point(line_end, Bias::Left))
 }
 
-pub fn prev_word_boundary(
-    map: &DisplayMapSnapshot,
-    mut point: DisplayPoint,
-) -> Result<DisplayPoint> {
+pub fn prev_word_boundary(map: &DisplayMapSnapshot, mut point: DisplayPoint) -> DisplayPoint {
     let mut line_start = 0;
     if point.row() > 0 {
         if let Some(indent) = map.soft_wrap_indent(point.row() - 1) {
@@ -126,7 +125,7 @@ pub fn prev_word_boundary(
 
     if point.column() == line_start {
         if point.row() == 0 {
-            return Ok(DisplayPoint::new(0, 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);
@@ -152,13 +151,10 @@ pub fn prev_word_boundary(
         prev_char_kind = char_kind;
         column += c.len_utf8() as u32;
     }
-    Ok(boundary)
+    boundary
 }
 
-pub fn next_word_boundary(
-    map: &DisplayMapSnapshot,
-    mut point: DisplayPoint,
-) -> Result<DisplayPoint> {
+pub fn next_word_boundary(map: &DisplayMapSnapshot, mut point: DisplayPoint) -> DisplayPoint {
     let mut prev_char_kind = None;
     for c in map.chars_at(point) {
         let char_kind = char_kind(c);
@@ -182,14 +178,46 @@ pub fn next_word_boundary(
         }
         prev_char_kind = Some(char_kind);
     }
-    Ok(point)
+    point
+}
+
+pub fn surrounding_word(map: &DisplayMapSnapshot, point: DisplayPoint) -> Range<DisplayPoint> {
+    let mut start = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left);
+    let mut end = start;
+
+    let text = map.buffer_snapshot.text();
+    let mut next_chars = text.chars_at(start).peekable();
+    let mut prev_chars = text.reversed_chars_at(start).peekable();
+    let word_kind = cmp::max(
+        prev_chars.peek().copied().map(char_kind),
+        next_chars.peek().copied().map(char_kind),
+    );
+
+    for ch in prev_chars {
+        if Some(char_kind(ch)) == word_kind {
+            start -= ch.len_utf8();
+        } else {
+            break;
+        }
+    }
+
+    for ch in next_chars {
+        if Some(char_kind(ch)) == word_kind {
+            end += ch.len_utf8();
+        } else {
+            break;
+        }
+    }
+
+    start.to_point(&map.buffer_snapshot).to_display_point(map)
+        ..end.to_point(&map.buffer_snapshot).to_display_point(map)
 }
 
-#[derive(Copy, Clone, Eq, PartialEq)]
+#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord)]
 enum CharKind {
     Newline,
-    Whitespace,
     Punctuation,
+    Whitespace,
     Word,
 }
 
@@ -225,45 +253,117 @@ mod tests {
             cx.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, cx));
         let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
         assert_eq!(
-            prev_word_boundary(&snapshot, DisplayPoint::new(0, 12)).unwrap(),
+            prev_word_boundary(&snapshot, DisplayPoint::new(0, 12)),
             DisplayPoint::new(0, 7)
         );
         assert_eq!(
-            prev_word_boundary(&snapshot, DisplayPoint::new(0, 7)).unwrap(),
+            prev_word_boundary(&snapshot, DisplayPoint::new(0, 7)),
             DisplayPoint::new(0, 2)
         );
         assert_eq!(
-            prev_word_boundary(&snapshot, DisplayPoint::new(0, 6)).unwrap(),
+            prev_word_boundary(&snapshot, DisplayPoint::new(0, 6)),
             DisplayPoint::new(0, 2)
         );
         assert_eq!(
-            prev_word_boundary(&snapshot, DisplayPoint::new(0, 2)).unwrap(),
+            prev_word_boundary(&snapshot, DisplayPoint::new(0, 2)),
             DisplayPoint::new(0, 0)
         );
         assert_eq!(
-            prev_word_boundary(&snapshot, DisplayPoint::new(0, 1)).unwrap(),
+            prev_word_boundary(&snapshot, DisplayPoint::new(0, 1)),
             DisplayPoint::new(0, 0)
         );
 
         assert_eq!(
-            next_word_boundary(&snapshot, DisplayPoint::new(0, 0)).unwrap(),
+            next_word_boundary(&snapshot, DisplayPoint::new(0, 0)),
             DisplayPoint::new(0, 1)
         );
         assert_eq!(
-            next_word_boundary(&snapshot, DisplayPoint::new(0, 1)).unwrap(),
+            next_word_boundary(&snapshot, DisplayPoint::new(0, 1)),
             DisplayPoint::new(0, 6)
         );
         assert_eq!(
-            next_word_boundary(&snapshot, DisplayPoint::new(0, 2)).unwrap(),
+            next_word_boundary(&snapshot, DisplayPoint::new(0, 2)),
             DisplayPoint::new(0, 6)
         );
         assert_eq!(
-            next_word_boundary(&snapshot, DisplayPoint::new(0, 6)).unwrap(),
+            next_word_boundary(&snapshot, DisplayPoint::new(0, 6)),
             DisplayPoint::new(0, 12)
         );
         assert_eq!(
-            next_word_boundary(&snapshot, DisplayPoint::new(0, 7)).unwrap(),
+            next_word_boundary(&snapshot, DisplayPoint::new(0, 7)),
             DisplayPoint::new(0, 12)
         );
     }
+
+    #[gpui::test]
+    fn test_surrounding_word(cx: &mut gpui::MutableAppContext) {
+        let tab_size = 4;
+        let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
+        let font_id = cx
+            .font_cache()
+            .select_font(family_id, &Default::default())
+            .unwrap();
+        let font_size = 14.0;
+        let buffer = cx.add_model(|cx| Buffer::new(0, "lorem ipsum   dolor\n    sit", cx));
+        let display_map =
+            cx.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, cx));
+        let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
+
+        assert_eq!(
+            surrounding_word(&snapshot, DisplayPoint::new(0, 0)),
+            DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5)
+        );
+        assert_eq!(
+            surrounding_word(&snapshot, DisplayPoint::new(0, 2)),
+            DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5)
+        );
+        assert_eq!(
+            surrounding_word(&snapshot, DisplayPoint::new(0, 5)),
+            DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5)
+        );
+        assert_eq!(
+            surrounding_word(&snapshot, DisplayPoint::new(0, 6)),
+            DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11)
+        );
+        assert_eq!(
+            surrounding_word(&snapshot, DisplayPoint::new(0, 7)),
+            DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11)
+        );
+        assert_eq!(
+            surrounding_word(&snapshot, DisplayPoint::new(0, 11)),
+            DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11)
+        );
+        assert_eq!(
+            surrounding_word(&snapshot, DisplayPoint::new(0, 13)),
+            DisplayPoint::new(0, 11)..DisplayPoint::new(0, 14)
+        );
+        assert_eq!(
+            surrounding_word(&snapshot, DisplayPoint::new(0, 14)),
+            DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19)
+        );
+        assert_eq!(
+            surrounding_word(&snapshot, DisplayPoint::new(0, 17)),
+            DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19)
+        );
+        assert_eq!(
+            surrounding_word(&snapshot, DisplayPoint::new(0, 19)),
+            DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19)
+        );
+        assert_eq!(
+            surrounding_word(&snapshot, DisplayPoint::new(1, 0)),
+            DisplayPoint::new(1, 0)..DisplayPoint::new(1, 4)
+        );
+        assert_eq!(
+            surrounding_word(&snapshot, DisplayPoint::new(1, 1)),
+            DisplayPoint::new(1, 0)..DisplayPoint::new(1, 4)
+        );
+        assert_eq!(
+            surrounding_word(&snapshot, DisplayPoint::new(1, 6)),
+            DisplayPoint::new(1, 4)..DisplayPoint::new(1, 7)
+        );
+        assert_eq!(
+            surrounding_word(&snapshot, DisplayPoint::new(1, 7)),
+            DisplayPoint::new(1, 4)..DisplayPoint::new(1, 7)
+        );
+    }
 }