Use chunk-wise DisplayMap iteration when laying out lines

Max Brunsfeld created

Change summary

zed/src/editor/buffer_view.rs          |  21 ++-
zed/src/editor/display_map/fold_map.rs |  55 +----------
zed/src/editor/display_map/mod.rs      | 128 ++++++++++++++++-----------
zed/src/editor/movement.rs             |   4 
4 files changed, 99 insertions(+), 109 deletions(-)

Detailed changes

zed/src/editor/buffer_view.rs 🔗

@@ -2140,23 +2140,26 @@ impl BufferView {
 
         let mut layouts = Vec::with_capacity(rows.len());
         let mut line = String::new();
-        let mut line_len = 0;
         let mut row = rows.start;
         let snapshot = self.display_map.snapshot(ctx);
-        let chars = snapshot.chars_at(DisplayPoint::new(rows.start, 0), ctx);
-        for char in chars.chain(Some('\n')) {
-            if char == '\n' {
-                layouts.push(layout_cache.layout_str(&line, font_size, &[(0..line_len, font_id)]));
+        let chunks = snapshot.chunks_at(DisplayPoint::new(rows.start, 0), ctx);
+        for (chunk_row, chunk_line) in chunks
+            .chain(Some("\n"))
+            .flat_map(|chunk| chunk.split("\n").enumerate())
+        {
+            if chunk_row > 0 {
+                layouts.push(layout_cache.layout_str(
+                    &line,
+                    font_size,
+                    &[(0..line.chars().count(), font_id)],
+                ));
                 line.clear();
-                line_len = 0;
                 row += 1;
                 if row == rows.end {
                     break;
                 }
-            } else {
-                line_len += 1;
-                line.push(char);
             }
+            line.push_str(chunk_line);
         }
 
         Ok(layouts)

zed/src/editor/display_map/fold_map.rs 🔗

@@ -3,7 +3,7 @@ use super::{
     Anchor, Buffer, DisplayPoint, Edit, Point, ToOffset,
 };
 use crate::{
-    editor::{buffer, rope},
+    editor::buffer,
     sum_tree::{self, Cursor, FilterCursor, SeekBias, SumTree},
     time,
 };
@@ -11,7 +11,6 @@ use gpui::{AppContext, ModelHandle};
 use parking_lot::{Mutex, MutexGuard};
 use std::{
     cmp::{self, Ordering},
-    iter::Take,
     ops::Range,
 };
 
@@ -442,19 +441,16 @@ impl FoldMapSnapshot {
         }
     }
 
-    pub fn chars_at<'a>(&'a self, point: DisplayPoint, ctx: &'a AppContext) -> Chars<'a> {
+    pub fn chars_at<'a>(
+        &'a self,
+        point: DisplayPoint,
+        ctx: &'a AppContext,
+    ) -> impl Iterator<Item = char> + 'a {
         let offset = self.to_display_offset(point, ctx);
-        let mut cursor = self.transforms.cursor();
-        cursor.seek(&offset, SeekBias::Right, &());
-        Chars {
-            cursor,
-            offset: offset.0,
-            buffer: self.buffer.read(ctx),
-            buffer_chars: None,
-        }
+        self.chunks_at(offset, ctx).flat_map(str::chars)
     }
 
-    fn to_display_offset(&self, point: DisplayPoint, ctx: &AppContext) -> DisplayOffset {
+    pub fn to_display_offset(&self, point: DisplayPoint, ctx: &AppContext) -> DisplayOffset {
         let mut cursor = self.transforms.cursor::<DisplayPoint, TransformSummary>();
         cursor.seek(&point, SeekBias::Right, &());
         let overshoot = point.0 - cursor.start().display.lines;
@@ -628,41 +624,6 @@ impl<'a> Iterator for BufferRows<'a> {
     }
 }
 
-pub struct Chars<'a> {
-    cursor: Cursor<'a, Transform, DisplayOffset, TransformSummary>,
-    offset: usize,
-    buffer: &'a Buffer,
-    buffer_chars: Option<Take<rope::Chars<'a>>>,
-}
-
-impl<'a> Iterator for Chars<'a> {
-    type Item = char;
-
-    fn next(&mut self) -> Option<Self::Item> {
-        if let Some(c) = self.buffer_chars.as_mut().and_then(|chars| chars.next()) {
-            self.offset += c.len_utf8();
-            return Some(c);
-        }
-
-        while self.offset == self.cursor.end().display.bytes && self.cursor.item().is_some() {
-            self.cursor.next();
-        }
-
-        self.cursor.item().and_then(|transform| {
-            if let Some(c) = transform.display_text {
-                self.offset += c.len();
-                Some(c.chars().next().unwrap())
-            } else {
-                let overshoot = self.offset - self.cursor.start().display.bytes;
-                let buffer_start = self.cursor.start().buffer.bytes + overshoot;
-                let char_count = self.cursor.end().buffer.bytes - buffer_start;
-                self.buffer_chars = Some(self.buffer.chars_at(buffer_start).take(char_count));
-                self.next()
-            }
-        })
-    }
-}
-
 pub struct Chunks<'a> {
     transform_cursor: Cursor<'a, Transform, DisplayOffset, TransformSummary>,
     buffer_chunks: buffer::ChunksIter<'a>,

zed/src/editor/display_map/mod.rs 🔗

@@ -67,15 +67,24 @@ impl DisplayMap {
 
     pub fn text(&self, ctx: &AppContext) -> String {
         self.snapshot(ctx)
-            .chars_at(DisplayPoint::zero(), ctx)
+            .chunks_at(DisplayPoint::zero(), ctx)
             .collect()
     }
 
     pub fn line(&self, display_row: u32, ctx: &AppContext) -> String {
-        self.snapshot(ctx)
-            .chars_at(DisplayPoint::new(display_row, 0), ctx)
-            .take_while(|c| *c != '\n')
-            .collect()
+        let mut result = String::new();
+        for chunk in self
+            .snapshot(ctx)
+            .chunks_at(DisplayPoint::new(display_row, 0), ctx)
+        {
+            if let Some(ix) = chunk.find('\n') {
+                result.push_str(&chunk[0..ix]);
+                break;
+            } else {
+                result.push_str(chunk);
+            }
+        }
+        result
     }
 
     pub fn line_indent(&self, display_row: u32, ctx: &AppContext) -> (u32, bool) {
@@ -83,7 +92,8 @@ impl DisplayMap {
         let mut is_blank = true;
         for c in self
             .snapshot(ctx)
-            .chars_at(DisplayPoint::new(display_row, 0), ctx)
+            .chunks_at(DisplayPoint::new(display_row, 0), ctx)
+            .flat_map(str::chars)
         {
             if c == ' ' {
                 indent += 1;
@@ -132,21 +142,28 @@ impl DisplayMapSnapshot {
         self.folds_snapshot.buffer_rows(start_row)
     }
 
-    pub fn chars_at<'a>(&'a self, point: DisplayPoint, app: &'a AppContext) -> Chars<'a> {
+    pub fn chunks_at<'a>(&'a self, point: DisplayPoint, app: &'a AppContext) -> Chunks<'a> {
         let column = point.column() as usize;
-        let (point, to_next_stop) = self.collapse_tabs(point, Bias::Left, app);
-        let mut fold_chars = self.folds_snapshot.chars_at(point, app);
-        if to_next_stop > 0 {
-            fold_chars.next();
-        }
-        Chars {
-            fold_chars,
+        let (point, _) = self.collapse_tabs(point, Bias::Left, app);
+        let fold_chunks = self
+            .folds_snapshot
+            .chunks_at(self.folds_snapshot.to_display_offset(point, app), app);
+        Chunks {
+            fold_chunks,
             column,
-            to_next_stop,
             tab_size: self.tab_size,
+            chunk: "",
         }
     }
 
+    pub fn chars_at<'a>(
+        &'a self,
+        point: DisplayPoint,
+        app: &'a AppContext,
+    ) -> impl Iterator<Item = char> + 'a {
+        self.chunks_at(point, app).flat_map(str::chars)
+    }
+
     fn expand_tabs(&self, mut point: DisplayPoint, ctx: &AppContext) -> DisplayPoint {
         let chars = self
             .folds_snapshot
@@ -238,38 +255,50 @@ impl Anchor {
     }
 }
 
-pub struct Chars<'a> {
-    fold_chars: fold_map::Chars<'a>,
+pub struct Chunks<'a> {
+    fold_chunks: fold_map::Chunks<'a>,
+    chunk: &'a str,
     column: usize,
-    to_next_stop: usize,
     tab_size: usize,
 }
 
-impl<'a> Iterator for Chars<'a> {
-    type Item = char;
+impl<'a> Iterator for Chunks<'a> {
+    type Item = &'a str;
 
     fn next(&mut self) -> Option<Self::Item> {
-        if self.to_next_stop > 0 {
-            self.to_next_stop -= 1;
-            self.column += 1;
-            Some(' ')
-        } else {
-            self.fold_chars.next().map(|c| match c {
+        // Handles a tab width <= 16
+        const SPACES: &'static str = "                ";
+
+        if self.chunk.is_empty() {
+            if let Some(chunk) = self.fold_chunks.next() {
+                self.chunk = chunk;
+            } else {
+                return None;
+            }
+        }
+
+        for (ix, c) in self.chunk.char_indices() {
+            match c {
                 '\t' => {
-                    self.to_next_stop = self.tab_size - self.column % self.tab_size - 1;
-                    self.column += 1;
-                    ' '
-                }
-                '\n' => {
-                    self.column = 0;
-                    c
-                }
-                _ => {
-                    self.column += 1;
-                    c
+                    if ix > 0 {
+                        let (prefix, suffix) = self.chunk.split_at(ix);
+                        self.chunk = suffix;
+                        return Some(prefix);
+                    } else {
+                        self.chunk = &self.chunk[1..];
+                        let len = self.tab_size - self.column % self.tab_size;
+                        self.column += len;
+                        return Some(&SPACES[0..len]);
+                    }
                 }
-            })
+                '\n' => self.column = 0,
+                _ => self.column += 1,
+            }
         }
+
+        let result = Some(self.chunk);
+        self.chunk = "";
+        result
     }
 }
 
@@ -321,7 +350,7 @@ mod tests {
     use crate::test::*;
 
     #[gpui::test]
-    fn test_chars_at(app: &mut gpui::MutableAppContext) {
+    fn test_chunks_at(app: &mut gpui::MutableAppContext) {
         let text = sample_text(6, 6);
         let buffer = app.add_model(|ctx| Buffer::new(0, text, ctx));
         let map = DisplayMap::new(buffer.clone(), 4, app.as_ref());
@@ -340,24 +369,21 @@ mod tests {
             .unwrap();
 
         assert_eq!(
-            map.snapshot(app.as_ref())
-                .chars_at(DisplayPoint::new(1, 0), app.as_ref())
-                .take(10)
-                .collect::<String>(),
+            &map.snapshot(app.as_ref())
+                .chunks_at(DisplayPoint::new(1, 0), app.as_ref())
+                .collect::<String>()[0..10],
             "    b   bb"
         );
         assert_eq!(
-            map.snapshot(app.as_ref())
-                .chars_at(DisplayPoint::new(1, 2), app.as_ref())
-                .take(10)
-                .collect::<String>(),
+            &map.snapshot(app.as_ref())
+                .chunks_at(DisplayPoint::new(1, 2), app.as_ref())
+                .collect::<String>()[0..10],
             "  b   bbbb"
         );
         assert_eq!(
-            map.snapshot(app.as_ref())
-                .chars_at(DisplayPoint::new(1, 6), app.as_ref())
-                .take(13)
-                .collect::<String>(),
+            &map.snapshot(app.as_ref())
+                .chunks_at(DisplayPoint::new(1, 6), app.as_ref())
+                .collect::<String>()[0..13],
             "  bbbbb\nc   c"
         );
     }

zed/src/editor/movement.rs 🔗

@@ -114,7 +114,7 @@ pub fn prev_word_boundary(
             }
 
             prev_c = Some(c);
-            column += 1;
+            column += c.len_utf8() as u32;
         }
         Ok(boundary)
     }
@@ -135,7 +135,7 @@ pub fn next_word_boundary(
             *point.row_mut() += 1;
             *point.column_mut() = 0;
         } else {
-            *point.column_mut() += 1;
+            *point.column_mut() += c.len_utf8() as u32;
         }
         prev_c = Some(c);
     }