Start work on making row/columnwise movement work w/ byte columns

Max Brunsfeld created

Change summary

zed/src/editor/buffer/rope.rs     |  5 +
zed/src/editor/buffer_view.rs     | 70 ++++++++++++++++++++++++++++++++
zed/src/editor/display_map/mod.rs | 34 ++++++++++++++-
zed/src/editor/movement.rs        | 19 +++++++-
4 files changed, 119 insertions(+), 9 deletions(-)

Detailed changes

zed/src/editor/buffer/rope.rs πŸ”—

@@ -335,6 +335,9 @@ impl Chunk {
         let mut point = Point::new(0, 0);
         for ch in self.0.chars() {
             if point >= target {
+                if point > target {
+                    panic!("point {:?} is inside of character {:?}", target, ch);
+                }
                 break;
             }
 
@@ -346,8 +349,6 @@ impl Chunk {
             }
             offset += ch.len_utf8();
         }
-
-        assert_eq!(point, target);
         offset
     }
 }

zed/src/editor/buffer_view.rs πŸ”—

@@ -2358,7 +2358,11 @@ impl workspace::ItemView for BufferView {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::{editor::Point, settings, test::sample_text};
+    use crate::{
+        editor::Point,
+        settings,
+        test::{multibyte_sample_text, sample_text},
+    };
     use unindent::Unindent;
 
     #[gpui::test]
@@ -2715,6 +2719,70 @@ mod tests {
         });
     }
 
