Preserve indentation when soft-wrapping

Antonio Scandurra and Nathan Sobo created

Co-Authored-By: Nathan Sobo <nathan@zed.dev>

Change summary

zed/src/editor/display_map.rs              |  78 ++++++-------
zed/src/editor/display_map/line_wrapper.rs | 107 +++++++++++++++----
zed/src/editor/display_map/wrap_map.rs     | 128 +++++++++++++++--------
zed/src/editor/movement.rs                 |   4 
4 files changed, 200 insertions(+), 117 deletions(-)

Detailed changes

zed/src/editor/display_map.rs šŸ”—

@@ -111,22 +111,26 @@ impl DisplayMapSnapshot {
         DisplayPoint(self.wraps_snapshot.max_point())
     }
 
-    pub fn chunks_at(&self, point: DisplayPoint) -> wrap_map::Chunks {
-        self.wraps_snapshot.chunks_at(point.0)
+    pub fn chunks_at(&self, display_row: u32) -> wrap_map::Chunks {
+        self.wraps_snapshot.chunks_at(display_row)
     }
 
-    pub fn highlighted_chunks_for_rows(&mut self, rows: Range<u32>) -> wrap_map::HighlightedChunks {
-        self.wraps_snapshot.highlighted_chunks_for_rows(rows)
+    pub fn highlighted_chunks_for_rows(
+        &mut self,
+        display_rows: Range<u32>,
+    ) -> wrap_map::HighlightedChunks {
+        self.wraps_snapshot
+            .highlighted_chunks_for_rows(display_rows)
     }
 
-    pub fn chars_at<'a>(&'a self, point: DisplayPoint) -> impl Iterator<Item = char> + 'a {
-        self.chunks_at(point).flat_map(str::chars)
+    pub fn chars_at<'a>(&'a self, display_row: u32) -> impl Iterator<Item = char> + 'a {
+        self.chunks_at(display_row).flat_map(str::chars)
     }
 
     pub fn column_to_chars(&self, display_row: u32, target: u32) -> u32 {
         let mut count = 0;
         let mut column = 0;
-        for c in self.chars_at(DisplayPoint::new(display_row, 0)) {
+        for c in self.chars_at(display_row) {
             if column >= target {
                 break;
             }
@@ -139,7 +143,7 @@ impl DisplayMapSnapshot {
     pub fn column_from_chars(&self, display_row: u32, char_count: u32) -> u32 {
         let mut count = 0;
         let mut column = 0;
-        for c in self.chars_at(DisplayPoint::new(display_row, 0)) {
+        for c in self.chars_at(display_row) {
             if c == '\n' || count >= char_count {
                 break;
             }
@@ -174,12 +178,12 @@ impl DisplayMapSnapshot {
     }
 
     pub fn text(&self) -> String {
-        self.chunks_at(DisplayPoint::zero()).collect()
+        self.chunks_at(0).collect()
     }
 
     pub fn line(&self, display_row: u32) -> String {
         let mut result = String::new();
-        for chunk in self.chunks_at(DisplayPoint::new(display_row, 0)) {
+        for chunk in self.chunks_at(display_row) {
             if let Some(ix) = chunk.find('\n') {
                 result.push_str(&chunk[0..ix]);
                 break;
@@ -193,7 +197,7 @@ impl DisplayMapSnapshot {
     pub fn line_indent(&self, display_row: u32) -> (u32, bool) {
         let mut indent = 0;
         let mut is_blank = true;
-        for c in self.chars_at(DisplayPoint::new(display_row, 0)) {
+        for c in self.chars_at(display_row) {
             if c == ' ' {
                 indent += 1;
             } else {
@@ -378,10 +382,8 @@ mod tests {
 
         let snapshot = map.update(&mut cx, |map, cx| map.snapshot(cx));
         assert_eq!(
-            snapshot
-                .chunks_at(DisplayPoint::new(0, 3))
-                .collect::<String>(),
-            " two \nthree four \nfive\nsix seven \neight"
+            snapshot.chunks_at(0).collect::<String>(),
+            "one two \nthree four \nfive\nsix seven \neight"
         );
         assert_eq!(
             snapshot.clip_point(DisplayPoint::new(0, 8), Bias::Left),
@@ -399,9 +401,7 @@ mod tests {
 
         let snapshot = map.update(&mut cx, |map, cx| map.snapshot(cx));
         assert_eq!(
-            snapshot
-                .chunks_at(DisplayPoint::new(1, 0))
-                .collect::<String>(),
+            snapshot.chunks_at(1).collect::<String>(),
             "three four \nfive\nsix and \nseven eight"
         );
     }
@@ -431,22 +431,20 @@ mod tests {
         });
 
         assert_eq!(
-            &map.update(cx, |map, cx| map.snapshot(cx))
-                .chunks_at(DisplayPoint::new(1, 0))
-                .collect::<String>()[0..10],
-            "    b   bb"
-        );
-        assert_eq!(
-            &map.update(cx, |map, cx| map.snapshot(cx))
-                .chunks_at(DisplayPoint::new(1, 2))
-                .collect::<String>()[0..10],
-            "  b   bbbb"
+            map.update(cx, |map, cx| map.snapshot(cx))
+                .chunks_at(1)
+                .collect::<String>()
+                .lines()
+                .next(),
+            Some("    b   bbbbb")
         );
         assert_eq!(
-            &map.update(cx, |map, cx| map.snapshot(cx))
-                .chunks_at(DisplayPoint::new(1, 6))
-                .collect::<String>()[0..13],
-            "  bbbbb\nc   c"
+            map.update(cx, |map, cx| map.snapshot(cx))
+                .chunks_at(2)
+                .collect::<String>()
+                .lines()
+                .next(),
+            Some("c   ccccc")
         );
     }
 
@@ -676,6 +674,12 @@ mod tests {
         });
         let map = map.update(cx, |map, cx| map.snapshot(cx));
         assert_eq!(map.text(), "āœ…       α\nβ   \nšŸ€Ī²      γ");
+        assert_eq!(
+            map.chunks_at(0).collect::<String>(),
+            "āœ…       α\nβ   \nšŸ€Ī²      γ"
+        );
+        assert_eq!(map.chunks_at(1).collect::<String>(), "β   \nšŸ€Ī²      γ");
+        assert_eq!(map.chunks_at(2).collect::<String>(), "šŸ€Ī²      γ");
 
         let point = Point::new(0, "āœ…\t\t".len() as u32);
         let display_point = DisplayPoint::new(0, "āœ…       ".len() as u32);
@@ -701,11 +705,6 @@ mod tests {
             DisplayPoint::new(0, "āœ…      ".len() as u32).to_buffer_point(&map, Bias::Left),
             Point::new(0, "āœ…\t".len() as u32),
         );
-        assert_eq!(
-            map.chunks_at(DisplayPoint::new(0, "āœ…      ".len() as u32))
-                .collect::<String>(),
-            " α\nβ   \nšŸ€Ī²      γ"
-        );
         assert_eq!(
             DisplayPoint::new(0, "āœ… ".len() as u32).to_buffer_point(&map, Bias::Right),
             Point::new(0, "āœ…\t".len() as u32),
@@ -714,11 +713,6 @@ mod tests {
             DisplayPoint::new(0, "āœ… ".len() as u32).to_buffer_point(&map, Bias::Left),
             Point::new(0, "āœ…".len() as u32),
         );
-        assert_eq!(
-            map.chunks_at(DisplayPoint::new(0, "āœ… ".len() as u32))
-                .collect::<String>(),
-            "      α\nβ   \nšŸ€Ī²      γ"
-        );
 
         // Clipping display points inside of multi-byte characters
         assert_eq!(

zed/src/editor/display_map/line_wrapper.rs šŸ”—

@@ -3,6 +3,7 @@ use gpui::{fonts::FontId, FontCache, FontSystem};
 use std::{
     cell::RefCell,
     collections::HashMap,
+    iter,
     ops::{Deref, DerefMut},
     sync::Arc,
 };
@@ -11,6 +12,18 @@ thread_local! {
     static WRAPPERS: RefCell<Vec<LineWrapper>> = Default::default();
 }
 
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+pub struct Boundary {
+    pub ix: usize,
+    pub next_indent: u32,
+}
+
+impl Boundary {
+    fn new(ix: usize, next_indent: u32) -> Self {
+        Self { ix, next_indent }
+    }
+}
+
 pub struct LineWrapper {
     font_system: Arc<dyn FontSystem>,
     font_id: FontId,
@@ -20,6 +33,8 @@ pub struct LineWrapper {
 }
 
 impl LineWrapper {
+    pub const MAX_INDENT: u32 = 256;
+
     pub fn thread_local(
         font_system: Arc<dyn FontSystem>,
         font_cache: &FontCache,
@@ -60,33 +75,44 @@ impl LineWrapper {
         }
     }
 
-    #[cfg(test)]
-    pub fn wrap_line_with_shaping(&self, line: &str, wrap_width: f32) -> Vec<usize> {
-        self.font_system
-            .wrap_line(line, self.font_id, self.font_size, wrap_width)
-    }
-
     pub fn wrap_line<'a>(
         &'a mut self,
         line: &'a str,
         wrap_width: f32,
-    ) -> impl Iterator<Item = usize> + 'a {
+    ) -> impl Iterator<Item = Boundary> + 'a {
         let mut width = 0.0;
+        let mut first_non_whitespace_ix = None;
+        let mut indent = None;
         let mut last_candidate_ix = 0;
         let mut last_candidate_width = 0.0;
         let mut last_wrap_ix = 0;
         let mut prev_c = '\0';
-        let char_indices = line.char_indices();
-        char_indices.filter_map(move |(ix, c)| {
-            if c != '\n' {
-                if self.is_boundary(prev_c, c) {
+        let mut char_indices = line.char_indices();
+        iter::from_fn(move || {
+            while let Some((ix, c)) = char_indices.next() {
+                if c == '\n' {
+                    continue;
+                }
+
+                if self.is_boundary(prev_c, c) && first_non_whitespace_ix.is_some() {
                     last_candidate_ix = ix;
                     last_candidate_width = width;
                 }
 
+                if c != ' ' && first_non_whitespace_ix.is_none() {
+                    first_non_whitespace_ix = Some(ix);
+                }
+
                 let char_width = self.width_for_char(c);
                 width += char_width;
                 if width > wrap_width && ix > last_wrap_ix {
+                    if let (None, Some(first_non_whitespace_ix)) = (indent, first_non_whitespace_ix)
+                    {
+                        indent = Some(
+                            Self::MAX_INDENT.min((first_non_whitespace_ix - last_wrap_ix) as u32),
+                        );
+                    }
+
                     if last_candidate_ix > 0 {
                         last_wrap_ix = last_candidate_ix;
                         width -= last_candidate_width;
@@ -95,7 +121,12 @@ impl LineWrapper {
                         last_wrap_ix = ix;
                         width = char_width;
                     }
-                    return Some(last_wrap_ix);
+
+                    let indent_width =
+                        indent.map(|indent| indent as f32 * self.width_for_char(' '));
+                    width += indent_width.unwrap_or(0.);
+
+                    return Some(Boundary::new(last_wrap_ix, indent.unwrap_or(0)));
                 }
                 prev_c = c;
             }
@@ -105,10 +136,7 @@ impl LineWrapper {
     }
 
     fn is_boundary(&self, prev: char, next: char) -> bool {
-        if prev == ' ' || next == ' ' {
-            return true;
-        }
-        false
+        (prev == ' ') && (next != ' ')
     }
 
     #[inline(always)]
@@ -184,27 +212,54 @@ mod tests {
         };
 
         let mut wrapper = LineWrapper::new(font_system, &font_cache, settings);
-
         assert_eq!(
-            wrapper.wrap_line_with_shaping("aa bbb cccc ddddd eeee", 72.0),
-            &[7, 12, 18],
+            wrapper
+                .wrap_line("aa bbb cccc ddddd eeee", 72.0)
+                .collect::<Vec<_>>(),
+            &[
+                Boundary::new(7, 0),
+                Boundary::new(12, 0),
+                Boundary::new(18, 0)
+            ],
         );
         assert_eq!(
             wrapper
-                .wrap_line("aa bbb cccc ddddd eeee", 72.0)
+                .wrap_line("aaa aaaaaaaaaaaaaaaaaa", 72.0)
                 .collect::<Vec<_>>(),
-            &[7, 12, 18],
+            &[
+                Boundary::new(4, 0),
+                Boundary::new(11, 0),
+                Boundary::new(18, 0)
+            ],
         );
-
         assert_eq!(
-            wrapper.wrap_line_with_shaping("aaa aaaaaaaaaaaaaaaaaa", 72.0),
-            &[4, 11, 18],
+            wrapper.wrap_line("     aaaaaaa", 72.).collect::<Vec<_>>(),
+            &[
+                Boundary::new(7, 5),
+                Boundary::new(9, 5),
+                Boundary::new(11, 5),
+            ]
         );
         assert_eq!(
             wrapper
-                .wrap_line("aaa aaaaaaaaaaaaaaaaaa", 72.0)
+                .wrap_line("                            ", 72.)
+                .collect::<Vec<_>>(),
+            &[
+                Boundary::new(7, 0),
+                Boundary::new(14, 0),
+                Boundary::new(21, 0)
+            ]
+        );
+        assert_eq!(
+            wrapper
+                .wrap_line("          aaaaaaaaaaaaaa", 72.)
                 .collect::<Vec<_>>(),
-            &[4, 11, 18],
+            &[
+                Boundary::new(7, 0),
+                Boundary::new(14, 3),
+                Boundary::new(18, 3),
+                Boundary::new(22, 3),
+            ]
         );
     }
 }

zed/src/editor/display_map/wrap_map.rs šŸ”—

@@ -11,6 +11,7 @@ use crate::{
     Settings,
 };
 use gpui::{Entity, ModelContext, Task};
+use lazy_static::lazy_static;
 use smol::future::yield_now;
 use std::{collections::VecDeque, ops::Range, time::Duration};
 
@@ -391,11 +392,11 @@ impl Snapshot {
                     }
 
                     let mut prev_boundary_ix = 0;
-                    for boundary_ix in line_wrapper.wrap_line(&line, wrap_width) {
-                        let wrapped = &line[prev_boundary_ix..boundary_ix];
+                    for boundary in line_wrapper.wrap_line(&line, wrap_width) {
+                        let wrapped = &line[prev_boundary_ix..boundary.ix];
                         push_isomorphic(&mut edit_transforms, TextSummary::from(wrapped));
-                        edit_transforms.push(Transform::newline());
-                        prev_boundary_ix = boundary_ix;
+                        edit_transforms.push(Transform::wrap(boundary.next_indent));
+                        prev_boundary_ix = boundary.ix;
                     }
 
                     if prev_boundary_ix < line.len() {
@@ -453,11 +454,14 @@ impl Snapshot {
         self.check_invariants();
     }
 
-    pub fn chunks_at(&self, point: WrapPoint) -> Chunks {
+    pub fn chunks_at(&self, wrap_row: u32) -> Chunks {
+        let point = WrapPoint::new(wrap_row, 0);
         let mut transforms = self.transforms.cursor::<WrapPoint, TabPoint>();
         transforms.seek(&point, Bias::Right, &());
-        let input_position =
-            TabPoint(transforms.sum_start().0 + (point.0 - transforms.seek_start().0));
+        let mut input_position = TabPoint(transforms.sum_start().0);
+        if transforms.item().map_or(false, |t| t.is_isomorphic()) {
+            input_position.0 += point.0 - transforms.seek_start().0;
+        }
         let input_chunks = self.tab_snapshot.chunks_at(input_position);
         Chunks {
             input_chunks,
@@ -472,8 +476,10 @@ impl Snapshot {
         let output_end = WrapPoint::new(rows.end, 0);
         let mut transforms = self.transforms.cursor::<WrapPoint, TabPoint>();
         transforms.seek(&output_start, Bias::Right, &());
-        let input_start =
-            TabPoint(transforms.sum_start().0 + (output_start.0 - transforms.seek_start().0));
+        let mut input_start = TabPoint(transforms.sum_start().0);
+        if transforms.item().map_or(false, |t| t.is_isomorphic()) {
+            input_start.0 += output_start.0 - transforms.seek_start().0;
+        }
         let input_end = self
             .to_tab_point(output_end)
             .min(self.tab_snapshot.max_point());
@@ -493,7 +499,7 @@ impl Snapshot {
 
     pub fn line_len(&self, row: u32) -> u32 {
         let mut len = 0;
-        for chunk in self.chunks_at(WrapPoint::new(row, 0)) {
+        for chunk in self.chunks_at(row) {
             if let Some(newline_ix) = chunk.find('\n') {
                 len += newline_ix;
                 break;
@@ -511,7 +517,10 @@ impl Snapshot {
     pub fn buffer_rows(&self, start_row: u32) -> BufferRows {
         let mut transforms = self.transforms.cursor::<WrapPoint, TabPoint>();
         transforms.seek(&WrapPoint::new(start_row, 0), Bias::Right, &());
-        let input_row = transforms.sum_start().row() + (start_row - transforms.seek_start().row());
+        let mut input_row = transforms.sum_start().row();
+        if transforms.item().map_or(false, |t| t.is_isomorphic()) {
+            input_row += start_row - transforms.seek_start().row();
+        }
         let mut input_buffer_rows = self.tab_snapshot.buffer_rows(input_row);
         let input_buffer_row = input_buffer_rows.next().unwrap();
         BufferRows {
@@ -526,7 +535,11 @@ impl Snapshot {
     pub fn to_tab_point(&self, point: WrapPoint) -> TabPoint {
         let mut cursor = self.transforms.cursor::<WrapPoint, TabPoint>();
         cursor.seek(&point, Bias::Right, &());
-        TabPoint(cursor.sum_start().0 + (point.0 - cursor.seek_start().0))
+        let mut tab_point = cursor.sum_start().0;
+        if cursor.item().map_or(false, |t| t.is_isomorphic()) {
+            tab_point += point.0 - cursor.seek_start().0;
+        }
+        TabPoint(tab_point)
     }
 
     pub fn to_wrap_point(&self, point: TabPoint) -> WrapPoint {
@@ -539,8 +552,8 @@ impl Snapshot {
         if bias == Bias::Left {
             let mut cursor = self.transforms.cursor::<WrapPoint, ()>();
             cursor.seek(&point, Bias::Right, &());
-            let transform = cursor.item().expect("invalid point");
-            if !transform.is_isomorphic() {
+            if cursor.item().map_or(false, |t| !t.is_isomorphic()) {
+                point = *cursor.seek_start();
                 *point.column_mut() -= 1;
             }
         }
@@ -559,11 +572,9 @@ impl Snapshot {
             {
                 let mut transforms = self.transforms.cursor::<(), ()>().peekable();
                 while let Some(transform) = transforms.next() {
-                    let next_transform = transforms.peek();
-                    assert!(
-                        !transform.is_isomorphic()
-                            || next_transform.map_or(true, |t| !t.is_isomorphic())
-                    );
+                    if let Some(next_transform) = transforms.peek() {
+                        assert!(transform.is_isomorphic() != next_transform.is_isomorphic());
+                    }
                 }
             }
         }
@@ -576,9 +587,15 @@ impl<'a> Iterator for Chunks<'a> {
     fn next(&mut self) -> Option<Self::Item> {
         let transform = self.transforms.item()?;
         if let Some(display_text) = transform.display_text {
-            self.output_position.0 += transform.summary.output.lines;
-            self.transforms.next(&());
-            return Some(display_text);
+            if self.output_position > *self.transforms.seek_start() {
+                self.output_position.0.column += transform.summary.output.lines.column;
+                self.transforms.next(&());
+                return Some(&display_text[1..]);
+            } else {
+                self.output_position.0 += transform.summary.output.lines;
+                self.transforms.next(&());
+                return Some(display_text);
+            }
         }
 
         if self.input_chunk.is_empty() {
@@ -619,9 +636,23 @@ impl<'a> Iterator for HighlightedChunks<'a> {
 
         let transform = self.transforms.item()?;
         if let Some(display_text) = transform.display_text {
-            self.output_position.0 += transform.summary.output.lines;
+            let mut start_ix = 0;
+            let mut end_ix = display_text.len();
+            let mut summary = transform.summary.output.lines;
+
+            if self.output_position > *self.transforms.seek_start() {
+                // Exclude newline starting prior to the desired row.
+                start_ix = 1;
+                summary.row = 0;
+            } else if self.output_position.row() + 1 >= self.max_output_row {
+                // Exclude soft indentation ending after the desired row.
+                end_ix = 1;
+                summary.column = 0;
+            }
+
+            self.output_position.0 += summary;
             self.transforms.next(&());
-            return Some((display_text, self.style_id));
+            return Some((&display_text[start_ix..end_ix], self.style_id));
         }
 
         if self.input_chunk.is_empty() {
@@ -688,19 +719,28 @@ impl Transform {
         }
     }
 
-    fn newline() -> Self {
+    fn wrap(indent: u32) -> Self {
+        lazy_static! {
+            static ref WRAP_TEXT: String = {
+                let mut wrap_text = String::new();
+                wrap_text.push('\n');
+                wrap_text.extend((0..LineWrapper::MAX_INDENT as usize).map(|_| ' '));
+                wrap_text
+            };
+        }
+
         Self {
             summary: TransformSummary {
                 input: TextSummary::default(),
                 output: TextSummary {
-                    lines: Point::new(1, 0),
+                    lines: Point::new(1, indent),
                     first_line_chars: 0,
-                    last_line_chars: 0,
-                    longest_row: 0,
-                    longest_row_chars: 0,
+                    last_line_chars: indent,
+                    longest_row: 1,
+                    longest_row_chars: indent,
                 },
             },
-            display_text: Some("\n"),
+            display_text: Some(&WRAP_TEXT[..1 + indent as usize]),
         }
     }
 
@@ -753,11 +793,6 @@ impl WrapPoint {
         Self(super::Point::new(row, column))
     }
 
-    #[cfg(test)]
-    pub fn zero() -> Self {
-        Self::new(0, 0)
-    }
-
     pub fn row(self) -> u32 {
         self.0.row
     }
@@ -918,7 +953,7 @@ mod tests {
                     map.sync(tabs_snapshot.clone(), edits, cx)
                 });
                 snapshot.check_invariants();
-                interpolated_snapshot.verify_chunks(&mut rng);
+                snapshot.verify_chunks(&mut rng);
 
                 if wrap_map.read_with(&cx, |map, _| map.is_rewrapping()) && rng.gen_bool(0.4) {
                     log::info!("Waiting for wrapping to finish");
@@ -928,18 +963,17 @@ mod tests {
                 }
 
                 if !wrap_map.read_with(&cx, |map, _| map.is_rewrapping()) {
-                    log::info!("Wrapping finished");
                     snapshot =
                         wrap_map.update(&mut cx, |map, cx| map.sync(tabs_snapshot, Vec::new(), cx));
-                    snapshot.check_invariants();
-                    interpolated_snapshot.verify_chunks(&mut rng);
                     let actual_text = snapshot.text();
+                    log::info!("Wrapping finished: {:?}", actual_text);
+                    snapshot.check_invariants();
+                    snapshot.verify_chunks(&mut rng);
                     assert_eq!(
                         actual_text, expected_text,
                         "unwrapped text is: {:?}",
                         unwrapped_text
                     );
-                    log::info!("New wrapped text: {:?}", actual_text);
                     interpolated_snapshot = snapshot.clone();
                 }
             }
@@ -959,10 +993,11 @@ mod tests {
                 }
 
                 let mut prev_ix = 0;
-                for ix in line_wrapper.wrap_line(line, wrap_width) {
-                    wrapped_text.push_str(&line[prev_ix..ix]);
+                for boundary in line_wrapper.wrap_line(line, wrap_width) {
+                    wrapped_text.push_str(&line[prev_ix..boundary.ix]);
                     wrapped_text.push('\n');
-                    prev_ix = ix;
+                    wrapped_text.push_str(&" ".repeat(boundary.next_indent as usize));
+                    prev_ix = boundary.ix;
                 }
                 wrapped_text.push_str(&line[prev_ix..]);
             }
@@ -974,7 +1009,7 @@ mod tests {
 
     impl Snapshot {
         fn text(&self) -> String {
-            self.chunks_at(WrapPoint::zero()).collect()
+            self.chunks_at(0).collect()
         }
 
         fn verify_chunks(&mut self, rng: &mut impl Rng) {
@@ -983,9 +1018,7 @@ mod tests {
                 let start_row = rng.gen_range(0..=end_row);
                 end_row += 1;
 
-                let mut expected_text = self
-                    .chunks_at(WrapPoint::new(start_row, 0))
-                    .collect::<String>();
+                let mut expected_text = self.chunks_at(start_row).collect::<String>();
                 if expected_text.ends_with("\n") {
                     expected_text.push('\n');
                 }
@@ -997,6 +1030,7 @@ mod tests {
                 if end_row <= self.max_point().row() {
                     expected_text.push('\n');
                 }
+
                 let actual_text = self
                     .highlighted_chunks_for_rows(start_row..end_row)
                     .map(|c| c.0)

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

@@ -94,7 +94,7 @@ pub fn prev_word_boundary(map: &DisplayMapSnapshot, point: DisplayPoint) -> Resu
         let mut boundary = DisplayPoint::new(point.row(), 0);
         let mut column = 0;
         let mut prev_c = None;
-        for c in map.chars_at(boundary) {
+        for c in map.chars_at(point.row()) {
             if column >= point.column() {
                 break;
             }
@@ -115,7 +115,7 @@ pub fn next_word_boundary(
     mut point: DisplayPoint,
 ) -> Result<DisplayPoint> {
     let mut prev_c = None;
-    for c in map.chars_at(point) {
+    for c in map.chars_at(point.row()).skip(point.column() as usize) {
         if prev_c.is_some() && (c == '\n' || char_kind(prev_c.unwrap()) != char_kind(c)) {
             break;
         }