Implement `move_to_previous_word_boundary`

Antonio Scandurra and Max Brunsfeld created

Co-Authored-By: Max Brunsfeld <max@zed.dev>

Change summary

zed/src/editor/buffer_view.rs | 27 ++++++++++++++++++++
zed/src/editor/movement.rs    | 48 ++++++++++++++++++++++++++++++------
2 files changed, 67 insertions(+), 8 deletions(-)

Detailed changes

zed/src/editor/buffer_view.rs 🔗

@@ -54,6 +54,11 @@ pub fn init(app: &mut MutableAppContext) {
         Binding::new("down", "buffer:move_down", Some("BufferView")),
         Binding::new("left", "buffer:move_left", Some("BufferView")),
         Binding::new("right", "buffer:move_right", Some("BufferView")),
+        Binding::new(
+            "alt-left",
+            "buffer:move_to_previous_word_boundary",
+            Some("BufferView"),
+        ),
         Binding::new(
             "alt-right",
             "buffer:move_to_next_word_boundary",
@@ -146,6 +151,10 @@ pub fn init(app: &mut MutableAppContext) {
     app.add_action("buffer:move_down", BufferView::move_down);
     app.add_action("buffer:move_left", BufferView::move_left);
     app.add_action("buffer:move_right", BufferView::move_right);
+    app.add_action(
+        "buffer:move_to_previous_word_boundary",
+        BufferView::move_to_previous_word_boundary,
+    );
     app.add_action(
         "buffer:move_to_next_word_boundary",
         BufferView::move_to_next_word_boundary,
@@ -1094,6 +1103,24 @@ impl BufferView {
         self.update_selections(selections, true, ctx);
     }
 
+    pub fn move_to_previous_word_boundary(&mut self, _: &(), ctx: &mut ViewContext<Self>) {
+        let app = ctx.as_ref();
+        let mut selections = self.selections(app).to_vec();
+        {
+            let map = self.display_map.read(app);
+            for selection in &mut selections {
+                let head = selection.head().to_display_point(map, app).unwrap();
+                let new_head = movement::prev_word_boundary(map, head, app).unwrap();
+                let anchor = map.anchor_before(new_head, Bias::Left, app).unwrap();
+                selection.start = anchor.clone();
+                selection.end = anchor;
+                selection.reversed = false;
+                selection.goal_column = None;
+            }
+        }
+        self.update_selections(selections, true, ctx);
+    }
+
     pub fn move_to_next_word_boundary(&mut self, _: &(), ctx: &mut ViewContext<Self>) {
         let app = ctx.as_ref();
         let mut selections = self.selections(app).to_vec();

zed/src/editor/movement.rs 🔗

@@ -85,7 +85,31 @@ pub fn prev_word_boundary(
     point: DisplayPoint,
     app: &AppContext,
 ) -> Result<DisplayPoint> {
-    todo!()
+    if point.column() == 0 {
+        if point.row() == 0 {
+            Ok(DisplayPoint::new(0, 0))
+        } else {
+            let row = point.row() - 1;
+            Ok(DisplayPoint::new(row, map.line_len(row, app)?))
+        }
+    } else {
+        let mut boundary = DisplayPoint::new(point.row(), 0);
+        let mut column = 0;
+        let mut prev_c = None;
+        for c in map.chars_at(boundary, app)? {
+            if column >= point.column() {
+                break;
+            }
+
+            if prev_c.is_none() || char_kind(prev_c.unwrap()) != char_kind(c) {
+                *boundary.column_mut() = column;
+            }
+
+            prev_c = Some(c);
+            column += 1;
+        }
+        Ok(boundary)
+    }
 }
 
 pub fn next_word_boundary(
@@ -95,7 +119,7 @@ pub fn next_word_boundary(
 ) -> Result<DisplayPoint> {
     let mut prev_c = None;
     for c in map.chars_at(point, app)? {
-        if prev_c.is_some() && (c == '\n' || is_word_char(prev_c.unwrap()) != is_word_char(c)) {
+        if prev_c.is_some() && (c == '\n' || char_kind(prev_c.unwrap()) != char_kind(c)) {
             break;
         }
 
@@ -110,11 +134,19 @@ pub fn next_word_boundary(
     Ok(point)
 }
 
-fn is_word_char(c: char) -> bool {
-    match c {
-        '/' | '\\' | '(' | ')' | '"' | '\'' | ':' | ',' | '.' | ';' | '<' | '>' | '~' | '!'
-        | '@' | '#' | '$' | '%' | '^' | '&' | '*' | '|' | '+' | '=' | '[' | ']' | '{' | '}'
-        | '`' | '?' | '-' | '…' | ' ' | '\n' => false,
-        _ => true,
+#[derive(Copy, Clone, Eq, PartialEq)]
+enum CharKind {
+    Whitespace,
+    Punctuation,
+    Word,
+}
+
+fn char_kind(c: char) -> CharKind {
+    if c.is_whitespace() {
+        CharKind::Whitespace
+    } else if c.is_alphanumeric() || c == '_' {
+        CharKind::Word
+    } else {
+        CharKind::Punctuation
     }
 }