diff --git a/zed/src/editor/buffer/mod.rs b/zed/src/editor/buffer/mod.rs index 7c2ca518becdfc99b9effef482d59f90406c4d93..a86182fbae89b1dfb90ad202a14ae81d0ce54bd5 100644 --- a/zed/src/editor/buffer/mod.rs +++ b/zed/src/editor/buffer/mod.rs @@ -11,6 +11,7 @@ pub use selection::*; use similar::{ChangeTag, TextDiff}; use crate::{ + editor::Bias, operation_queue::{self, OperationQueue}, sum_tree::{self, FilterCursor, SeekBias, SumTree}, time::{self, ReplicaId}, @@ -1865,12 +1866,12 @@ impl Buffer { } } - pub fn next_char_boundary(&self, offset: usize) -> usize { - self.visible_text.next_char_boundary(offset) + pub fn clip_point(&self, point: Point, bias: Bias) -> Point { + self.visible_text.clip_point(point, bias) } - pub fn prev_char_boundary(&self, offset: usize) -> usize { - self.visible_text.prev_char_boundary(offset) + pub fn clip_offset(&self, offset: usize, bias: Bias) -> usize { + self.visible_text.clip_offset(offset, bias) } } @@ -3232,8 +3233,8 @@ mod tests { impl Buffer { fn random_byte_range(&mut self, start_offset: usize, rng: &mut impl Rng) -> Range { - let end = self.next_char_boundary(rng.gen_range(start_offset..=self.len())); - let start = self.prev_char_boundary(rng.gen_range(start_offset..=end)); + let end = self.clip_offset(rng.gen_range(start_offset..=self.len()), Bias::Right); + let start = self.clip_offset(rng.gen_range(start_offset..=end), Bias::Left); start..end } diff --git a/zed/src/editor/buffer/rope.rs b/zed/src/editor/buffer/rope.rs index 195d89386b6a3744a679f35202f2f4ebb3675c97..ddf1a92cbfb4a2b8647cf5b011e142acba60d8a4 100644 --- a/zed/src/editor/buffer/rope.rs +++ b/zed/src/editor/buffer/rope.rs @@ -1,5 +1,8 @@ use super::Point; -use crate::sum_tree::{self, SeekBias, SumTree}; +use crate::{ + editor::Bias, + sum_tree::{self, SeekBias, SumTree}, +}; use arrayvec::ArrayString; use smallvec::SmallVec; use std::{cmp, ops::Range, str}; @@ -142,32 +145,38 @@ impl Rope { cursor.start().bytes + cursor.item().map_or(0, |chunk| chunk.to_offset(overshoot)) } - pub fn next_char_boundary(&self, mut offset: usize) -> usize { - assert!(offset <= self.summary().bytes); + pub fn clip_offset(&self, mut offset: usize, bias: Bias) -> usize { let mut cursor = self.chunks.cursor::(); cursor.seek(&offset, SeekBias::Left, &()); if let Some(chunk) = cursor.item() { let mut ix = offset - cursor.start(); while !chunk.0.is_char_boundary(ix) { - ix += 1; - offset += 1; + match bias { + Bias::Left => { + ix -= 1; + offset -= 1; + } + Bias::Right => { + ix += 1; + offset += 1; + } + } } + offset + } else { + self.summary().bytes } - offset } - pub fn prev_char_boundary(&self, mut offset: usize) -> usize { - assert!(offset <= self.summary().bytes); - let mut cursor = self.chunks.cursor::(); - cursor.seek(&offset, SeekBias::Left, &()); + pub fn clip_point(&self, point: Point, bias: Bias) -> Point { + let mut cursor = self.chunks.cursor::(); + cursor.seek(&point, SeekBias::Right, &()); if let Some(chunk) = cursor.item() { - let mut ix = offset - cursor.start(); - while !chunk.0.is_char_boundary(ix) { - ix -= 1; - offset -= 1; - } + let overshoot = point - cursor.start(); + *cursor.start() + chunk.clip_point(overshoot, bias) + } else { + self.summary().lines } - offset } } @@ -351,6 +360,22 @@ impl Chunk { } offset } + + fn clip_point(&self, target: Point, bias: Bias) -> Point { + for (row, line) in self.0.split('\n').enumerate() { + if row == target.row as usize { + let mut column = target.column.min(line.len() as u32); + while !line.is_char_boundary(column as usize) { + match bias { + Bias::Left => column -= 1, + Bias::Right => column += 1, + } + } + return Point::new(row as u32, column); + } + } + unreachable!() + } } impl sum_tree::Item for Chunk { @@ -466,30 +491,13 @@ fn find_split_ix(text: &str) -> usize { ix } -#[inline(always)] -fn assert_char_boundary(s: &str, ix: usize) { - if !s.is_char_boundary(ix) { - let mut char_start = ix; - while !s.is_char_boundary(char_start) { - char_start -= 1; - } - - let ch = s[char_start..].chars().next().unwrap(); - let char_range = char_start..char_start + ch.len_utf8(); - panic!( - "byte index {} is not a char boundary; it is inside {:?} (bytes {:?}) of `{}`", - ix, ch, char_range, s - ); - } -} - #[cfg(test)] mod tests { - use crate::util::RandomCharIter; - use super::*; + use crate::util::RandomCharIter; use rand::prelude::*; use std::env; + use Bias::{Left, Right}; #[test] fn test_all_4_byte_chars() { @@ -520,8 +528,8 @@ mod tests { let mut expected = String::new(); let mut actual = Rope::new(); for _ in 0..operations { - let end_ix = actual.next_char_boundary(rng.gen_range(0..=expected.len())); - let start_ix = actual.prev_char_boundary(rng.gen_range(0..=end_ix)); + let end_ix = clip_offset(&expected, rng.gen_range(0..=expected.len()), Right); + let start_ix = clip_offset(&expected, rng.gen_range(0..=end_ix), Left); let len = rng.gen_range(0..=64); let new_text: String = RandomCharIter::new(&mut rng).take(len).collect(); @@ -539,8 +547,8 @@ mod tests { log::info!("text: {:?}", expected); for _ in 0..5 { - let end_ix = actual.next_char_boundary(rng.gen_range(0..=expected.len())); - let start_ix = actual.prev_char_boundary(rng.gen_range(0..=end_ix)); + let end_ix = clip_offset(&expected, rng.gen_range(0..=expected.len()), Right); + let start_ix = clip_offset(&expected, rng.gen_range(0..=end_ix), Left); assert_eq!( actual.chunks_in_range(start_ix..end_ix).collect::(), &expected[start_ix..end_ix] @@ -560,8 +568,8 @@ mod tests { } for _ in 0..5 { - let end_ix = actual.next_char_boundary(rng.gen_range(0..=expected.len())); - let start_ix = actual.prev_char_boundary(rng.gen_range(0..=end_ix)); + let end_ix = clip_offset(&expected, rng.gen_range(0..=expected.len()), Right); + let start_ix = clip_offset(&expected, rng.gen_range(0..=end_ix), Left); assert_eq!( actual.cursor(start_ix).summary(end_ix), TextSummary::from(&expected[start_ix..end_ix]) @@ -571,6 +579,16 @@ mod tests { } } + fn clip_offset(text: &str, mut offset: usize, bias: Bias) -> usize { + while !text.is_char_boundary(offset) { + match bias { + Bias::Left => offset -= 1, + Bias::Right => offset += 1, + } + } + offset + } + impl Rope { fn text(&self) -> String { let mut text = String::new(); diff --git a/zed/src/editor/buffer/selection.rs b/zed/src/editor/buffer/selection.rs index 016cbe59b3f5530e5fcc097bf9045923c4df0b8c..e150ce0725966f49cdba352f008506eae9ce8913 100644 --- a/zed/src/editor/buffer/selection.rs +++ b/zed/src/editor/buffer/selection.rs @@ -1,8 +1,8 @@ use crate::{ editor::{ buffer::{Anchor, Buffer, Point, ToPoint}, - display_map::{Bias, DisplayMap}, - DisplayPoint, + display_map::DisplayMap, + Bias, DisplayPoint, }, time, }; diff --git a/zed/src/editor/buffer_view.rs b/zed/src/editor/buffer_view.rs index 23b5b08e90fa28426ecf2138bcebcdd456080092..94a98384396de900a99f688899013c88902943f6 100644 --- a/zed/src/editor/buffer_view.rs +++ b/zed/src/editor/buffer_view.rs @@ -650,16 +650,16 @@ impl BufferView { if let Err(error) = buffer.edit(edit_ranges, text.as_str(), Some(ctx)) { log::error!("error inserting text: {}", error); }; - let char_count = text.chars().count() as isize; + let text_len = text.len() as isize; let mut delta = 0_isize; new_selections = old_selections .into_iter() .map(|(id, range)| { let start = range.start as isize; let end = range.end as isize; - let anchor = buffer.anchor_before((start + delta + char_count) as usize); + let anchor = buffer.anchor_before((start + delta + text_len) as usize); let deleted_count = end - start; - delta += char_count - deleted_count; + delta += text_len - deleted_count; Selection { id, start: anchor.clone(), @@ -2358,11 +2358,7 @@ impl workspace::ItemView for BufferView { #[cfg(test)] mod tests { use super::*; - use crate::{ - editor::Point, - settings, - test::{multibyte_sample_text, sample_text}, - }; + use crate::{editor::Point, settings, test::sample_text}; use unindent::Unindent; #[gpui::test] @@ -2667,6 +2663,11 @@ mod tests { }); view.update(app, |view, ctx| { + assert_eq!( + view.selection_ranges(ctx.as_ref()), + &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)] + ); + view.move_down(&(), ctx); assert_eq!( view.selection_ranges(ctx.as_ref()), @@ -2728,6 +2729,11 @@ mod tests { assert_eq!('ⓐ'.len_utf8(), 3); assert_eq!('α'.len_utf8(), 2); + fn empty_range(row: usize, column: usize) -> Range { + let point = DisplayPoint::new(row as u32, column as u32); + point..point + } + view.update(app, |view, ctx| { view.fold_ranges( vec![ @@ -2742,43 +2748,80 @@ mod tests { view.move_right(&(), ctx); assert_eq!( view.selection_ranges(ctx.as_ref()), - &[DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3)] + &[empty_range(0, "ⓐ".len())] ); - - view.move_down(&(), ctx); + view.move_right(&(), ctx); assert_eq!( view.selection_ranges(ctx.as_ref()), - &[DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1)] + &[empty_range(0, "ⓐⓑ".len())] ); - view.move_right(&(), ctx); assert_eq!( view.selection_ranges(ctx.as_ref()), - &[DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2)] + &[empty_range(0, "ⓐⓑ…".len())] ); view.move_down(&(), ctx); assert_eq!( view.selection_ranges(ctx.as_ref()), - &[DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4)] + &[empty_range(1, "ab…".len())] ); - view.move_left(&(), ctx); assert_eq!( view.selection_ranges(ctx.as_ref()), - &[DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)] + &[empty_range(1, "ab".len())] + ); + view.move_left(&(), ctx); + assert_eq!( + view.selection_ranges(ctx.as_ref()), + &[empty_range(1, "a".len())] ); - view.move_up(&(), ctx); + view.move_down(&(), ctx); + assert_eq!( + view.selection_ranges(ctx.as_ref()), + &[empty_range(2, "α".len())] + ); + view.move_right(&(), ctx); + assert_eq!( + view.selection_ranges(ctx.as_ref()), + &[empty_range(2, "αβ".len())] + ); + view.move_right(&(), ctx); + assert_eq!( + view.selection_ranges(ctx.as_ref()), + &[empty_range(2, "αβ…".len())] + ); + view.move_right(&(), ctx); assert_eq!( view.selection_ranges(ctx.as_ref()), - &[DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1)] + &[empty_range(2, "αβ…ε".len())] ); view.move_up(&(), ctx); assert_eq!( view.selection_ranges(ctx.as_ref()), - &[DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3)] + &[empty_range(1, "ab…e".len())] + ); + view.move_up(&(), ctx); + assert_eq!( + view.selection_ranges(ctx.as_ref()), + &[empty_range(0, "ⓐⓑ…ⓔ".len())] + ); + view.move_left(&(), ctx); + assert_eq!( + view.selection_ranges(ctx.as_ref()), + &[empty_range(0, "ⓐⓑ…".len())] + ); + view.move_left(&(), ctx); + assert_eq!( + view.selection_ranges(ctx.as_ref()), + &[empty_range(0, "ⓐⓑ".len())] + ); + view.move_left(&(), ctx); + assert_eq!( + view.selection_ranges(ctx.as_ref()), + &[empty_range(0, "ⓐ".len())] ); }); } diff --git a/zed/src/editor/display_map/fold_map.rs b/zed/src/editor/display_map/fold_map.rs index 0176da26dec6c640d2a6cf6d8d232ac179b3211c..3560fd187918517e2ab8fedb9174b2243cf6a6a9 100644 --- a/zed/src/editor/display_map/fold_map.rs +++ b/zed/src/editor/display_map/fold_map.rs @@ -1,6 +1,6 @@ use super::{ buffer::{AnchorRangeExt, TextSummary}, - Anchor, Buffer, DisplayPoint, Edit, Point, ToOffset, + Anchor, Bias, Buffer, DisplayPoint, Edit, Point, ToOffset, }; use crate::{ editor::buffer, @@ -233,26 +233,6 @@ impl FoldMap { )) } - #[cfg(test)] - pub fn prev_char_boundary(&self, offset: DisplayOffset, ctx: &AppContext) -> DisplayOffset { - let transforms = self.sync(ctx); - let mut cursor = transforms.cursor::(); - cursor.seek(&offset, SeekBias::Right, &()); - if let Some(transform) = cursor.item() { - if transform.display_text.is_some() { - DisplayOffset(cursor.start().display.bytes) - } else { - let overshoot = offset.0 - cursor.start().display.bytes; - let buffer_offset = cursor.start().buffer.bytes + overshoot; - let buffer_boundary_offset = - self.buffer.read(ctx).prev_char_boundary(buffer_offset); - DisplayOffset(offset.0 - (buffer_offset - buffer_boundary_offset)) - } - } else { - offset - } - } - fn sync(&self, ctx: &AppContext) -> MutexGuard> { let buffer = self.buffer.read(ctx); let mut edits = buffer.edits_since(self.last_sync.lock().clone()).peekable(); @@ -471,6 +451,64 @@ impl FoldMapSnapshot { let overshoot = point.0 - cursor.start().display.lines; (cursor.start().buffer.lines + overshoot).to_offset(self.buffer.read(ctx)) } + + #[cfg(test)] + pub fn clip_offset( + &self, + offset: DisplayOffset, + bias: Bias, + ctx: &AppContext, + ) -> DisplayOffset { + let mut cursor = self.transforms.cursor::(); + cursor.seek(&offset, SeekBias::Right, &()); + if let Some(transform) = cursor.item() { + let transform_start = cursor.start().display.bytes; + if transform.display_text.is_some() { + if offset.0 == transform_start || matches!(bias, Bias::Left) { + DisplayOffset(transform_start) + } else { + DisplayOffset(cursor.end().display.bytes) + } + } else { + let overshoot = offset.0 - transform_start; + let buffer_offset = cursor.start().buffer.bytes + overshoot; + let clipped_buffer_offset = self.buffer.read(ctx).clip_offset(buffer_offset, bias); + DisplayOffset( + (offset.0 as isize + (clipped_buffer_offset as isize - buffer_offset as isize)) + as usize, + ) + } + } else { + DisplayOffset(self.transforms.summary().display.bytes) + } + } + + pub fn clip_point(&self, point: DisplayPoint, bias: Bias, ctx: &AppContext) -> DisplayPoint { + let mut cursor = self.transforms.cursor::(); + cursor.seek(&point, SeekBias::Right, &()); + if let Some(transform) = cursor.item() { + let transform_start = cursor.start().display.lines; + if transform.display_text.is_some() { + if point.0 == transform_start || matches!(bias, Bias::Left) { + DisplayPoint(transform_start) + } else { + DisplayPoint(cursor.end().display.lines) + } + } else { + let overshoot = point.0 - transform_start; + let buffer_position = cursor.start().buffer.lines + overshoot; + let clipped_buffer_position = + self.buffer.read(ctx).clip_point(buffer_position, bias); + DisplayPoint::new( + point.row(), + ((point.column() as i32) + clipped_buffer_position.column as i32 + - buffer_position.column as i32) as u32, + ) + } + } else { + DisplayPoint(self.transforms.summary().display.lines) + } + } } #[derive(Clone, Debug, Default, Eq, PartialEq)] @@ -864,6 +902,7 @@ mod tests { use crate::util::RandomCharIter; use rand::prelude::*; use std::env; + use Bias::{Left, Right}; let iterations = env::var("ITERATIONS") .map(|i| i.parse().expect("invalid `ITERATIONS` variable")) @@ -896,8 +935,8 @@ mod tests { let buffer = buffer.read(app); let mut to_fold = Vec::new(); for _ in 0..rng.gen_range(1..=5) { - let end = buffer.next_char_boundary(rng.gen_range(0..=buffer.len())); - let start = buffer.prev_char_boundary(rng.gen_range(0..=end)); + let end = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Right); + let start = buffer.clip_offset(rng.gen_range(0..=end), Left); to_fold.push(start..end); } log::info!("folding {:?}", to_fold); @@ -907,8 +946,8 @@ mod tests { let buffer = buffer.read(app); let mut to_unfold = Vec::new(); for _ in 0..rng.gen_range(1..=3) { - let end = buffer.next_char_boundary(rng.gen_range(0..=buffer.len())); - let start = buffer.prev_char_boundary(rng.gen_range(0..=end)); + let end = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Right); + let start = buffer.clip_offset(rng.gen_range(0..=end), Left); to_unfold.push(start..end); } log::info!("unfolding {:?}", to_unfold); @@ -989,8 +1028,9 @@ mod tests { } for _ in 0..5 { - let offset = map.prev_char_boundary( + let offset = map.snapshot(app.as_ref()).clip_offset( DisplayOffset(rng.gen_range(0..=map.len(app.as_ref()))), + Bias::Right, app.as_ref(), ); assert_eq!( @@ -1020,8 +1060,8 @@ mod tests { } for _ in 0..5 { - let end = buffer.next_char_boundary(rng.gen_range(0..=buffer.len())); - let start = buffer.prev_char_boundary(rng.gen_range(0..=end)); + let end = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Right); + let start = buffer.clip_offset(rng.gen_range(0..=end), Left); let expected_folds = map .folds .items() diff --git a/zed/src/editor/display_map/mod.rs b/zed/src/editor/display_map/mod.rs index 919f2967494a2ac4af2cc756f329ab6ad3e4c2e7..792df1c8d92d17127c42fa5bc20dd98e4db8670a 100644 --- a/zed/src/editor/display_map/mod.rs +++ b/zed/src/editor/display_map/mod.rs @@ -1,17 +1,11 @@ mod fold_map; -use super::{buffer, Anchor, Buffer, Edit, Point, ToOffset, ToPoint}; +use super::{buffer, Anchor, Bias, Buffer, Edit, Point, ToOffset, ToPoint}; pub use fold_map::BufferRows; use fold_map::{FoldMap, FoldMapSnapshot}; use gpui::{AppContext, ModelHandle}; use std::ops::Range; -#[derive(Copy, Clone)] -pub enum Bias { - Left, - Right, -} - pub struct DisplayMap { buffer: ModelHandle, fold_map: FoldMap, @@ -110,6 +104,7 @@ impl DisplayMap { .column() } + // TODO - make this delegate to the DisplayMapSnapshot pub fn max_point(&self, ctx: &AppContext) -> DisplayPoint { self.fold_map.max_point(ctx).expand_tabs(self, ctx) } @@ -168,11 +163,11 @@ impl DisplayMapSnapshot { 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 += 1; + column += c.len_utf8() as u32; } count } @@ -181,15 +176,23 @@ impl DisplayMapSnapshot { 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; } + count += 1; + column += c.len_utf8() as u32; } column } + pub fn clip_point(&self, point: DisplayPoint, bias: Bias, ctx: &AppContext) -> DisplayPoint { + self.expand_tabs( + self.folds_snapshot + .clip_point(self.collapse_tabs(point, bias, ctx).0, bias, ctx), + ctx, + ) + } + fn expand_tabs(&self, mut point: DisplayPoint, ctx: &AppContext) -> DisplayPoint { let chars = self .folds_snapshot @@ -364,7 +367,7 @@ pub fn collapse_tabs( let mut expanded_chars = 0; let mut collapsed_bytes = 0; while let Some(c) = chars.next() { - if expanded_bytes == column { + if expanded_bytes >= column { break; } @@ -383,19 +386,19 @@ pub fn collapse_tabs( expanded_chars += 1; expanded_bytes += c.len_utf8(); } - collapsed_bytes += c.len_utf8(); - if expanded_bytes > column { - panic!("column {} is inside of character {:?}", column, c); + if expanded_bytes > column && matches!(bias, Bias::Left) { + expanded_chars -= 1; + break; } + + collapsed_bytes += c.len_utf8(); } (collapsed_bytes, expanded_chars, 0) } #[cfg(test)] mod tests { - use gpui::MutableAppContext; - use super::*; use crate::test::*; @@ -446,25 +449,25 @@ mod tests { } #[gpui::test] - fn test_tabs_with_multibyte_chars(app: &mut MutableAppContext) { - let text = "✅\t\tx\nα\t\n🏀α\t\ty"; + fn test_tabs_with_multibyte_chars(app: &mut gpui::MutableAppContext) { + let text = "✅\t\tα\nβ\t\n🏀β\t\tγ"; let buffer = app.add_model(|ctx| Buffer::new(0, text, ctx)); let ctx = app.as_ref(); let map = DisplayMap::new(buffer.clone(), 4, ctx); - assert_eq!(map.text(ctx), "✅ x\nα \n🏀α y"); + assert_eq!(map.text(ctx), "✅ α\nβ \n🏀β γ"); let point = Point::new(0, "✅\t\t".len() as u32); let display_point = DisplayPoint::new(0, "✅ ".len() as u32); assert_eq!(point.to_display_point(&map, ctx), display_point); assert_eq!(display_point.to_buffer_point(&map, Bias::Left, ctx), point,); - let point = Point::new(1, "α\t".len() as u32); - let display_point = DisplayPoint::new(1, "α ".len() as u32); + let point = Point::new(1, "β\t".len() as u32); + let display_point = DisplayPoint::new(1, "β ".len() as u32); assert_eq!(point.to_display_point(&map, ctx), display_point); assert_eq!(display_point.to_buffer_point(&map, Bias::Left, ctx), point,); - let point = Point::new(2, "🏀α\t\t".len() as u32); - let display_point = DisplayPoint::new(2, "🏀α ".len() as u32); + let point = Point::new(2, "🏀β\t\t".len() as u32); + let display_point = DisplayPoint::new(2, "🏀β ".len() as u32); assert_eq!(point.to_display_point(&map, ctx), display_point); assert_eq!(display_point.to_buffer_point(&map, Bias::Left, ctx), point,); @@ -481,7 +484,7 @@ mod tests { map.snapshot(ctx) .chunks_at(DisplayPoint::new(0, "✅ ".len() as u32), ctx) .collect::(), - " x\nα \n🏀α y" + " α\nβ \n🏀β γ" ); assert_eq!( DisplayPoint::new(0, "✅ ".len() as u32).to_buffer_point(&map, Bias::Right, ctx), @@ -495,7 +498,25 @@ mod tests { map.snapshot(ctx) .chunks_at(DisplayPoint::new(0, "✅ ".len() as u32), ctx) .collect::(), - " x\nα \n🏀α y" + " α\nβ \n🏀β γ" + ); + + // Clipping display points inside of multi-byte characters + assert_eq!( + map.snapshot(ctx).clip_point( + DisplayPoint::new(0, "✅".len() as u32 - 1), + Bias::Left, + ctx + ), + DisplayPoint::new(0, 0) + ); + assert_eq!( + map.snapshot(ctx).clip_point( + DisplayPoint::new(0, "✅".len() as u32 - 1), + Bias::Right, + ctx + ), + DisplayPoint::new(0, "✅".len() as u32) ); } diff --git a/zed/src/editor/mod.rs b/zed/src/editor/mod.rs index 0229e6ae7e08169a4c5e4f7e8a265f712b55137b..d3c892367471b6a6a2aa46eabef36020853d5f2f 100644 --- a/zed/src/editor/mod.rs +++ b/zed/src/editor/mod.rs @@ -11,6 +11,12 @@ pub use display_map::DisplayPoint; use display_map::*; use std::{cmp, ops::Range}; +#[derive(Copy, Clone)] +pub enum Bias { + Left, + Right, +} + trait RangeExt { fn sorted(&self) -> Range; } diff --git a/zed/src/editor/movement.rs b/zed/src/editor/movement.rs index 3e89537f41c3fe4c9b69e64748a48018a231c623..b40573e5e8ea0d4f91112e77967154beedf0e4ee 100644 --- a/zed/src/editor/movement.rs +++ b/zed/src/editor/movement.rs @@ -1,7 +1,6 @@ -use super::{DisplayMap, DisplayPoint, SelectionGoal}; +use super::{Bias, DisplayMap, DisplayPoint, SelectionGoal}; use anyhow::Result; use gpui::AppContext; -use std::cmp; pub fn left(map: &DisplayMap, mut point: DisplayPoint, app: &AppContext) -> Result { if point.column() > 0 { @@ -10,23 +9,18 @@ pub fn left(map: &DisplayMap, mut point: DisplayPoint, app: &AppContext) -> Resu *point.row_mut() -= 1; *point.column_mut() = map.line_len(point.row(), app); } - Ok(point) + Ok(map.snapshot(app).clip_point(point, Bias::Left, app)) } 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() += map - .snapshot(app) - .chars_at(point, app) - .next() - .unwrap() - .len_utf8() as u32; + *point.column_mut() += 1; } else if point.row() < map.max_point(app).row() { *point.row_mut() += 1; *point.column_mut() = 0; } - Ok(point) + Ok(map.snapshot(app).clip_point(point, Bias::Right, app)) } pub fn up( @@ -35,18 +29,16 @@ pub fn up( goal: SelectionGoal, app: &AppContext, ) -> Result<(DisplayPoint, SelectionGoal)> { + let map = map.snapshot(app); let goal_column = if let SelectionGoal::Column(column) = goal { column } else { - point.column() + map.column_to_chars(point.row(), point.column(), app) }; - 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() = map.column_from_chars(point.row(), char_column, app); + *point.column_mut() = map.column_from_chars(point.row(), goal_column, app); } else { point = DisplayPoint::new(0, 0); } @@ -60,19 +52,17 @@ pub fn down( goal: SelectionGoal, app: &AppContext, ) -> Result<(DisplayPoint, SelectionGoal)> { + let max_point = map.max_point(app); + let map = map.snapshot(app); let goal_column = if let SelectionGoal::Column(column) = goal { column } else { - point.column() + map.column_to_chars(point.row(), point.column(), app) }; - 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() = map.column_from_chars(point.row(), char_column, app); + *point.column_mut() = map.column_from_chars(point.row(), goal_column, app); } else { point = max_point; }