+    #[gpui::test]
+    fn test_move_cursor_multibyte(app: &mut gpui::MutableAppContext) {
+        let buffer = app.add_model(|ctx| Buffer::new(0, "ⓐⓑⓒⓓⓔ\nabcde\nαβγδΡ\n", ctx));
+        let settings = settings::channel(&app.font_cache()).unwrap().1;
+        let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx));
+
+        assert_eq!('ⓐ'.len_utf8(), 3);
+        assert_eq!('Ξ±'.len_utf8(), 2);
+
+        view.update(app, |view, ctx| {
+            view.fold_ranges(
+                vec![
+                    Point::new(0, 6)..Point::new(0, 12),
+                    Point::new(1, 2)..Point::new(1, 4),
+                    Point::new(2, 4)..Point::new(2, 8),
+                ],
+                ctx,
+            );
+            assert_eq!(view.text(ctx.as_ref()), "ⓐⓑ…ⓔ\nab…e\nαβ…Ρ\n");
+
+            view.move_right(&(), ctx);
+            assert_eq!(
+                view.selection_ranges(ctx.as_ref()),
+                &[DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3)]
+            );
+
+            view.move_down(&(), ctx);
+            assert_eq!(
+                view.selection_ranges(ctx.as_ref()),
+                &[DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1)]
+            );
+
+            view.move_right(&(), ctx);
+            assert_eq!(
+                view.selection_ranges(ctx.as_ref()),
+                &[DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2)]
+            );
+
+            view.move_down(&(), ctx);
+            assert_eq!(
+                view.selection_ranges(ctx.as_ref()),
+                &[DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4)]
+            );
+
+            view.move_left(&(), ctx);
+            assert_eq!(
+                view.selection_ranges(ctx.as_ref()),
+                &[DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)]
+            );
+
+            view.move_up(&(), ctx);
+            assert_eq!(
+                view.selection_ranges(ctx.as_ref()),
+                &[DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1)]
+            );
+
+            view.move_up(&(), ctx);
+            assert_eq!(
+                view.selection_ranges(ctx.as_ref()),
+                &[DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3)]
+            );
+        });
+    }
+
     #[gpui::test]
     fn test_beginning_end_of_line(app: &mut gpui::MutableAppContext) {
         let buffer = app.add_model(|ctx| Buffer::new(0, "abc\n  def", ctx));

zed/src/editor/display_map/mod.rs πŸ”—

@@ -92,8 +92,7 @@ impl DisplayMap {
         let mut is_blank = true;
         for c in self
             .snapshot(ctx)
-            .chunks_at(DisplayPoint::new(display_row, 0), ctx)
-            .flat_map(str::chars)
+            .chars_at(DisplayPoint::new(display_row, 0), ctx)
         {
             if c == ' ' {
                 indent += 1;
@@ -165,6 +164,32 @@ impl DisplayMapSnapshot {
         self.chunks_at(point, app).flat_map(str::chars)
     }
 
+    pub fn column_to_chars(&self, display_row: u32, target: u32, ctx: &AppContext) -> u32 {
+        let mut count = 0;
+        let mut column = 0;
+        for c in self.chars_at(DisplayPoint::new(display_row, 0), ctx) {
+            count += 1;
+            column += c.len_utf8() as u32;
+            if column >= target {
+                break;
+            }
+        }
+        count
+    }
+
+    pub fn column_from_chars(&self, display_row: u32, char_count: u32, ctx: &AppContext) -> u32 {
+        let mut count = 0;
+        let mut column = 0;
+        for c in self.chars_at(DisplayPoint::new(display_row, 0), ctx) {
+            count += 1;
+            column += c.len_utf8() as u32;
+            if count >= char_count {
+                break;
+            }
+        }
+        column
+    }
+
     fn expand_tabs(&self, mut point: DisplayPoint, ctx: &AppContext) -> DisplayPoint {
         let chars = self
             .folds_snapshot
@@ -187,7 +212,6 @@ impl DisplayMapSnapshot {
         let (collapsed, expanded_char_column, to_next_stop) =
             collapse_tabs(chars, expanded, bias, self.tab_size);
         *point.column_mut() = collapsed as u32;
-
         (point, expanded_char_column, to_next_stop)
     }
 }
@@ -360,6 +384,10 @@ pub fn collapse_tabs(
             expanded_bytes += c.len_utf8();
         }
         collapsed_bytes += c.len_utf8();
+
+        if expanded_bytes > column {
+            panic!("column {} is inside of character {:?}", column, c);
+        }
     }
     (collapsed_bytes, expanded_chars, 0)
 }

zed/src/editor/movement.rs πŸ”—

@@ -16,7 +16,12 @@ pub fn left(map: &DisplayMap, mut point: DisplayPoint, app: &AppContext) -> Resu
 pub fn right(map: &DisplayMap, mut point: DisplayPoint, app: &AppContext) -> Result<DisplayPoint> {
     let max_column = map.line_len(point.row(), app);
     if point.column() < max_column {
-        *point.column_mut() += 1;
+        *point.column_mut() += map
+            .snapshot(app)
+            .chars_at(point, app)
+            .next()
+            .unwrap()
+            .len_utf8() as u32;
     } else if point.row() < map.max_point(app).row() {
         *point.row_mut() += 1;
         *point.column_mut() = 0;
@@ -35,9 +40,13 @@ pub fn up(
     } else {
         point.column()
     };
+
+    let map = map.snapshot(app);
+    let char_column = map.column_to_chars(point.row(), goal_column, app);
+
     if point.row() > 0 {
         *point.row_mut() -= 1;
-        *point.column_mut() = cmp::min(goal_column, map.line_len(point.row(), app));
+        *point.column_mut() = map.column_from_chars(point.row(), char_column, app);
     } else {
         point = DisplayPoint::new(0, 0);
     }
@@ -56,10 +65,14 @@ pub fn down(
     } else {
         point.column()
     };
+
     let max_point = map.max_point(app);
+    let map = map.snapshot(app);
+    let char_column = map.column_to_chars(point.row(), goal_column, app);
+
     if point.row() < max_point.row() {
         *point.row_mut() += 1;
-        *point.column_mut() = cmp::min(goal_column, map.line_len(point.row(), app))
+        *point.column_mut() = map.column_from_chars(point.row(), char_column, app);
     } else {
         point = max_point;
     }