diff --git a/zed/src/editor/buffer/rope.rs b/zed/src/editor/buffer/rope.rs index 15cf7ff0d7..195d89386b 100644 --- a/zed/src/editor/buffer/rope.rs +++ b/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 } } diff --git a/zed/src/editor/buffer_view.rs b/zed/src/editor/buffer_view.rs index c2964a41df..23b5b08e90 100644 --- a/zed/src/editor/buffer_view.rs +++ b/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)); diff --git a/zed/src/editor/display_map/mod.rs b/zed/src/editor/display_map/mod.rs index d1b09f6447..919f296749 100644 --- a/zed/src/editor/display_map/mod.rs +++ b/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) } diff --git a/zed/src/editor/movement.rs b/zed/src/editor/movement.rs index 3ac135d08c..3e89537f41 100644 --- a/zed/src/editor/movement.rs +++ b/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 { 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; }