Merge pull request #226 from zed-industries/1d-block-map

Max Brunsfeld created

Allow full diagnostic messages to be displayed in the editor

Change summary

Cargo.lock                                 |    2 
crates/buffer/src/anchor.rs                |   98 +
crates/buffer/src/point.rs                 |    9 
crates/buffer/src/rope.rs                  |  106 +
crates/buffer/src/selection.rs             |   32 
crates/editor/Cargo.toml                   |    2 
crates/editor/src/display_map.rs           |  337 +++-
crates/editor/src/display_map/block_map.rs | 1600 ++++++++++++++++++++++++
crates/editor/src/display_map/fold_map.rs  |  132 -
crates/editor/src/display_map/patch.rs     |  511 +++++++
crates/editor/src/display_map/tab_map.rs   |  239 ++-
crates/editor/src/display_map/wrap_map.rs  |  476 ++++--
crates/editor/src/element.rs               |  155 +
crates/editor/src/lib.rs                   |  511 +++++-
crates/editor/src/movement.rs              |   32 
crates/editor/src/test.rs                  |    7 
crates/gpui/src/fonts.rs                   |    2 
crates/gpui/src/platform/mac/event.rs      |   46 
crates/language/src/lib.rs                 |  278 ++-
crates/language/src/proto.rs               |    4 
crates/language/src/tests.rs               |  316 ++++
crates/project/src/worktree.rs             |    4 
crates/rpc/proto/zed.proto                 |    2 
crates/server/src/rpc.rs                   |    6 
crates/sum_tree/src/lib.rs                 |    6 
crates/theme/src/lib.rs                    |   41 
crates/workspace/src/items.rs              |    7 
crates/zed/assets/themes/_base.toml        |   15 
28 files changed, 4,157 insertions(+), 819 deletions(-)

Detailed changes

Cargo.lock šŸ”—

@@ -1622,6 +1622,8 @@ dependencies = [
  "anyhow",
  "buffer",
  "clock",
+ "ctor",
+ "env_logger",
  "gpui",
  "language",
  "lazy_static",

crates/buffer/src/anchor.rs šŸ”—

@@ -194,6 +194,66 @@ impl<T> AnchorRangeMap<T> {
             .iter()
             .map(|(range, value)| (range.start.0..range.end.0, value))
     }
+
+    pub fn min_by_key<'a, C, D, F, K>(
+        &self,
+        content: C,
+        mut extract_key: F,
+    ) -> Option<(Range<D>, &T)>
+    where
+        C: Into<Content<'a>>,
+        D: 'a + TextDimension<'a>,
+        F: FnMut(&T) -> K,
+        K: Ord,
+    {
+        let content = content.into();
+        self.entries
+            .iter()
+            .min_by_key(|(_, value)| extract_key(value))
+            .map(|(range, value)| (self.resolve_range(range, &content), value))
+    }
+
+    pub fn max_by_key<'a, C, D, F, K>(
+        &self,
+        content: C,
+        mut extract_key: F,
+    ) -> Option<(Range<D>, &T)>
+    where
+        C: Into<Content<'a>>,
+        D: 'a + TextDimension<'a>,
+        F: FnMut(&T) -> K,
+        K: Ord,
+    {
+        let content = content.into();
+        self.entries
+            .iter()
+            .max_by_key(|(_, value)| extract_key(value))
+            .map(|(range, value)| (self.resolve_range(range, &content), value))
+    }
+
+    fn resolve_range<'a, D>(
+        &self,
+        range: &Range<(FullOffset, Bias)>,
+        content: &Content<'a>,
+    ) -> Range<D>
+    where
+        D: 'a + TextDimension<'a>,
+    {
+        let (start, start_bias) = range.start;
+        let mut anchor = Anchor {
+            full_offset: start,
+            bias: start_bias,
+            version: self.version.clone(),
+        };
+        let start = content.summary_for_anchor(&anchor);
+
+        let (end, end_bias) = range.end;
+        anchor.full_offset = end;
+        anchor.bias = end_bias;
+        let end = content.summary_for_anchor(&anchor);
+
+        start..end
+    }
 }
 
 impl<T: PartialEq> PartialEq for AnchorRangeMap<T> {
@@ -354,6 +414,38 @@ impl<T: Clone> AnchorRangeMultimap<T> {
             .cursor::<()>()
             .map(|entry| (entry.range.start..entry.range.end, &entry.value))
     }
+
+    pub fn filter<'a, O, F>(
+        &'a self,
+        content: Content<'a>,
+        mut f: F,
+    ) -> impl 'a + Iterator<Item = (usize, Range<O>, &T)>
+    where
+        O: FromAnchor,
+        F: 'a + FnMut(&'a T) -> bool,
+    {
+        let mut endpoint = Anchor {
+            full_offset: FullOffset(0),
+            bias: Bias::Left,
+            version: self.version.clone(),
+        };
+        self.entries
+            .cursor::<()>()
+            .enumerate()
+            .filter_map(move |(ix, entry)| {
+                if f(&entry.value) {
+                    endpoint.full_offset = entry.range.start;
+                    endpoint.bias = self.start_bias;
+                    let start = O::from_anchor(&endpoint, &content);
+                    endpoint.full_offset = entry.range.end;
+                    endpoint.bias = self.end_bias;
+                    let end = O::from_anchor(&endpoint, &content);
+                    Some((ix, start..end, &entry.value))
+                } else {
+                    None
+                }
+            })
+    }
 }
 
 impl<T: Clone> sum_tree::Item for AnchorRangeMultimapEntry<T> {
@@ -435,6 +527,7 @@ impl<'a> sum_tree::SeekTarget<'a, AnchorRangeMultimapSummary, FullOffsetRange> f
 
 pub trait AnchorRangeExt {
     fn cmp<'a>(&self, b: &Range<Anchor>, buffer: impl Into<Content<'a>>) -> Result<Ordering>;
+    fn to_offset<'a>(&self, content: impl Into<Content<'a>>) -> Range<usize>;
 }
 
 impl AnchorRangeExt for Range<Anchor> {
@@ -445,4 +538,9 @@ impl AnchorRangeExt for Range<Anchor> {
             ord @ _ => ord,
         })
     }
+
+    fn to_offset<'a>(&self, content: impl Into<Content<'a>>) -> Range<usize> {
+        let content = content.into();
+        self.start.to_offset(&content)..self.end.to_offset(&content)
+    }
 }

crates/buffer/src/point.rs šŸ”—

@@ -23,6 +23,15 @@ impl Point {
         Point::new(0, 0)
     }
 
+    pub fn from_str(s: &str) -> Self {
+        let mut point = Self::zero();
+        for (row, line) in s.split('\n').enumerate() {
+            point.row = row as u32;
+            point.column = line.len() as u32;
+        }
+        point
+    }
+
     pub fn is_zero(&self) -> bool {
         self.row == 0 && self.column == 0
     }

crates/buffer/src/rope.rs šŸ”—

@@ -3,7 +3,7 @@ use crate::PointUtf16;
 use super::Point;
 use arrayvec::ArrayString;
 use smallvec::SmallVec;
-use std::{cmp, ops::Range, str};
+use std::{cmp, fmt, mem, ops::Range, str};
 use sum_tree::{Bias, Dimension, SumTree};
 
 #[cfg(test)]
@@ -38,6 +38,16 @@ impl Rope {
         self.check_invariants();
     }
 
+    pub fn replace(&mut self, range: Range<usize>, text: &str) {
+        let mut new_rope = Rope::new();
+        let mut cursor = self.cursor(0);
+        new_rope.append(cursor.slice(range.start));
+        cursor.seek_forward(range.end);
+        new_rope.push(text);
+        new_rope.append(cursor.suffix());
+        *self = new_rope;
+    }
+
     pub fn push(&mut self, text: &str) {
         let mut new_chunks = SmallVec::<[_; 16]>::new();
         let mut new_chunk = ArrayString::new();
@@ -79,6 +89,11 @@ impl Rope {
         self.check_invariants();
     }
 
+    pub fn push_front(&mut self, text: &str) {
+        let suffix = mem::replace(self, Rope::from(text));
+        self.append(suffix);
+    }
+
     fn check_invariants(&self) {
         #[cfg(test)]
         {
@@ -139,7 +154,9 @@ impl Rope {
     }
 
     pub fn offset_to_point(&self, offset: usize) -> Point {
-        assert!(offset <= self.summary().bytes);
+        if offset >= self.summary().bytes {
+            return self.summary().lines;
+        }
         let mut cursor = self.chunks.cursor::<(usize, Point)>();
         cursor.seek(&offset, Bias::Left, &());
         let overshoot = offset - cursor.start().0;
@@ -150,7 +167,9 @@ impl Rope {
     }
 
     pub fn offset_to_point_utf16(&self, offset: usize) -> PointUtf16 {
-        assert!(offset <= self.summary().bytes);
+        if offset >= self.summary().bytes {
+            return self.summary().lines_utf16;
+        }
         let mut cursor = self.chunks.cursor::<(usize, PointUtf16)>();
         cursor.seek(&offset, Bias::Left, &());
         let overshoot = offset - cursor.start().0;
@@ -161,7 +180,9 @@ impl Rope {
     }
 
     pub fn point_to_offset(&self, point: Point) -> usize {
-        assert!(point <= self.summary().lines);
+        if point >= self.summary().lines {
+            return self.summary().bytes;
+        }
         let mut cursor = self.chunks.cursor::<(Point, usize)>();
         cursor.seek(&point, Bias::Left, &());
         let overshoot = point - cursor.start().0;
@@ -172,7 +193,9 @@ impl Rope {
     }
 
     pub fn point_utf16_to_offset(&self, point: PointUtf16) -> usize {
-        assert!(point <= self.summary().lines_utf16);
+        if point >= self.summary().lines_utf16 {
+            return self.summary().bytes;
+        }
         let mut cursor = self.chunks.cursor::<(PointUtf16, usize)>();
         cursor.seek(&point, Bias::Left, &());
         let overshoot = point - cursor.start().0;
@@ -226,6 +249,11 @@ impl Rope {
             self.summary().lines_utf16
         }
     }
+
+    pub fn line_len(&self, row: u32) -> u32 {
+        self.clip_point(Point::new(row, u32::MAX), Bias::Left)
+            .column
+    }
 }
 
 impl<'a> From<&'a str> for Rope {
@@ -236,9 +264,12 @@ impl<'a> From<&'a str> for Rope {
     }
 }
 
-impl Into<String> for Rope {
-    fn into(self) -> String {
-        self.chunks().collect()
+impl fmt::Display for Rope {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        for chunk in self.chunks() {
+            write!(f, "{}", chunk)?;
+        }
+        Ok(())
     }
 }
 
@@ -303,7 +334,7 @@ impl<'a> Cursor<'a> {
         if let Some(start_chunk) = self.chunks.item() {
             let start_ix = self.offset - self.chunks.start();
             let end_ix = cmp::min(end_offset, self.chunks.end(&())) - self.chunks.start();
-            summary.add_assign(&D::from_summary(&TextSummary::from(
+            summary.add_assign(&D::from_text_summary(&TextSummary::from(
                 &start_chunk.0[start_ix..end_ix],
             )));
         }
@@ -313,7 +344,9 @@ impl<'a> Cursor<'a> {
             summary.add_assign(&self.chunks.summary(&end_offset, Bias::Right, &()));
             if let Some(end_chunk) = self.chunks.item() {
                 let end_ix = end_offset - self.chunks.start();
-                summary.add_assign(&D::from_summary(&TextSummary::from(&end_chunk.0[..end_ix])));
+                summary.add_assign(&D::from_text_summary(&TextSummary::from(
+                    &end_chunk.0[..end_ix],
+                )));
             }
         }
 
@@ -634,13 +667,16 @@ impl std::ops::AddAssign<Self> for TextSummary {
 }
 
 pub trait TextDimension<'a>: Dimension<'a, TextSummary> {
-    fn from_summary(summary: &TextSummary) -> Self;
+    fn from_text_summary(summary: &TextSummary) -> Self;
     fn add_assign(&mut self, other: &Self);
 }
 
 impl<'a, D1: TextDimension<'a>, D2: TextDimension<'a>> TextDimension<'a> for (D1, D2) {
-    fn from_summary(summary: &TextSummary) -> Self {
-        (D1::from_summary(summary), D2::from_summary(summary))
+    fn from_text_summary(summary: &TextSummary) -> Self {
+        (
+            D1::from_text_summary(summary),
+            D2::from_text_summary(summary),
+        )
     }
 
     fn add_assign(&mut self, other: &Self) {
@@ -650,7 +686,7 @@ impl<'a, D1: TextDimension<'a>, D2: TextDimension<'a>> TextDimension<'a> for (D1
 }
 
 impl<'a> TextDimension<'a> for TextSummary {
-    fn from_summary(summary: &TextSummary) -> Self {
+    fn from_text_summary(summary: &TextSummary) -> Self {
         summary.clone()
     }
 
@@ -666,7 +702,7 @@ impl<'a> sum_tree::Dimension<'a, TextSummary> for usize {
 }
 
 impl<'a> TextDimension<'a> for usize {
-    fn from_summary(summary: &TextSummary) -> Self {
+    fn from_text_summary(summary: &TextSummary) -> Self {
         summary.bytes
     }
 
@@ -682,7 +718,7 @@ impl<'a> sum_tree::Dimension<'a, TextSummary> for Point {
 }
 
 impl<'a> TextDimension<'a> for Point {
-    fn from_summary(summary: &TextSummary) -> Self {
+    fn from_text_summary(summary: &TextSummary) -> Self {
         summary.lines
     }
 
@@ -698,7 +734,7 @@ impl<'a> sum_tree::Dimension<'a, TextSummary> for PointUtf16 {
 }
 
 impl<'a> TextDimension<'a> for PointUtf16 {
-    fn from_summary(summary: &TextSummary) -> Self {
+    fn from_text_summary(summary: &TextSummary) -> Self {
         summary.lines_utf16
     }
 
@@ -731,7 +767,7 @@ mod tests {
     use super::*;
     use crate::random_char_iter::RandomCharIter;
     use rand::prelude::*;
-    use std::env;
+    use std::{cmp::Ordering, env};
     use Bias::{Left, Right};
 
     #[test]
@@ -778,7 +814,7 @@ mod tests {
     }
 
     #[gpui::test(iterations = 100)]
-    fn test_random(mut rng: StdRng) {
+    fn test_random_rope(mut rng: StdRng) {
         let operations = env::var("OPERATIONS")
             .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
             .unwrap_or(10);
@@ -862,6 +898,38 @@ mod tests {
                     TextSummary::from(&expected[start_ix..end_ix])
                 );
             }
+
+            let mut expected_longest_rows = Vec::new();
+            let mut longest_line_len = -1_isize;
+            for (row, line) in expected.split('\n').enumerate() {
+                let row = row as u32;
+                assert_eq!(
+                    actual.line_len(row),
+                    line.len() as u32,
+                    "invalid line len for row {}",
+                    row
+                );
+
+                let line_char_count = line.chars().count() as isize;
+                match line_char_count.cmp(&longest_line_len) {
+                    Ordering::Less => {}
+                    Ordering::Equal => expected_longest_rows.push(row),
+                    Ordering::Greater => {
+                        longest_line_len = line_char_count;
+                        expected_longest_rows.clear();
+                        expected_longest_rows.push(row);
+                    }
+                }
+            }
+
+            let longest_row = actual.summary().longest_row;
+            assert!(
+                expected_longest_rows.contains(&longest_row),
+                "incorrect longest row {}. expected {:?} with length {}",
+                longest_row,
+                expected_longest_rows,
+                longest_line_len,
+            );
         }
     }
 

crates/buffer/src/selection.rs šŸ”—

@@ -116,4 +116,36 @@ impl SelectionSet {
                 goal: state.goal,
             })
     }
+
+    pub fn oldest_selection<'a, D, C>(&'a self, content: C) -> Option<Selection<D>>
+    where
+        D: 'a + TextDimension<'a>,
+        C: 'a + Into<Content<'a>>,
+    {
+        self.selections
+            .min_by_key(content, |selection| selection.id)
+            .map(|(range, state)| Selection {
+                id: state.id,
+                start: range.start,
+                end: range.end,
+                reversed: state.reversed,
+                goal: state.goal,
+            })
+    }
+
+    pub fn newest_selection<'a, D, C>(&'a self, content: C) -> Option<Selection<D>>
+    where
+        D: 'a + TextDimension<'a>,
+        C: 'a + Into<Content<'a>>,
+    {
+        self.selections
+            .max_by_key(content, |selection| selection.id)
+            .map(|(range, state)| Selection {
+                id: state.id,
+                start: range.start,
+                end: range.end,
+                reversed: state.reversed,
+                goal: state.goal,
+            })
+    }
 }

crates/editor/Cargo.toml šŸ”—

@@ -31,6 +31,8 @@ smol = "1.2"
 buffer = { path = "../buffer", features = ["test-support"] }
 language = { path = "../language", features = ["test-support"] }
 gpui = { path = "../gpui", features = ["test-support"] }
+ctor = "0.1"
+env_logger = "0.8"
 rand = "0.8"
 unindent = "0.1.7"
 tree-sitter = "0.19"

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

@@ -1,18 +1,29 @@
+mod block_map;
 mod fold_map;
+mod patch;
 mod tab_map;
 mod wrap_map;
 
+pub use block_map::{BlockDisposition, BlockId, BlockProperties, BufferRows, Chunks};
+use block_map::{BlockMap, BlockPoint};
+use buffer::Rope;
 use fold_map::{FoldMap, ToFoldPoint as _};
-use gpui::{fonts::FontId, Entity, ModelContext, ModelHandle};
+use gpui::{
+    fonts::{FontId, HighlightStyle},
+    AppContext, Entity, ModelContext, ModelHandle,
+};
 use language::{Anchor, Buffer, Point, ToOffset, ToPoint};
-use std::ops::Range;
+use std::{
+    collections::{HashMap, HashSet},
+    ops::Range,
+};
 use sum_tree::Bias;
 use tab_map::TabMap;
+use theme::{BlockStyle, SyntaxTheme};
 use wrap_map::WrapMap;
-pub use wrap_map::{BufferRows, HighlightedChunks};
 
 pub trait ToDisplayPoint {
-    fn to_display_point(&self, map: &DisplayMapSnapshot, bias: Bias) -> DisplayPoint;
+    fn to_display_point(&self, map: &DisplayMapSnapshot) -> DisplayPoint;
 }
 
 pub struct DisplayMap {
@@ -20,6 +31,7 @@ pub struct DisplayMap {
     fold_map: FoldMap,
     tab_map: TabMap,
     wrap_map: ModelHandle<WrapMap>,
+    block_map: BlockMap,
 }
 
 impl Entity for DisplayMap {
@@ -37,28 +49,32 @@ impl DisplayMap {
     ) -> Self {
         let (fold_map, snapshot) = FoldMap::new(buffer.clone(), cx);
         let (tab_map, snapshot) = TabMap::new(snapshot, tab_size);
-        let wrap_map =
-            cx.add_model(|cx| WrapMap::new(snapshot, font_id, font_size, wrap_width, cx));
+        let (wrap_map, snapshot) = WrapMap::new(snapshot, font_id, font_size, wrap_width, cx);
+        let block_map = BlockMap::new(buffer.clone(), snapshot);
         cx.observe(&wrap_map, |_, _, cx| cx.notify()).detach();
         DisplayMap {
             buffer,
             fold_map,
             tab_map,
             wrap_map,
+            block_map,
         }
     }
 
     pub fn snapshot(&self, cx: &mut ModelContext<Self>) -> DisplayMapSnapshot {
         let (folds_snapshot, edits) = self.fold_map.read(cx);
         let (tabs_snapshot, edits) = self.tab_map.sync(folds_snapshot.clone(), edits);
-        let wraps_snapshot = self
+        let (wraps_snapshot, edits) = self
             .wrap_map
             .update(cx, |map, cx| map.sync(tabs_snapshot.clone(), edits, cx));
+        let blocks_snapshot = self.block_map.read(wraps_snapshot.clone(), edits, cx);
+
         DisplayMapSnapshot {
             buffer_snapshot: self.buffer.read(cx).snapshot(),
             folds_snapshot,
             tabs_snapshot,
             wraps_snapshot,
+            blocks_snapshot,
         }
     }
 
@@ -69,12 +85,16 @@ impl DisplayMap {
     ) {
         let (mut fold_map, snapshot, edits) = self.fold_map.write(cx);
         let (snapshot, edits) = self.tab_map.sync(snapshot, edits);
-        self.wrap_map
+        let (snapshot, edits) = self
+            .wrap_map
             .update(cx, |map, cx| map.sync(snapshot, edits, cx));
+        self.block_map.read(snapshot, edits, cx);
         let (snapshot, edits) = fold_map.fold(ranges, cx);
         let (snapshot, edits) = self.tab_map.sync(snapshot, edits);
-        self.wrap_map
+        let (snapshot, edits) = self
+            .wrap_map
             .update(cx, |map, cx| map.sync(snapshot, edits, cx));
+        self.block_map.read(snapshot, edits, cx);
     }
 
     pub fn unfold<T: ToOffset>(
@@ -84,12 +104,52 @@ impl DisplayMap {
     ) {
         let (mut fold_map, snapshot, edits) = self.fold_map.write(cx);
         let (snapshot, edits) = self.tab_map.sync(snapshot, edits);
-        self.wrap_map
+        let (snapshot, edits) = self
+            .wrap_map
             .update(cx, |map, cx| map.sync(snapshot, edits, cx));
+        self.block_map.read(snapshot, edits, cx);
         let (snapshot, edits) = fold_map.unfold(ranges, cx);
         let (snapshot, edits) = self.tab_map.sync(snapshot, edits);
-        self.wrap_map
+        let (snapshot, edits) = self
+            .wrap_map
+            .update(cx, |map, cx| map.sync(snapshot, edits, cx));
+        self.block_map.read(snapshot, edits, cx);
+    }
+
+    pub fn insert_blocks<P, T>(
+        &mut self,
+        blocks: impl IntoIterator<Item = BlockProperties<P, T>>,
+        cx: &mut ModelContext<Self>,
+    ) -> Vec<BlockId>
+    where
+        P: ToOffset + Clone,
+        T: Into<Rope> + Clone,
+    {
+        let (snapshot, edits) = self.fold_map.read(cx);
+        let (snapshot, edits) = self.tab_map.sync(snapshot, edits);
+        let (snapshot, edits) = self
+            .wrap_map
+            .update(cx, |map, cx| map.sync(snapshot, edits, cx));
+        let mut block_map = self.block_map.write(snapshot, edits, cx);
+        block_map.insert(blocks, cx)
+    }
+
+    pub fn restyle_blocks<F1, F2>(&mut self, styles: HashMap<BlockId, (Option<F1>, Option<F2>)>)
+    where
+        F1: 'static + Fn(&AppContext) -> Vec<(usize, HighlightStyle)>,
+        F2: 'static + Fn(&AppContext) -> BlockStyle,
+    {
+        self.block_map.restyle(styles);
+    }
+
+    pub fn remove_blocks(&mut self, ids: HashSet<BlockId>, cx: &mut ModelContext<Self>) {
+        let (snapshot, edits) = self.fold_map.read(cx);
+        let (snapshot, edits) = self.tab_map.sync(snapshot, edits);
+        let (snapshot, edits) = self
+            .wrap_map
             .update(cx, |map, cx| map.sync(snapshot, edits, cx));
+        let mut block_map = self.block_map.write(snapshot, edits, cx);
+        block_map.remove(ids, cx);
     }
 
     pub fn set_font(&self, font_id: FontId, font_size: f32, cx: &mut ModelContext<Self>) {
@@ -113,6 +173,7 @@ pub struct DisplayMapSnapshot {
     folds_snapshot: fold_map::Snapshot,
     tabs_snapshot: tab_map::Snapshot,
     wraps_snapshot: wrap_map::Snapshot,
+    blocks_snapshot: block_map::BlockSnapshot,
 }
 
 impl DisplayMapSnapshot {
@@ -125,8 +186,8 @@ impl DisplayMapSnapshot {
         self.buffer_snapshot.len() == 0
     }
 
-    pub fn buffer_rows(&self, start_row: u32) -> BufferRows {
-        self.wraps_snapshot.buffer_rows(start_row)
+    pub fn buffer_rows<'a>(&'a self, start_row: u32, cx: Option<&'a AppContext>) -> BufferRows<'a> {
+        self.blocks_snapshot.buffer_rows(start_row, cx)
     }
 
     pub fn buffer_row_count(&self) -> u32 {
@@ -136,9 +197,9 @@ impl DisplayMapSnapshot {
     pub fn prev_row_boundary(&self, mut display_point: DisplayPoint) -> (DisplayPoint, Point) {
         loop {
             *display_point.column_mut() = 0;
-            let mut point = display_point.to_buffer_point(self, Bias::Left);
+            let mut point = display_point.to_point(self);
             point.column = 0;
-            let next_display_point = point.to_display_point(self, Bias::Left);
+            let next_display_point = self.point_to_display_point(point, Bias::Left);
             if next_display_point == display_point {
                 return (display_point, point);
             }
@@ -149,9 +210,9 @@ impl DisplayMapSnapshot {
     pub fn next_row_boundary(&self, mut display_point: DisplayPoint) -> (DisplayPoint, Point) {
         loop {
             *display_point.column_mut() = self.line_len(display_point.row());
-            let mut point = display_point.to_buffer_point(self, Bias::Right);
+            let mut point = display_point.to_point(self);
             point.column = self.buffer_snapshot.line_len(point.row);
-            let next_display_point = point.to_display_point(self, Bias::Right);
+            let next_display_point = self.point_to_display_point(point, Bias::Right);
             if next_display_point == display_point {
                 return (display_point, point);
             }
@@ -159,25 +220,46 @@ impl DisplayMapSnapshot {
         }
     }
 
+    fn point_to_display_point(&self, point: Point, bias: Bias) -> DisplayPoint {
+        DisplayPoint(
+            self.blocks_snapshot.to_block_point(
+                self.wraps_snapshot.from_tab_point(
+                    self.tabs_snapshot
+                        .to_tab_point(point.to_fold_point(&self.folds_snapshot, bias)),
+                ),
+            ),
+        )
+    }
+
+    fn display_point_to_point(&self, point: DisplayPoint, bias: Bias) -> Point {
+        let unblocked_point = self.blocks_snapshot.to_wrap_point(point.0);
+        let unwrapped_point = self.wraps_snapshot.to_tab_point(unblocked_point);
+        let unexpanded_point = self.tabs_snapshot.to_fold_point(unwrapped_point, bias).0;
+        unexpanded_point.to_buffer_point(&self.folds_snapshot)
+    }
+
     pub fn max_point(&self) -> DisplayPoint {
-        DisplayPoint(self.wraps_snapshot.max_point())
+        DisplayPoint(self.blocks_snapshot.max_point())
     }
 
-    pub fn chunks_at(&self, display_row: u32) -> wrap_map::Chunks {
-        self.wraps_snapshot.chunks_at(display_row)
+    pub fn text_chunks(&self, display_row: u32) -> impl Iterator<Item = &str> {
+        self.blocks_snapshot
+            .chunks(display_row..self.max_point().row() + 1, None, None)
+            .map(|h| h.text)
     }
 
-    pub fn highlighted_chunks_for_rows(
-        &mut self,
+    pub fn chunks<'a>(
+        &'a self,
         display_rows: Range<u32>,
-    ) -> wrap_map::HighlightedChunks {
-        self.wraps_snapshot
-            .highlighted_chunks_for_rows(display_rows)
+        theme: Option<&'a SyntaxTheme>,
+        cx: &'a AppContext,
+    ) -> block_map::Chunks<'a> {
+        self.blocks_snapshot.chunks(display_rows, theme, Some(cx))
     }
 
     pub fn chars_at<'a>(&'a self, point: DisplayPoint) -> impl Iterator<Item = char> + 'a {
         let mut column = 0;
-        let mut chars = self.chunks_at(point.row()).flat_map(str::chars);
+        let mut chars = self.text_chunks(point.row()).flat_map(str::chars);
         while column < point.column() {
             if let Some(c) = chars.next() {
                 column += c.len_utf8() as u32;
@@ -215,7 +297,7 @@ impl DisplayMapSnapshot {
     }
 
     pub fn clip_point(&self, point: DisplayPoint, bias: Bias) -> DisplayPoint {
-        DisplayPoint(self.wraps_snapshot.clip_point(point.0, bias))
+        DisplayPoint(self.blocks_snapshot.clip_point(point.0, bias))
     }
 
     pub fn folds_in_range<'a, T>(
@@ -233,22 +315,31 @@ impl DisplayMapSnapshot {
     }
 
     pub fn is_line_folded(&self, display_row: u32) -> bool {
-        let wrap_point = DisplayPoint::new(display_row, 0).0;
-        let row = self.wraps_snapshot.to_tab_point(wrap_point).row();
-        self.folds_snapshot.is_line_folded(row)
+        let block_point = BlockPoint(Point::new(display_row, 0));
+        let wrap_point = self.blocks_snapshot.to_wrap_point(block_point);
+        let tab_point = self.wraps_snapshot.to_tab_point(wrap_point);
+        self.folds_snapshot.is_line_folded(tab_point.row())
+    }
+
+    pub fn is_block_line(&self, display_row: u32) -> bool {
+        self.blocks_snapshot.is_block_line(display_row)
     }
 
     pub fn soft_wrap_indent(&self, display_row: u32) -> Option<u32> {
-        self.wraps_snapshot.soft_wrap_indent(display_row)
+        let wrap_row = self
+            .blocks_snapshot
+            .to_wrap_point(BlockPoint::new(display_row, 0))
+            .row();
+        self.wraps_snapshot.soft_wrap_indent(wrap_row)
     }
 
     pub fn text(&self) -> String {
-        self.chunks_at(0).collect()
+        self.text_chunks(0).collect()
     }
 
     pub fn line(&self, display_row: u32) -> String {
         let mut result = String::new();
-        for chunk in self.chunks_at(display_row) {
+        for chunk in self.text_chunks(display_row) {
             if let Some(ix) = chunk.find('\n') {
                 result.push_str(&chunk[0..ix]);
                 break;
@@ -274,30 +365,20 @@ impl DisplayMapSnapshot {
     }
 
     pub fn line_len(&self, row: u32) -> u32 {
-        self.wraps_snapshot.line_len(row)
+        self.blocks_snapshot.line_len(row)
     }
 
     pub fn longest_row(&self) -> u32 {
-        self.wraps_snapshot.longest_row()
-    }
-
-    pub fn anchor_before(&self, point: DisplayPoint, bias: Bias) -> Anchor {
-        self.buffer_snapshot
-            .anchor_before(point.to_buffer_point(self, bias))
-    }
-
-    pub fn anchor_after(&self, point: DisplayPoint, bias: Bias) -> Anchor {
-        self.buffer_snapshot
-            .anchor_after(point.to_buffer_point(self, bias))
+        self.blocks_snapshot.longest_row()
     }
 }
 
 #[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
-pub struct DisplayPoint(wrap_map::WrapPoint);
+pub struct DisplayPoint(BlockPoint);
 
 impl DisplayPoint {
     pub fn new(row: u32, column: u32) -> Self {
-        Self(wrap_map::WrapPoint::new(row, column))
+        Self(BlockPoint(Point::new(row, column)))
     }
 
     pub fn zero() -> Self {
@@ -310,50 +391,52 @@ impl DisplayPoint {
     }
 
     pub fn row(self) -> u32 {
-        self.0.row()
+        self.0.row
     }
 
     pub fn column(self) -> u32 {
-        self.0.column()
+        self.0.column
     }
 
     pub fn row_mut(&mut self) -> &mut u32 {
-        self.0.row_mut()
+        &mut self.0.row
     }
 
     pub fn column_mut(&mut self) -> &mut u32 {
-        self.0.column_mut()
+        &mut self.0.column
     }
 
-    pub fn to_buffer_point(self, map: &DisplayMapSnapshot, bias: Bias) -> Point {
-        let unwrapped_point = map.wraps_snapshot.to_tab_point(self.0);
-        let unexpanded_point = map.tabs_snapshot.to_fold_point(unwrapped_point, bias).0;
-        unexpanded_point.to_buffer_point(&map.folds_snapshot)
+    pub fn to_point(self, map: &DisplayMapSnapshot) -> Point {
+        map.display_point_to_point(self, Bias::Left)
     }
 
-    pub fn to_buffer_offset(self, map: &DisplayMapSnapshot, bias: Bias) -> usize {
-        let unwrapped_point = map.wraps_snapshot.to_tab_point(self.0);
+    pub fn to_offset(self, map: &DisplayMapSnapshot, bias: Bias) -> usize {
+        let unblocked_point = map.blocks_snapshot.to_wrap_point(self.0);
+        let unwrapped_point = map.wraps_snapshot.to_tab_point(unblocked_point);
         let unexpanded_point = map.tabs_snapshot.to_fold_point(unwrapped_point, bias).0;
         unexpanded_point.to_buffer_offset(&map.folds_snapshot)
     }
 }
 
 impl ToDisplayPoint for Point {
-    fn to_display_point(&self, map: &DisplayMapSnapshot, bias: Bias) -> DisplayPoint {
-        let fold_point = self.to_fold_point(&map.folds_snapshot, bias);
-        let tab_point = map.tabs_snapshot.to_tab_point(fold_point);
-        let wrap_point = map.wraps_snapshot.to_wrap_point(tab_point);
-        DisplayPoint(wrap_point)
+    fn to_display_point(&self, map: &DisplayMapSnapshot) -> DisplayPoint {
+        map.point_to_display_point(*self, Bias::Left)
     }
 }
 
 impl ToDisplayPoint for Anchor {
-    fn to_display_point(&self, map: &DisplayMapSnapshot, bias: Bias) -> DisplayPoint {
-        self.to_point(&map.buffer_snapshot)
-            .to_display_point(map, bias)
+    fn to_display_point(&self, map: &DisplayMapSnapshot) -> DisplayPoint {
+        self.to_point(&map.buffer_snapshot).to_display_point(map)
     }
 }
 
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum DisplayRow {
+    Buffer(u32),
+    Block(BlockId, Option<BlockStyle>),
+    Wrap,
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -472,28 +555,28 @@ mod tests {
 
                 assert_eq!(
                     prev_display_bound,
-                    prev_buffer_bound.to_display_point(&snapshot, Left),
+                    prev_buffer_bound.to_display_point(&snapshot),
                     "row boundary before {:?}. reported buffer row boundary: {:?}",
                     point,
                     prev_buffer_bound
                 );
                 assert_eq!(
                     next_display_bound,
-                    next_buffer_bound.to_display_point(&snapshot, Right),
+                    next_buffer_bound.to_display_point(&snapshot),
                     "display row boundary after {:?}. reported buffer row boundary: {:?}",
                     point,
                     next_buffer_bound
                 );
                 assert_eq!(
                     prev_buffer_bound,
-                    prev_display_bound.to_buffer_point(&snapshot, Left),
+                    prev_display_bound.to_point(&snapshot),
                     "row boundary before {:?}. reported display row boundary: {:?}",
                     point,
                     prev_display_bound
                 );
                 assert_eq!(
                     next_buffer_bound,
-                    next_display_bound.to_buffer_point(&snapshot, Right),
+                    next_display_bound.to_point(&snapshot),
                     "row boundary after {:?}. reported display row boundary: {:?}",
                     point,
                     next_display_bound
@@ -559,7 +642,7 @@ mod tests {
 
         let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
         assert_eq!(
-            snapshot.chunks_at(0).collect::<String>(),
+            snapshot.text_chunks(0).collect::<String>(),
             "one two \nthree four \nfive\nsix seven \neight"
         );
         assert_eq!(
@@ -608,7 +691,7 @@ mod tests {
 
         let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
         assert_eq!(
-            snapshot.chunks_at(1).collect::<String>(),
+            snapshot.text_chunks(1).collect::<String>(),
             "three four \nfive\nsix and \nseven eight"
         );
 
@@ -617,13 +700,13 @@ mod tests {
 
         let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
         assert_eq!(
-            snapshot.chunks_at(1).collect::<String>(),
+            snapshot.text_chunks(1).collect::<String>(),
             "three \nfour five\nsix and \nseven \neight"
         )
     }
 
     #[gpui::test]
-    fn test_chunks_at(cx: &mut gpui::MutableAppContext) {
+    fn test_text_chunks(cx: &mut gpui::MutableAppContext) {
         let text = sample_text(6, 6);
         let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
         let tab_size = 4;
@@ -650,7 +733,7 @@ mod tests {
 
         assert_eq!(
             map.update(cx, |map, cx| map.snapshot(cx))
-                .chunks_at(1)
+                .text_chunks(1)
                 .collect::<String>()
                 .lines()
                 .next(),
@@ -658,7 +741,7 @@ mod tests {
         );
         assert_eq!(
             map.update(cx, |map, cx| map.snapshot(cx))
-                .chunks_at(2)
+                .text_chunks(2)
                 .collect::<String>()
                 .lines()
                 .next(),
@@ -667,7 +750,7 @@ mod tests {
     }
 
     #[gpui::test]
-    async fn test_highlighted_chunks_at(mut cx: gpui::TestAppContext) {
+    async fn test_chunks(mut cx: gpui::TestAppContext) {
         use unindent::Unindent as _;
 
         let text = r#"
@@ -679,8 +762,8 @@ mod tests {
         .unindent();
 
         let theme = SyntaxTheme::new(vec![
-            ("mod.body".to_string(), Color::from_u32(0xff0000ff).into()),
-            ("fn.name".to_string(), Color::from_u32(0x00ff00ff).into()),
+            ("mod.body".to_string(), Color::red().into()),
+            ("fn.name".to_string(), Color::blue().into()),
         ]);
         let lang = Arc::new(
             Language::new(
@@ -716,22 +799,22 @@ mod tests {
         let map =
             cx.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, cx));
         assert_eq!(
-            cx.update(|cx| highlighted_chunks(0..5, &map, &theme, cx)),
+            cx.update(|cx| chunks(0..5, &map, &theme, cx)),
             vec![
                 ("fn ".to_string(), None),
-                ("outer".to_string(), Some("fn.name")),
+                ("outer".to_string(), Some(Color::blue())),
                 ("() {}\n\nmod module ".to_string(), None),
-                ("{\n    fn ".to_string(), Some("mod.body")),
-                ("inner".to_string(), Some("fn.name")),
-                ("() {}\n}".to_string(), Some("mod.body")),
+                ("{\n    fn ".to_string(), Some(Color::red())),
+                ("inner".to_string(), Some(Color::blue())),
+                ("() {}\n}".to_string(), Some(Color::red())),
             ]
         );
         assert_eq!(
-            cx.update(|cx| highlighted_chunks(3..5, &map, &theme, cx)),
+            cx.update(|cx| chunks(3..5, &map, &theme, cx)),
             vec![
-                ("    fn ".to_string(), Some("mod.body")),
-                ("inner".to_string(), Some("fn.name")),
-                ("() {}\n}".to_string(), Some("mod.body")),
+                ("    fn ".to_string(), Some(Color::red())),
+                ("inner".to_string(), Some(Color::blue())),
+                ("() {}\n}".to_string(), Some(Color::red())),
             ]
         );
 
@@ -739,20 +822,20 @@ mod tests {
             map.fold(vec![Point::new(0, 6)..Point::new(3, 2)], cx)
         });
         assert_eq!(
-            cx.update(|cx| highlighted_chunks(0..2, &map, &theme, cx)),
+            cx.update(|cx| chunks(0..2, &map, &theme, cx)),
             vec![
                 ("fn ".to_string(), None),
-                ("out".to_string(), Some("fn.name")),
+                ("out".to_string(), Some(Color::blue())),
                 ("…".to_string(), None),
-                ("  fn ".to_string(), Some("mod.body")),
-                ("inner".to_string(), Some("fn.name")),
-                ("() {}\n}".to_string(), Some("mod.body")),
+                ("  fn ".to_string(), Some(Color::red())),
+                ("inner".to_string(), Some(Color::blue())),
+                ("() {}\n}".to_string(), Some(Color::red())),
             ]
         );
     }
 
     #[gpui::test]
-    async fn test_highlighted_chunks_with_soft_wrapping(mut cx: gpui::TestAppContext) {
+    async fn test_chunks_with_soft_wrapping(mut cx: gpui::TestAppContext) {
         use unindent::Unindent as _;
 
         cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
@@ -766,8 +849,8 @@ mod tests {
         .unindent();
 
         let theme = SyntaxTheme::new(vec![
-            ("mod.body".to_string(), Color::from_u32(0xff0000ff).into()),
-            ("fn.name".to_string(), Color::from_u32(0x00ff00ff).into()),
+            ("mod.body".to_string(), Color::red().into()),
+            ("fn.name".to_string(), Color::blue().into()),
         ]);
         let lang = Arc::new(
             Language::new(
@@ -804,15 +887,15 @@ mod tests {
         let map = cx
             .add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, Some(40.0), cx));
         assert_eq!(
-            cx.update(|cx| highlighted_chunks(0..5, &map, &theme, cx)),
+            cx.update(|cx| chunks(0..5, &map, &theme, cx)),
             [
                 ("fn \n".to_string(), None),
-                ("oute\nr".to_string(), Some("fn.name")),
+                ("oute\nr".to_string(), Some(Color::blue())),
                 ("() \n{}\n\n".to_string(), None),
             ]
         );
         assert_eq!(
-            cx.update(|cx| highlighted_chunks(3..5, &map, &theme, cx)),
+            cx.update(|cx| chunks(3..5, &map, &theme, cx)),
             [("{}\n\n".to_string(), None)]
         );
 
@@ -820,12 +903,12 @@ mod tests {
             map.fold(vec![Point::new(0, 6)..Point::new(3, 2)], cx)
         });
         assert_eq!(
-            cx.update(|cx| highlighted_chunks(1..4, &map, &theme, cx)),
+            cx.update(|cx| chunks(1..4, &map, &theme, cx)),
             [
-                ("out".to_string(), Some("fn.name")),
+                ("out".to_string(), Some(Color::blue())),
                 ("…\n".to_string(), None),
-                ("  \nfn ".to_string(), Some("mod.body")),
-                ("i\n".to_string(), Some("fn.name"))
+                ("  \nfn ".to_string(), Some(Color::red())),
+                ("i\n".to_string(), Some(Color::blue()))
             ]
         );
     }
@@ -895,42 +978,34 @@ 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>(),
+            map.text_chunks(0).collect::<String>(),
             "āœ…       α\nβ   \nšŸ€Ī²      γ"
         );
-        assert_eq!(map.chunks_at(1).collect::<String>(), "β   \nšŸ€Ī²      γ");
-        assert_eq!(map.chunks_at(2).collect::<String>(), "šŸ€Ī²      γ");
+        assert_eq!(map.text_chunks(1).collect::<String>(), "β   \nšŸ€Ī²      γ");
+        assert_eq!(map.text_chunks(2).collect::<String>(), "šŸ€Ī²      γ");
 
         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, Left), display_point);
-        assert_eq!(display_point.to_buffer_point(&map, Left), point,);
+        assert_eq!(point.to_display_point(&map), display_point);
+        assert_eq!(display_point.to_point(&map), point);
 
         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, Left), display_point);
-        assert_eq!(display_point.to_buffer_point(&map, Left), point,);
+        assert_eq!(point.to_display_point(&map), display_point);
+        assert_eq!(display_point.to_point(&map), point,);
 
         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, Left), display_point);
-        assert_eq!(display_point.to_buffer_point(&map, Left), point,);
+        assert_eq!(point.to_display_point(&map), display_point);
+        assert_eq!(display_point.to_point(&map), point,);
 
         // Display points inside of expanded tabs
         assert_eq!(
-            DisplayPoint::new(0, "āœ…      ".len() as u32).to_buffer_point(&map, Right),
-            Point::new(0, "āœ…\t\t".len() as u32),
-        );
-        assert_eq!(
-            DisplayPoint::new(0, "āœ…      ".len() as u32).to_buffer_point(&map, Left),
-            Point::new(0, "āœ…\t".len() as u32),
-        );
-        assert_eq!(
-            DisplayPoint::new(0, "āœ… ".len() as u32).to_buffer_point(&map, Right),
+            DisplayPoint::new(0, "āœ…      ".len() as u32).to_point(&map),
             Point::new(0, "āœ…\t".len() as u32),
         );
         assert_eq!(
-            DisplayPoint::new(0, "āœ… ".len() as u32).to_buffer_point(&map, Left),
+            DisplayPoint::new(0, "āœ… ".len() as u32).to_point(&map),
             Point::new(0, "āœ…".len() as u32),
         );
 
@@ -964,24 +1039,24 @@ mod tests {
         )
     }
 
-    fn highlighted_chunks<'a>(
+    fn chunks<'a>(
         rows: Range<u32>,
         map: &ModelHandle<DisplayMap>,
         theme: &'a SyntaxTheme,
         cx: &mut MutableAppContext,
-    ) -> Vec<(String, Option<&'a str>)> {
-        let mut snapshot = map.update(cx, |map, cx| map.snapshot(cx));
-        let mut chunks: Vec<(String, Option<&str>)> = Vec::new();
-        for chunk in snapshot.highlighted_chunks_for_rows(rows) {
-            let style_name = chunk.highlight_id.name(theme);
-            if let Some((last_chunk, last_style_name)) = chunks.last_mut() {
-                if style_name == *last_style_name {
+    ) -> Vec<(String, Option<Color>)> {
+        let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
+        let mut chunks: Vec<(String, Option<Color>)> = Vec::new();
+        for chunk in snapshot.chunks(rows, Some(theme), cx) {
+            let color = chunk.highlight_style.map(|s| s.color);
+            if let Some((last_chunk, last_color)) = chunks.last_mut() {
+                if color == *last_color {
                     last_chunk.push_str(chunk.text);
                 } else {
-                    chunks.push((chunk.text.to_string(), style_name));
+                    chunks.push((chunk.text.to_string(), color));
                 }
             } else {
-                chunks.push((chunk.text.to_string(), style_name));
+                chunks.push((chunk.text.to_string(), color));
             }
         }
         chunks

crates/editor/src/display_map/block_map.rs šŸ”—

@@ -0,0 +1,1600 @@
+use super::{
+    wrap_map::{self, Edit as WrapEdit, Snapshot as WrapSnapshot, WrapPoint},
+    BlockStyle, DisplayRow,
+};
+use buffer::{rope, Anchor, Bias, Edit, Point, Rope, ToOffset, ToPoint as _};
+use gpui::{fonts::HighlightStyle, AppContext, ModelHandle};
+use language::{Buffer, Chunk};
+use parking_lot::Mutex;
+use std::{
+    cmp::{self, Ordering},
+    collections::{HashMap, HashSet},
+    fmt::Debug,
+    iter,
+    ops::{Deref, Range},
+    sync::{
+        atomic::{AtomicUsize, Ordering::SeqCst},
+        Arc,
+    },
+    vec,
+};
+use sum_tree::SumTree;
+use theme::SyntaxTheme;
+
+pub struct BlockMap {
+    buffer: ModelHandle<Buffer>,
+    next_block_id: AtomicUsize,
+    wrap_snapshot: Mutex<WrapSnapshot>,
+    blocks: Vec<Arc<Block>>,
+    transforms: Mutex<SumTree<Transform>>,
+}
+
+pub struct BlockMapWriter<'a>(&'a mut BlockMap);
+
+pub struct BlockSnapshot {
+    wrap_snapshot: WrapSnapshot,
+    transforms: SumTree<Transform>,
+}
+
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct BlockId(usize);
+
+#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
+pub struct BlockPoint(pub super::Point);
+
+#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
+struct BlockRow(u32);
+
+#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
+struct WrapRow(u32);
+
+pub struct Block {
+    id: BlockId,
+    position: Anchor,
+    text: Rope,
+    build_runs: Mutex<Option<Arc<dyn Fn(&AppContext) -> Vec<(usize, HighlightStyle)>>>>,
+    build_style: Mutex<Option<Arc<dyn Fn(&AppContext) -> BlockStyle>>>,
+    disposition: BlockDisposition,
+}
+
+#[derive(Clone)]
+pub struct BlockProperties<P, T>
+where
+    P: Clone,
+    T: Clone,
+{
+    pub position: P,
+    pub text: T,
+    pub build_runs: Option<Arc<dyn Fn(&AppContext) -> Vec<(usize, HighlightStyle)>>>,
+    pub build_style: Option<Arc<dyn Fn(&AppContext) -> BlockStyle>>,
+    pub disposition: BlockDisposition,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
+pub enum BlockDisposition {
+    Above,
+    Below,
+}
+
+#[derive(Clone, Debug)]
+struct Transform {
+    summary: TransformSummary,
+    block: Option<AlignedBlock>,
+}
+
+#[derive(Clone, Debug)]
+struct AlignedBlock {
+    block: Arc<Block>,
+    column: u32,
+}
+
+#[derive(Clone, Debug, Default)]
+struct TransformSummary {
+    input_rows: u32,
+    output_rows: u32,
+    longest_row_in_block: u32,
+    longest_row_in_block_chars: u32,
+}
+
+pub struct Chunks<'a> {
+    transforms: sum_tree::Cursor<'a, Transform, (BlockRow, WrapRow)>,
+    input_chunks: wrap_map::Chunks<'a>,
+    input_chunk: Chunk<'a>,
+    block_chunks: Option<BlockChunks<'a>>,
+    output_row: u32,
+    max_output_row: u32,
+    cx: Option<&'a AppContext>,
+}
+
+struct BlockChunks<'a> {
+    chunks: rope::Chunks<'a>,
+    runs: iter::Peekable<vec::IntoIter<(usize, HighlightStyle)>>,
+    chunk: Option<&'a str>,
+    remaining_padding: u32,
+    padding_column: u32,
+    run_start: usize,
+    offset: usize,
+}
+
+pub struct BufferRows<'a> {
+    transforms: sum_tree::Cursor<'a, Transform, (BlockRow, WrapRow)>,
+    input_buffer_rows: wrap_map::BufferRows<'a>,
+    output_row: u32,
+    cx: Option<&'a AppContext>,
+    started: bool,
+}
+
+impl BlockMap {
+    pub fn new(buffer: ModelHandle<Buffer>, wrap_snapshot: WrapSnapshot) -> Self {
+        Self {
+            buffer,
+            next_block_id: AtomicUsize::new(0),
+            blocks: Vec::new(),
+            transforms: Mutex::new(SumTree::from_item(
+                Transform::isomorphic(wrap_snapshot.text_summary().lines.row + 1),
+                &(),
+            )),
+            wrap_snapshot: Mutex::new(wrap_snapshot),
+        }
+    }
+
+    pub fn read(
+        &self,
+        wrap_snapshot: WrapSnapshot,
+        edits: Vec<WrapEdit>,
+        cx: &AppContext,
+    ) -> BlockSnapshot {
+        self.sync(&wrap_snapshot, edits, cx);
+        *self.wrap_snapshot.lock() = wrap_snapshot.clone();
+        BlockSnapshot {
+            wrap_snapshot,
+            transforms: self.transforms.lock().clone(),
+        }
+    }
+
+    pub fn write(
+        &mut self,
+        wrap_snapshot: WrapSnapshot,
+        edits: Vec<WrapEdit>,
+        cx: &AppContext,
+    ) -> BlockMapWriter {
+        self.sync(&wrap_snapshot, edits, cx);
+        *self.wrap_snapshot.lock() = wrap_snapshot;
+        BlockMapWriter(self)
+    }
+
+    fn sync(&self, wrap_snapshot: &WrapSnapshot, edits: Vec<WrapEdit>, cx: &AppContext) {
+        if edits.is_empty() {
+            return;
+        }
+
+        let buffer = self.buffer.read(cx);
+        let mut transforms = self.transforms.lock();
+        let mut new_transforms = SumTree::new();
+        let old_row_count = transforms.summary().input_rows;
+        let new_row_count = wrap_snapshot.max_point().row() + 1;
+        let mut cursor = transforms.cursor::<WrapRow>();
+        let mut last_block_ix = 0;
+        let mut blocks_in_edit = Vec::new();
+        let mut edits = edits.into_iter().peekable();
+
+        while let Some(edit) = edits.next() {
+            // Preserve any old transforms that precede this edit.
+            let old_start = WrapRow(edit.old.start);
+            let new_start = WrapRow(edit.new.start);
+            new_transforms.push_tree(cursor.slice(&old_start, Bias::Left, &()), &());
+            if let Some(transform) = cursor.item() {
+                if transform.is_isomorphic() && old_start == cursor.end(&()) {
+                    new_transforms.push(transform.clone(), &());
+                    cursor.next(&());
+                    while let Some(transform) = cursor.item() {
+                        if transform
+                            .block
+                            .as_ref()
+                            .map_or(false, |b| b.disposition.is_below())
+                        {
+                            new_transforms.push(transform.clone(), &());
+                            cursor.next(&());
+                        } else {
+                            break;
+                        }
+                    }
+                }
+            }
+
+            // Preserve any portion of an old transform that precedes this edit.
+            let extent_before_edit = old_start.0 - cursor.start().0;
+            push_isomorphic(&mut new_transforms, extent_before_edit);
+
+            // Skip over any old transforms that intersect this edit.
+            let mut old_end = WrapRow(edit.old.end);
+            let mut new_end = WrapRow(edit.new.end);
+            cursor.seek(&old_end, Bias::Left, &());
+            cursor.next(&());
+            if old_end == *cursor.start() {
+                while let Some(transform) = cursor.item() {
+                    if transform
+                        .block
+                        .as_ref()
+                        .map_or(false, |b| b.disposition.is_below())
+                    {
+                        cursor.next(&());
+                    } else {
+                        break;
+                    }
+                }
+            }
+
+            // Combine this edit with any subsequent edits that intersect the same transform.
+            while let Some(next_edit) = edits.peek() {
+                if next_edit.old.start <= cursor.start().0 {
+                    old_end = WrapRow(next_edit.old.end);
+                    new_end = WrapRow(next_edit.new.end);
+                    cursor.seek(&old_end, Bias::Left, &());
+                    cursor.next(&());
+                    if old_end == *cursor.start() {
+                        while let Some(transform) = cursor.item() {
+                            if transform
+                                .block
+                                .as_ref()
+                                .map_or(false, |b| b.disposition.is_below())
+                            {
+                                cursor.next(&());
+                            } else {
+                                break;
+                            }
+                        }
+                    }
+                    edits.next();
+                } else {
+                    break;
+                }
+            }
+
+            // Find the blocks within this edited region.
+            let new_start = wrap_snapshot.to_point(WrapPoint::new(new_start.0, 0), Bias::Left);
+            let start_anchor = buffer.anchor_before(new_start);
+            let start_block_ix = match self.blocks[last_block_ix..].binary_search_by(|probe| {
+                probe
+                    .position
+                    .cmp(&start_anchor, buffer)
+                    .unwrap()
+                    .then(Ordering::Greater)
+            }) {
+                Ok(ix) | Err(ix) => last_block_ix + ix,
+            };
+            let end_block_ix = if new_end.0 > wrap_snapshot.max_point().row() {
+                self.blocks.len()
+            } else {
+                let new_end = wrap_snapshot.to_point(WrapPoint::new(new_end.0, 0), Bias::Left);
+                let end_anchor = buffer.anchor_before(new_end);
+                match self.blocks[start_block_ix..].binary_search_by(|probe| {
+                    probe
+                        .position
+                        .cmp(&end_anchor, buffer)
+                        .unwrap()
+                        .then(Ordering::Greater)
+                }) {
+                    Ok(ix) | Err(ix) => start_block_ix + ix,
+                }
+            };
+            last_block_ix = end_block_ix;
+            blocks_in_edit.clear();
+            blocks_in_edit.extend(
+                self.blocks[start_block_ix..end_block_ix]
+                    .iter()
+                    .map(|block| {
+                        let mut position = block.position.to_point(buffer);
+                        let column = wrap_snapshot.from_point(position, Bias::Left).column();
+                        match block.disposition {
+                            BlockDisposition::Above => position.column = 0,
+                            BlockDisposition::Below => {
+                                position.column = buffer.line_len(position.row)
+                            }
+                        }
+                        let position = wrap_snapshot.from_point(position, Bias::Left);
+                        (position.row(), column, block)
+                    }),
+            );
+            blocks_in_edit
+                .sort_unstable_by_key(|(row, _, block)| (*row, block.disposition, block.id));
+
+            // For each of these blocks, insert a new isomorphic transform preceding the block,
+            // and then insert the block itself.
+            for (block_row, column, block) in blocks_in_edit.iter().copied() {
+                let insertion_row = match block.disposition {
+                    BlockDisposition::Above => block_row,
+                    BlockDisposition::Below => block_row + 1,
+                };
+                let extent_before_block = insertion_row - new_transforms.summary().input_rows;
+                push_isomorphic(&mut new_transforms, extent_before_block);
+                new_transforms.push(Transform::block(block.clone(), column), &());
+            }
+
+            old_end = WrapRow(old_end.0.min(old_row_count));
+            new_end = WrapRow(new_end.0.min(new_row_count));
+
+            // Insert an isomorphic transform after the final block.
+            let extent_after_last_block = new_end.0 - new_transforms.summary().input_rows;
+            push_isomorphic(&mut new_transforms, extent_after_last_block);
+
+            // Preserve any portion of the old transform after this edit.
+            let extent_after_edit = cursor.start().0 - old_end.0;
+            push_isomorphic(&mut new_transforms, extent_after_edit);
+        }
+
+        new_transforms.push_tree(cursor.suffix(&()), &());
+        debug_assert_eq!(
+            new_transforms.summary().input_rows,
+            wrap_snapshot.max_point().row() + 1
+        );
+
+        drop(cursor);
+        *transforms = new_transforms;
+    }
+
+    pub fn restyle<F1, F2>(&mut self, mut styles: HashMap<BlockId, (Option<F1>, Option<F2>)>)
+    where
+        F1: 'static + Fn(&AppContext) -> Vec<(usize, HighlightStyle)>,
+        F2: 'static + Fn(&AppContext) -> BlockStyle,
+    {
+        for block in &self.blocks {
+            if let Some((build_runs, build_style)) = styles.remove(&block.id) {
+                *block.build_runs.lock() = build_runs.map(|build_runs| {
+                    Arc::new(build_runs) as Arc<dyn Fn(&AppContext) -> Vec<(usize, HighlightStyle)>>
+                });
+                *block.build_style.lock() = build_style.map(|build_style| {
+                    Arc::new(build_style) as Arc<dyn Fn(&AppContext) -> BlockStyle>
+                });
+            }
+        }
+    }
+}
+
+fn push_isomorphic(tree: &mut SumTree<Transform>, rows: u32) {
+    if rows == 0 {
+        return;
+    }
+
+    let mut extent = Some(rows);
+    tree.update_last(
+        |last_transform| {
+            if last_transform.is_isomorphic() {
+                let extent = extent.take().unwrap();
+                last_transform.summary.input_rows += extent;
+                last_transform.summary.output_rows += extent;
+            }
+        },
+        &(),
+    );
+    if let Some(extent) = extent {
+        tree.push(Transform::isomorphic(extent), &());
+    }
+}
+
+impl BlockPoint {
+    pub fn new(row: u32, column: u32) -> Self {
+        Self(Point::new(row, column))
+    }
+}
+
+impl Deref for BlockPoint {
+    type Target = Point;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl std::ops::DerefMut for BlockPoint {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.0
+    }
+}
+
+impl<'a> BlockMapWriter<'a> {
+    pub fn insert<P, T>(
+        &mut self,
+        blocks: impl IntoIterator<Item = BlockProperties<P, T>>,
+        cx: &AppContext,
+    ) -> Vec<BlockId>
+    where
+        P: ToOffset + Clone,
+        T: Into<Rope> + Clone,
+    {
+        let buffer = self.0.buffer.read(cx);
+        let mut ids = Vec::new();
+        let mut edits = Vec::<Edit<u32>>::new();
+        let wrap_snapshot = &*self.0.wrap_snapshot.lock();
+
+        for block in blocks {
+            let id = BlockId(self.0.next_block_id.fetch_add(1, SeqCst));
+            ids.push(id);
+
+            let position = buffer.anchor_after(block.position);
+            let point = position.to_point(buffer);
+            let start_row = wrap_snapshot
+                .from_point(Point::new(point.row, 0), Bias::Left)
+                .row();
+            let end_row = if point.row == buffer.max_point().row {
+                wrap_snapshot.max_point().row() + 1
+            } else {
+                wrap_snapshot
+                    .from_point(Point::new(point.row + 1, 0), Bias::Left)
+                    .row()
+            };
+
+            let block_ix = match self
+                .0
+                .blocks
+                .binary_search_by(|probe| probe.position.cmp(&position, buffer).unwrap())
+            {
+                Ok(ix) | Err(ix) => ix,
+            };
+            self.0.blocks.insert(
+                block_ix,
+                Arc::new(Block {
+                    id,
+                    position,
+                    text: block.text.into(),
+                    build_runs: Mutex::new(block.build_runs),
+                    build_style: Mutex::new(block.build_style),
+                    disposition: block.disposition,
+                }),
+            );
+
+            if let Err(edit_ix) = edits.binary_search_by_key(&start_row, |edit| edit.old.start) {
+                edits.insert(
+                    edit_ix,
+                    Edit {
+                        old: start_row..end_row,
+                        new: start_row..end_row,
+                    },
+                );
+            }
+        }
+
+        self.0.sync(wrap_snapshot, edits, cx);
+        ids
+    }
+
+    pub fn remove(&mut self, block_ids: HashSet<BlockId>, cx: &AppContext) {
+        let buffer = self.0.buffer.read(cx);
+        let wrap_snapshot = &*self.0.wrap_snapshot.lock();
+        let mut edits = Vec::new();
+        let mut last_block_buffer_row = None;
+        self.0.blocks.retain(|block| {
+            if block_ids.contains(&block.id) {
+                let buffer_row = block.position.to_point(buffer).row;
+                if last_block_buffer_row != Some(buffer_row) {
+                    last_block_buffer_row = Some(buffer_row);
+                    let start_row = wrap_snapshot
+                        .from_point(Point::new(buffer_row, 0), Bias::Left)
+                        .row();
+                    let end_row = wrap_snapshot
+                        .from_point(
+                            Point::new(buffer_row, buffer.line_len(buffer_row)),
+                            Bias::Left,
+                        )
+                        .row()
+                        + 1;
+                    edits.push(Edit {
+                        old: start_row..end_row,
+                        new: start_row..end_row,
+                    })
+                }
+                false
+            } else {
+                true
+            }
+        });
+        self.0.sync(wrap_snapshot, edits, cx);
+    }
+}
+
+impl BlockSnapshot {
+    #[cfg(test)]
+    fn text(&mut self) -> String {
+        self.chunks(0..self.transforms.summary().output_rows, None, None)
+            .map(|chunk| chunk.text)
+            .collect()
+    }
+
+    pub fn chunks<'a>(
+        &'a self,
+        rows: Range<u32>,
+        theme: Option<&'a SyntaxTheme>,
+        cx: Option<&'a AppContext>,
+    ) -> Chunks<'a> {
+        let max_output_row = cmp::min(rows.end, self.transforms.summary().output_rows);
+        let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>();
+        let input_end = {
+            cursor.seek(&BlockRow(rows.end), Bias::Right, &());
+            let overshoot = if cursor
+                .item()
+                .map_or(false, |transform| transform.is_isomorphic())
+            {
+                rows.end - cursor.start().0 .0
+            } else {
+                0
+            };
+            cursor.start().1 .0 + overshoot
+        };
+        let input_start = {
+            cursor.seek(&BlockRow(rows.start), Bias::Right, &());
+            let overshoot = if cursor
+                .item()
+                .map_or(false, |transform| transform.is_isomorphic())
+            {
+                rows.start - cursor.start().0 .0
+            } else {
+                0
+            };
+            cursor.start().1 .0 + overshoot
+        };
+        Chunks {
+            input_chunks: self.wrap_snapshot.chunks(input_start..input_end, theme),
+            input_chunk: Default::default(),
+            block_chunks: None,
+            transforms: cursor,
+            output_row: rows.start,
+            max_output_row,
+            cx,
+        }
+    }
+
+    pub fn buffer_rows<'a>(&'a self, start_row: u32, cx: Option<&'a AppContext>) -> BufferRows<'a> {
+        let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>();
+        cursor.seek(&BlockRow(start_row), Bias::Right, &());
+        let (output_start, input_start) = cursor.start();
+        let overshoot = if cursor.item().map_or(false, |t| t.is_isomorphic()) {
+            start_row - output_start.0
+        } else {
+            0
+        };
+        let input_start_row = input_start.0 + overshoot;
+        BufferRows {
+            cx,
+            transforms: cursor,
+            input_buffer_rows: self.wrap_snapshot.buffer_rows(input_start_row),
+            output_row: start_row,
+            started: false,
+        }
+    }
+
+    pub fn max_point(&self) -> BlockPoint {
+        let row = self.transforms.summary().output_rows - 1;
+        BlockPoint::new(row, self.line_len(row))
+    }
+
+    pub fn longest_row(&self) -> u32 {
+        let input_row = self.wrap_snapshot.longest_row();
+        let input_row_chars = self.wrap_snapshot.line_char_count(input_row);
+        let TransformSummary {
+            longest_row_in_block: block_row,
+            longest_row_in_block_chars: block_row_chars,
+            ..
+        } = &self.transforms.summary();
+
+        if *block_row_chars > input_row_chars {
+            *block_row
+        } else {
+            self.to_block_point(WrapPoint::new(input_row, 0)).row
+        }
+    }
+
+    pub fn line_len(&self, row: u32) -> u32 {
+        let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>();
+        cursor.seek(&BlockRow(row), Bias::Right, &());
+        if let Some(transform) = cursor.item() {
+            let (output_start, input_start) = cursor.start();
+            let overshoot = row - output_start.0;
+            if let Some(block) = &transform.block {
+                let mut len = block.text.line_len(overshoot);
+                if len > 0 {
+                    len += block.column;
+                }
+                len
+            } else {
+                self.wrap_snapshot.line_len(input_start.0 + overshoot)
+            }
+        } else {
+            panic!("row out of range");
+        }
+    }
+
+    pub fn is_block_line(&self, row: u32) -> bool {
+        let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>();
+        cursor.seek(&BlockRow(row), Bias::Right, &());
+        cursor.item().map_or(false, |t| t.block.is_some())
+    }
+
+    pub fn clip_point(&self, point: BlockPoint, bias: Bias) -> BlockPoint {
+        let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>();
+        cursor.seek(&BlockRow(point.row), Bias::Right, &());
+
+        let max_input_row = WrapRow(self.transforms.summary().input_rows);
+        let search_left =
+            (bias == Bias::Left && cursor.start().1 .0 > 0) || cursor.end(&()).1 == max_input_row;
+
+        loop {
+            if let Some(transform) = cursor.item() {
+                if transform.is_isomorphic() {
+                    let (output_start_row, input_start_row) = cursor.start();
+                    let (output_end_row, input_end_row) = cursor.end(&());
+
+                    if point.row >= output_end_row.0 {
+                        return BlockPoint::new(
+                            output_end_row.0 - 1,
+                            self.wrap_snapshot.line_len(input_end_row.0 - 1),
+                        );
+                    }
+
+                    let output_start = Point::new(output_start_row.0, 0);
+                    if point.0 > output_start {
+                        let output_overshoot = point.0 - output_start;
+                        let input_start = Point::new(input_start_row.0, 0);
+                        let input_point = self
+                            .wrap_snapshot
+                            .clip_point(WrapPoint(input_start + output_overshoot), bias);
+                        let input_overshoot = input_point.0 - input_start;
+                        return BlockPoint(output_start + input_overshoot);
+                    } else {
+                        return BlockPoint(output_start);
+                    }
+                } else if search_left {
+                    cursor.prev(&());
+                } else {
+                    cursor.next(&());
+                }
+            } else {
+                return self.max_point();
+            }
+        }
+    }
+
+    pub fn to_block_point(&self, wrap_point: WrapPoint) -> BlockPoint {
+        let mut cursor = self.transforms.cursor::<(WrapRow, BlockRow)>();
+        cursor.seek(&WrapRow(wrap_point.row()), Bias::Right, &());
+        if let Some(transform) = cursor.item() {
+            debug_assert!(transform.is_isomorphic());
+        } else {
+            return self.max_point();
+        }
+
+        let (input_start_row, output_start_row) = cursor.start();
+        let input_start = Point::new(input_start_row.0, 0);
+        let output_start = Point::new(output_start_row.0, 0);
+        let input_overshoot = wrap_point.0 - input_start;
+        BlockPoint(output_start + input_overshoot)
+    }
+
+    pub fn to_wrap_point(&self, block_point: BlockPoint) -> WrapPoint {
+        let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>();
+        cursor.seek(&BlockRow(block_point.row), Bias::Right, &());
+        if let Some(transform) = cursor.item() {
+            match transform.block.as_ref().map(|b| b.disposition) {
+                Some(BlockDisposition::Above) => WrapPoint::new(cursor.start().1 .0, 0),
+                Some(BlockDisposition::Below) => {
+                    let wrap_row = cursor.start().1 .0 - 1;
+                    WrapPoint::new(wrap_row, self.wrap_snapshot.line_len(wrap_row))
+                }
+                None => {
+                    let overshoot = block_point.row - cursor.start().0 .0;
+                    let wrap_row = cursor.start().1 .0 + overshoot;
+                    WrapPoint::new(wrap_row, block_point.column)
+                }
+            }
+        } else {
+            self.wrap_snapshot.max_point()
+        }
+    }
+}
+
+impl Transform {
+    fn isomorphic(rows: u32) -> Self {
+        Self {
+            summary: TransformSummary {
+                input_rows: rows,
+                output_rows: rows,
+                longest_row_in_block: 0,
+                longest_row_in_block_chars: 0,
+            },
+            block: None,
+        }
+    }
+
+    fn block(block: Arc<Block>, column: u32) -> Self {
+        let text_summary = block.text.summary();
+        Self {
+            summary: TransformSummary {
+                input_rows: 0,
+                output_rows: text_summary.lines.row + 1,
+                longest_row_in_block: text_summary.longest_row,
+                longest_row_in_block_chars: column + text_summary.longest_row_chars,
+            },
+            block: Some(AlignedBlock { block, column }),
+        }
+    }
+
+    fn is_isomorphic(&self) -> bool {
+        self.block.is_none()
+    }
+}
+
+impl<'a> Iterator for Chunks<'a> {
+    type Item = Chunk<'a>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        if self.output_row >= self.max_output_row {
+            return None;
+        }
+
+        if let Some(block_chunks) = self.block_chunks.as_mut() {
+            if let Some(block_chunk) = block_chunks.next() {
+                self.output_row += block_chunk.text.matches('\n').count() as u32;
+                return Some(block_chunk);
+            } else {
+                self.block_chunks.take();
+                self.output_row += 1;
+                if self.output_row < self.max_output_row {
+                    return Some(Chunk {
+                        text: "\n",
+                        ..Default::default()
+                    });
+                } else {
+                    return None;
+                }
+            }
+        }
+
+        let transform = self.transforms.item()?;
+        if let Some(block) = transform.block.as_ref() {
+            let block_start = self.transforms.start().0 .0;
+            let block_end = self.transforms.end(&()).0 .0;
+            let start_in_block = self.output_row - block_start;
+            let end_in_block = cmp::min(self.max_output_row, block_end) - block_start;
+            self.transforms.next(&());
+            self.block_chunks = Some(BlockChunks::new(
+                block,
+                start_in_block..end_in_block,
+                self.cx,
+            ));
+            return self.next();
+        }
+
+        if self.input_chunk.text.is_empty() {
+            if let Some(input_chunk) = self.input_chunks.next() {
+                self.input_chunk = input_chunk;
+            } else {
+                self.output_row += 1;
+                if self.output_row < self.max_output_row {
+                    self.transforms.next(&());
+                    return Some(Chunk {
+                        text: "\n",
+                        ..Default::default()
+                    });
+                } else {
+                    return None;
+                }
+            }
+        }
+
+        let transform_end = self.transforms.end(&()).0 .0;
+        let (prefix_rows, prefix_bytes) =
+            offset_for_row(self.input_chunk.text, transform_end - self.output_row);
+        self.output_row += prefix_rows;
+        let (prefix, suffix) = self.input_chunk.text.split_at(prefix_bytes);
+        self.input_chunk.text = suffix;
+        if self.output_row == transform_end {
+            self.transforms.next(&());
+        }
+
+        Some(Chunk {
+            text: prefix,
+            ..self.input_chunk
+        })
+    }
+}
+
+impl<'a> BlockChunks<'a> {
+    fn new(block: &'a AlignedBlock, rows: Range<u32>, cx: Option<&'a AppContext>) -> Self {
+        let offset_range = block.text.point_to_offset(Point::new(rows.start, 0))
+            ..block.text.point_to_offset(Point::new(rows.end, 0));
+
+        let mut runs = block
+            .build_runs
+            .lock()
+            .as_ref()
+            .zip(cx)
+            .map(|(build_runs, cx)| build_runs(cx))
+            .unwrap_or_default()
+            .into_iter()
+            .peekable();
+        let mut run_start = 0;
+        while let Some((run_len, _)) = runs.peek() {
+            let run_end = run_start + run_len;
+            if run_end <= offset_range.start {
+                run_start = run_end;
+                runs.next();
+            } else {
+                break;
+            }
+        }
+
+        Self {
+            chunk: None,
+            run_start,
+            padding_column: block.column,
+            remaining_padding: block.column,
+            chunks: block.text.chunks_in_range(offset_range.clone()),
+            runs,
+            offset: offset_range.start,
+        }
+    }
+}
+
+impl<'a> Iterator for BlockChunks<'a> {
+    type Item = Chunk<'a>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        if self.chunk.is_none() {
+            self.chunk = self.chunks.next();
+        }
+
+        let chunk = self.chunk?;
+
+        if chunk.starts_with('\n') {
+            self.remaining_padding = 0;
+        }
+
+        if self.remaining_padding > 0 {
+            const PADDING: &'static str = "                ";
+            let padding_len = self.remaining_padding.min(PADDING.len() as u32);
+            self.remaining_padding -= padding_len;
+            return Some(Chunk {
+                text: &PADDING[..padding_len as usize],
+                ..Default::default()
+            });
+        }
+
+        let mut chunk_len = if let Some(ix) = chunk.find('\n') {
+            ix + 1
+        } else {
+            chunk.len()
+        };
+
+        let mut highlight_style = None;
+        if let Some((run_len, style)) = self.runs.peek() {
+            highlight_style = Some(style.clone());
+            let run_end_in_chunk = self.run_start + run_len - self.offset;
+            if run_end_in_chunk <= chunk_len {
+                chunk_len = run_end_in_chunk;
+                self.run_start += run_len;
+                self.runs.next();
+            }
+        }
+
+        self.offset += chunk_len;
+        let (chunk, suffix) = chunk.split_at(chunk_len);
+
+        if chunk.ends_with('\n') {
+            self.remaining_padding = self.padding_column;
+        }
+
+        self.chunk = if suffix.is_empty() {
+            None
+        } else {
+            Some(suffix)
+        };
+
+        Some(Chunk {
+            text: chunk,
+            highlight_style,
+            diagnostic: None,
+        })
+    }
+}
+
+impl<'a> Iterator for BufferRows<'a> {
+    type Item = DisplayRow;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        if self.started {
+            self.output_row += 1;
+        } else {
+            self.started = true;
+        }
+
+        if self.output_row >= self.transforms.end(&()).0 .0 {
+            self.transforms.next(&());
+        }
+
+        let transform = self.transforms.item()?;
+        if let Some(block) = &transform.block {
+            let style = self
+                .cx
+                .and_then(|cx| block.build_style.lock().as_ref().map(|f| f(cx)));
+            Some(DisplayRow::Block(block.id, style))
+        } else {
+            Some(self.input_buffer_rows.next().unwrap())
+        }
+    }
+}
+
+impl sum_tree::Item for Transform {
+    type Summary = TransformSummary;
+
+    fn summary(&self) -> Self::Summary {
+        self.summary.clone()
+    }
+}
+
+impl sum_tree::Summary for TransformSummary {
+    type Context = ();
+
+    fn add_summary(&mut self, summary: &Self, _: &()) {
+        if summary.longest_row_in_block_chars > self.longest_row_in_block_chars {
+            self.longest_row_in_block_chars = summary.longest_row_in_block_chars;
+            self.longest_row_in_block = self.output_rows + summary.longest_row_in_block;
+        }
+
+        self.input_rows += summary.input_rows;
+        self.output_rows += summary.output_rows;
+    }
+}
+
+impl<'a> sum_tree::Dimension<'a, TransformSummary> for WrapRow {
+    fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
+        self.0 += summary.input_rows;
+    }
+}
+
+impl<'a> sum_tree::Dimension<'a, TransformSummary> for BlockRow {
+    fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
+        self.0 += summary.output_rows;
+    }
+}
+
+impl BlockDisposition {
+    fn is_below(&self) -> bool {
+        matches!(self, BlockDisposition::Below)
+    }
+}
+
+impl Deref for AlignedBlock {
+    type Target = Block;
+
+    fn deref(&self) -> &Self::Target {
+        self.block.as_ref()
+    }
+}
+
+impl Debug for Block {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("Block")
+            .field("id", &self.id)
+            .field("position", &self.position)
+            .field("text", &self.text)
+            .field("disposition", &self.disposition)
+            .finish()
+    }
+}
+
+// Count the number of bytes prior to a target point. If the string doesn't contain the target
+// point, return its total extent. Otherwise return the target point itself.
+fn offset_for_row(s: &str, target: u32) -> (u32, usize) {
+    let mut row = 0;
+    let mut offset = 0;
+    for (ix, line) in s.split('\n').enumerate() {
+        if ix > 0 {
+            row += 1;
+            offset += 1;
+        }
+        if row >= target {
+            break;
+        }
+        offset += line.len() as usize;
+    }
+    (row, offset)
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::display_map::{fold_map::FoldMap, tab_map::TabMap, wrap_map::WrapMap};
+    use buffer::RandomCharIter;
+    use gpui::color::Color;
+    use language::Buffer;
+    use rand::prelude::*;
+    use std::env;
+
+    #[gpui::test]
+    fn test_offset_for_row() {
+        assert_eq!(offset_for_row("", 0), (0, 0));
+        assert_eq!(offset_for_row("", 1), (0, 0));
+        assert_eq!(offset_for_row("abcd", 0), (0, 0));
+        assert_eq!(offset_for_row("abcd", 1), (0, 4));
+        assert_eq!(offset_for_row("\n", 0), (0, 0));
+        assert_eq!(offset_for_row("\n", 1), (1, 1));
+        assert_eq!(offset_for_row("abc\ndef\nghi", 0), (0, 0));
+        assert_eq!(offset_for_row("abc\ndef\nghi", 1), (1, 4));
+        assert_eq!(offset_for_row("abc\ndef\nghi", 2), (2, 8));
+        assert_eq!(offset_for_row("abc\ndef\nghi", 3), (2, 11));
+    }
+
+    #[gpui::test]
+    fn test_block_chunks(cx: &mut gpui::MutableAppContext) {
+        let red = Color::red();
+        let blue = Color::blue();
+        let clear = Color::default();
+
+        let block = AlignedBlock {
+            column: 5,
+            block: Arc::new(Block {
+                id: BlockId(0),
+                position: Anchor::min(),
+                text: "one!\ntwo three\nfour".into(),
+                build_style: Mutex::new(None),
+                build_runs: Mutex::new(Some(Arc::new(move |_| {
+                    vec![(3, red.into()), (6, Default::default()), (5, blue.into())]
+                }))),
+                disposition: BlockDisposition::Above,
+            }),
+        };
+
+        assert_eq!(
+            colored_chunks(&block, 0..3, cx),
+            &[
+                ("     ", clear),
+                ("one", red),
+                ("!\n", clear),
+                ("     ", clear),
+                ("two ", clear),
+                ("three", blue),
+                ("\n", clear),
+                ("     ", clear),
+                ("four", clear)
+            ]
+        );
+        assert_eq!(
+            colored_chunks(&block, 0..1, cx),
+            &[
+                ("     ", clear), //
+                ("one", red),
+                ("!\n", clear),
+            ]
+        );
+        assert_eq!(
+            colored_chunks(&block, 1..3, cx),
+            &[
+                ("     ", clear),
+                ("two ", clear),
+                ("three", blue),
+                ("\n", clear),
+                ("     ", clear),
+                ("four", clear)
+            ]
+        );
+
+        fn colored_chunks<'a>(
+            block: &'a AlignedBlock,
+            row_range: Range<u32>,
+            cx: &'a AppContext,
+        ) -> Vec<(&'a str, Color)> {
+            BlockChunks::new(block, row_range, Some(cx))
+                .map(|c| {
+                    (
+                        c.text,
+                        c.highlight_style.map_or(Color::default(), |s| s.color),
+                    )
+                })
+                .collect()
+        }
+    }
+
+    #[gpui::test]
+    fn test_basic_blocks(cx: &mut gpui::MutableAppContext) {
+        let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
+        let font_id = cx
+            .font_cache()
+            .select_font(family_id, &Default::default())
+            .unwrap();
+
+        let text = "aaa\nbbb\nccc\nddd";
+
+        let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
+        let (fold_map, folds_snapshot) = FoldMap::new(buffer.clone(), cx);
+        let (tab_map, tabs_snapshot) = TabMap::new(folds_snapshot.clone(), 1);
+        let (wrap_map, wraps_snapshot) = WrapMap::new(tabs_snapshot, font_id, 14.0, None, cx);
+        let mut block_map = BlockMap::new(buffer.clone(), wraps_snapshot.clone());
+
+        let mut writer = block_map.write(wraps_snapshot.clone(), vec![], cx);
+        let block_ids = writer.insert(
+            vec![
+                BlockProperties {
+                    position: Point::new(1, 0),
+                    text: "BLOCK 1",
+                    disposition: BlockDisposition::Above,
+                    build_runs: None,
+                    build_style: None,
+                },
+                BlockProperties {
+                    position: Point::new(1, 2),
+                    text: "BLOCK 2",
+                    disposition: BlockDisposition::Above,
+                    build_runs: None,
+                    build_style: None,
+                },
+                BlockProperties {
+                    position: Point::new(3, 2),
+                    text: "BLOCK 3",
+                    disposition: BlockDisposition::Below,
+                    build_runs: None,
+                    build_style: None,
+                },
+            ],
+            cx,
+        );
+
+        let mut snapshot = block_map.read(wraps_snapshot, vec![], cx);
+        assert_eq!(
+            snapshot.text(),
+            "aaa\nBLOCK 1\n  BLOCK 2\nbbb\nccc\nddd\n  BLOCK 3"
+        );
+        assert_eq!(
+            snapshot.to_block_point(WrapPoint::new(0, 3)),
+            BlockPoint::new(0, 3)
+        );
+        assert_eq!(
+            snapshot.to_block_point(WrapPoint::new(1, 0)),
+            BlockPoint::new(3, 0)
+        );
+        assert_eq!(
+            snapshot.to_block_point(WrapPoint::new(3, 3)),
+            BlockPoint::new(5, 3)
+        );
+
+        assert_eq!(
+            snapshot.to_wrap_point(BlockPoint::new(0, 3)),
+            WrapPoint::new(0, 3)
+        );
+        assert_eq!(
+            snapshot.to_wrap_point(BlockPoint::new(1, 0)),
+            WrapPoint::new(1, 0)
+        );
+        assert_eq!(
+            snapshot.to_wrap_point(BlockPoint::new(3, 0)),
+            WrapPoint::new(1, 0)
+        );
+        assert_eq!(
+            snapshot.to_wrap_point(BlockPoint::new(6, 0)),
+            WrapPoint::new(3, 3)
+        );
+
+        assert_eq!(
+            snapshot.clip_point(BlockPoint::new(1, 0), Bias::Left),
+            BlockPoint::new(0, 3)
+        );
+        assert_eq!(
+            snapshot.clip_point(BlockPoint::new(1, 0), Bias::Right),
+            BlockPoint::new(3, 0)
+        );
+        assert_eq!(
+            snapshot.clip_point(BlockPoint::new(1, 1), Bias::Left),
+            BlockPoint::new(0, 3)
+        );
+        assert_eq!(
+            snapshot.clip_point(BlockPoint::new(1, 1), Bias::Right),
+            BlockPoint::new(3, 0)
+        );
+        assert_eq!(
+            snapshot.clip_point(BlockPoint::new(3, 0), Bias::Left),
+            BlockPoint::new(3, 0)
+        );
+        assert_eq!(
+            snapshot.clip_point(BlockPoint::new(3, 0), Bias::Right),
+            BlockPoint::new(3, 0)
+        );
+        assert_eq!(
+            snapshot.clip_point(BlockPoint::new(5, 3), Bias::Left),
+            BlockPoint::new(5, 3)
+        );
+        assert_eq!(
+            snapshot.clip_point(BlockPoint::new(5, 3), Bias::Right),
+            BlockPoint::new(5, 3)
+        );
+        assert_eq!(
+            snapshot.clip_point(BlockPoint::new(6, 0), Bias::Left),
+            BlockPoint::new(5, 3)
+        );
+        assert_eq!(
+            snapshot.clip_point(BlockPoint::new(6, 0), Bias::Right),
+            BlockPoint::new(5, 3)
+        );
+
+        assert_eq!(
+            snapshot.buffer_rows(0, None).collect::<Vec<_>>(),
+            &[
+                DisplayRow::Buffer(0),
+                DisplayRow::Block(block_ids[0], None),
+                DisplayRow::Block(block_ids[1], None),
+                DisplayRow::Buffer(1),
+                DisplayRow::Buffer(2),
+                DisplayRow::Buffer(3),
+                DisplayRow::Block(block_ids[2], None)
+            ]
+        );
+
+        // Insert a line break, separating two block decorations into separate
+        // lines.
+        buffer.update(cx, |buffer, cx| {
+            buffer.edit([Point::new(1, 1)..Point::new(1, 1)], "!!!\n", cx)
+        });
+
+        let (folds_snapshot, fold_edits) = fold_map.read(cx);
+        let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits);
+        let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
+            wrap_map.sync(tabs_snapshot, tab_edits, cx)
+        });
+        let mut snapshot = block_map.read(wraps_snapshot, wrap_edits, cx);
+        assert_eq!(
+            snapshot.text(),
+            "aaa\nBLOCK 1\nb!!!\n BLOCK 2\nbb\nccc\nddd\n  BLOCK 3"
+        );
+    }
+
+    #[gpui::test]
+    fn test_blocks_on_wrapped_lines(cx: &mut gpui::MutableAppContext) {
+        let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
+        let font_id = cx
+            .font_cache()
+            .select_font(family_id, &Default::default())
+            .unwrap();
+
+        let text = "one two three\nfour five six\nseven eight";
+
+        let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
+        let (_, folds_snapshot) = FoldMap::new(buffer.clone(), cx);
+        let (_, tabs_snapshot) = TabMap::new(folds_snapshot.clone(), 1);
+        let (_, wraps_snapshot) = WrapMap::new(tabs_snapshot, font_id, 14.0, Some(60.), cx);
+        let mut block_map = BlockMap::new(buffer.clone(), wraps_snapshot.clone());
+
+        let mut writer = block_map.write(wraps_snapshot.clone(), vec![], cx);
+        writer.insert(
+            vec![
+                BlockProperties {
+                    position: Point::new(1, 12),
+                    text: "<BLOCK 1",
+                    disposition: BlockDisposition::Above,
+                    build_runs: None,
+                    build_style: None,
+                },
+                BlockProperties {
+                    position: Point::new(1, 1),
+                    text: ">BLOCK 2",
+                    disposition: BlockDisposition::Below,
+                    build_runs: None,
+                    build_style: None,
+                },
+            ],
+            cx,
+        );
+
+        // Blocks with an 'above' disposition go above their corresponding buffer line.
+        // Blocks with a 'below' disposition go below their corresponding buffer line.
+        let mut snapshot = block_map.read(wraps_snapshot, vec![], cx);
+        assert_eq!(
+            snapshot.text(),
+            "one two \nthree\n  <BLOCK 1\nfour five \nsix\n >BLOCK 2\nseven \neight"
+        );
+    }
+
+    #[gpui::test(iterations = 100)]
+    fn test_random_blocks(cx: &mut gpui::MutableAppContext, mut rng: StdRng) {
+        let operations = env::var("OPERATIONS")
+            .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
+            .unwrap_or(10);
+
+        let wrap_width = if rng.gen_bool(0.2) {
+            None
+        } else {
+            Some(rng.gen_range(0.0..=100.0))
+        };
+        let tab_size = 1;
+        let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
+        let font_id = cx
+            .font_cache()
+            .select_font(family_id, &Default::default())
+            .unwrap();
+        let font_size = 14.0;
+
+        log::info!("Wrap width: {:?}", wrap_width);
+
+        let buffer = cx.add_model(|cx| {
+            let len = rng.gen_range(0..10);
+            let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
+            log::info!("initial buffer text: {:?}", text);
+            Buffer::new(0, text, cx)
+        });
+        let (fold_map, folds_snapshot) = FoldMap::new(buffer.clone(), cx);
+        let (tab_map, tabs_snapshot) = TabMap::new(folds_snapshot.clone(), tab_size);
+        let (wrap_map, wraps_snapshot) =
+            WrapMap::new(tabs_snapshot, font_id, font_size, wrap_width, cx);
+        let mut block_map = BlockMap::new(buffer.clone(), wraps_snapshot);
+        let mut expected_blocks = Vec::new();
+
+        for _ in 0..operations {
+            match rng.gen_range(0..=100) {
+                0..=19 => {
+                    let wrap_width = if rng.gen_bool(0.2) {
+                        None
+                    } else {
+                        Some(rng.gen_range(0.0..=100.0))
+                    };
+                    log::info!("Setting wrap width to {:?}", wrap_width);
+                    wrap_map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx));
+                }
+                20..=39 => {
+                    let block_count = rng.gen_range(1..=1);
+                    let block_properties = (0..block_count)
+                        .map(|_| {
+                            let buffer = buffer.read(cx);
+                            let position = buffer.anchor_after(
+                                buffer.clip_offset(rng.gen_range(0..=buffer.len()), Bias::Left),
+                            );
+
+                            let len = rng.gen_range(0..10);
+                            let mut text = Rope::from(
+                                RandomCharIter::new(&mut rng)
+                                    .take(len)
+                                    .collect::<String>()
+                                    .to_uppercase()
+                                    .as_str(),
+                            );
+                            let disposition = if rng.gen() {
+                                text.push_front("<");
+                                BlockDisposition::Above
+                            } else {
+                                text.push_front(">");
+                                BlockDisposition::Below
+                            };
+                            log::info!(
+                                "inserting block {:?} {:?} with text {:?}",
+                                disposition,
+                                position.to_point(buffer),
+                                text.to_string()
+                            );
+                            BlockProperties {
+                                position,
+                                text,
+                                disposition,
+                                build_runs: None,
+                                build_style: None,
+                            }
+                        })
+                        .collect::<Vec<_>>();
+
+                    let (folds_snapshot, fold_edits) = fold_map.read(cx);
+                    let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits);
+                    let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
+                        wrap_map.sync(tabs_snapshot, tab_edits, cx)
+                    });
+                    let mut block_map = block_map.write(wraps_snapshot, wrap_edits, cx);
+                    let block_ids = block_map.insert(block_properties.clone(), cx);
+                    for (block_id, props) in block_ids.into_iter().zip(block_properties) {
+                        expected_blocks.push((block_id, props));
+                    }
+                }
+                40..=59 if !expected_blocks.is_empty() => {
+                    let block_count = rng.gen_range(1..=4.min(expected_blocks.len()));
+                    let block_ids_to_remove = (0..block_count)
+                        .map(|_| {
+                            expected_blocks
+                                .remove(rng.gen_range(0..expected_blocks.len()))
+                                .0
+                        })
+                        .collect();
+
+                    let (folds_snapshot, fold_edits) = fold_map.read(cx);
+                    let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits);
+                    let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
+                        wrap_map.sync(tabs_snapshot, tab_edits, cx)
+                    });
+                    let mut block_map = block_map.write(wraps_snapshot, wrap_edits, cx);
+                    block_map.remove(block_ids_to_remove, cx);
+                }
+                _ => {
+                    buffer.update(cx, |buffer, _| {
+                        buffer.randomly_edit(&mut rng, 1);
+                        log::info!("buffer text: {:?}", buffer.text());
+                    });
+                }
+            }
+
+            let (folds_snapshot, fold_edits) = fold_map.read(cx);
+            let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits);
+            let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
+                wrap_map.sync(tabs_snapshot, tab_edits, cx)
+            });
+            let mut blocks_snapshot = block_map.read(wraps_snapshot.clone(), wrap_edits, cx);
+            assert_eq!(
+                blocks_snapshot.transforms.summary().input_rows,
+                wraps_snapshot.max_point().row() + 1
+            );
+            log::info!("blocks text: {:?}", blocks_snapshot.text());
+
+            let buffer = buffer.read(cx);
+            let mut sorted_blocks = expected_blocks
+                .iter()
+                .cloned()
+                .map(|(id, block)| {
+                    let mut position = block.position.to_point(buffer);
+                    let column = wraps_snapshot.from_point(position, Bias::Left).column();
+                    match block.disposition {
+                        BlockDisposition::Above => {
+                            position.column = 0;
+                        }
+                        BlockDisposition::Below => {
+                            position.column = buffer.line_len(position.row);
+                        }
+                    };
+                    let row = wraps_snapshot.from_point(position, Bias::Left).row();
+                    (
+                        id,
+                        BlockProperties {
+                            position: BlockPoint::new(row, column),
+                            text: block.text,
+                            build_runs: block.build_runs.clone(),
+                            build_style: None,
+                            disposition: block.disposition,
+                        },
+                    )
+                })
+                .collect::<Vec<_>>();
+            sorted_blocks
+                .sort_unstable_by_key(|(id, block)| (block.position.row, block.disposition, *id));
+            let mut sorted_blocks = sorted_blocks.into_iter().peekable();
+
+            let mut expected_buffer_rows = Vec::new();
+            let mut expected_text = String::new();
+            let input_text = wraps_snapshot.text();
+            for (row, input_line) in input_text.split('\n').enumerate() {
+                let row = row as u32;
+                if row > 0 {
+                    expected_text.push('\n');
+                }
+
+                let buffer_row = wraps_snapshot
+                    .to_point(WrapPoint::new(row, 0), Bias::Left)
+                    .row;
+
+                while let Some((block_id, block)) = sorted_blocks.peek() {
+                    if block.position.row == row && block.disposition == BlockDisposition::Above {
+                        let text = block.text.to_string();
+                        let padding = " ".repeat(block.position.column as usize);
+                        for line in text.split('\n') {
+                            if !line.is_empty() {
+                                expected_text.push_str(&padding);
+                                expected_text.push_str(line);
+                            }
+                            expected_text.push('\n');
+                            expected_buffer_rows.push(DisplayRow::Block(*block_id, None));
+                        }
+                        sorted_blocks.next();
+                    } else {
+                        break;
+                    }
+                }
+
+                let soft_wrapped = wraps_snapshot.to_tab_point(WrapPoint::new(row, 0)).column() > 0;
+                expected_buffer_rows.push(if soft_wrapped {
+                    DisplayRow::Wrap
+                } else {
+                    DisplayRow::Buffer(buffer_row)
+                });
+                expected_text.push_str(input_line);
+
+                while let Some((block_id, block)) = sorted_blocks.peek() {
+                    if block.position.row == row && block.disposition == BlockDisposition::Below {
+                        let text = block.text.to_string();
+                        let padding = " ".repeat(block.position.column as usize);
+                        for line in text.split('\n') {
+                            expected_text.push('\n');
+                            if !line.is_empty() {
+                                expected_text.push_str(&padding);
+                                expected_text.push_str(line);
+                            }
+                            expected_buffer_rows.push(DisplayRow::Block(*block_id, None));
+                        }
+                        sorted_blocks.next();
+                    } else {
+                        break;
+                    }
+                }
+            }
+
+            let expected_lines = expected_text.split('\n').collect::<Vec<_>>();
+            let expected_row_count = expected_lines.len();
+            for start_row in 0..expected_row_count {
+                let expected_text = expected_lines[start_row..].join("\n");
+                let actual_text = blocks_snapshot
+                    .chunks(start_row as u32..expected_row_count as u32, None, None)
+                    .map(|chunk| chunk.text)
+                    .collect::<String>();
+                assert_eq!(
+                    actual_text, expected_text,
+                    "incorrect text starting from row {}",
+                    start_row
+                );
+                assert_eq!(
+                    blocks_snapshot
+                        .buffer_rows(start_row as u32, None)
+                        .collect::<Vec<_>>(),
+                    &expected_buffer_rows[start_row..]
+                );
+            }
+
+            let mut expected_longest_rows = Vec::new();
+            let mut longest_line_len = -1_isize;
+            for (row, line) in expected_lines.iter().enumerate() {
+                let row = row as u32;
+
+                assert_eq!(
+                    blocks_snapshot.line_len(row),
+                    line.len() as u32,
+                    "invalid line len for row {}",
+                    row
+                );
+
+                let line_char_count = line.chars().count() as isize;
+                match line_char_count.cmp(&longest_line_len) {
+                    Ordering::Less => {}
+                    Ordering::Equal => expected_longest_rows.push(row),
+                    Ordering::Greater => {
+                        longest_line_len = line_char_count;
+                        expected_longest_rows.clear();
+                        expected_longest_rows.push(row);
+                    }
+                }
+            }
+
+            log::info!("getting longest row >>>>>>>>>>>>>>>>>>>>>>>>");
+            let longest_row = blocks_snapshot.longest_row();
+            assert!(
+                expected_longest_rows.contains(&longest_row),
+                "incorrect longest row {}. expected {:?} with length {}",
+                longest_row,
+                expected_longest_rows,
+                longest_line_len,
+            );
+
+            for row in 0..=blocks_snapshot.wrap_snapshot.max_point().row() {
+                let wrap_point = WrapPoint::new(row, 0);
+                let block_point = blocks_snapshot.to_block_point(wrap_point);
+                assert_eq!(blocks_snapshot.to_wrap_point(block_point), wrap_point);
+            }
+
+            let mut block_point = BlockPoint::new(0, 0);
+            for c in expected_text.chars() {
+                let left_point = blocks_snapshot.clip_point(block_point, Bias::Left);
+                let right_point = blocks_snapshot.clip_point(block_point, Bias::Right);
+
+                assert_eq!(
+                    blocks_snapshot.to_block_point(blocks_snapshot.to_wrap_point(left_point)),
+                    left_point
+                );
+                assert_eq!(
+                    blocks_snapshot.to_block_point(blocks_snapshot.to_wrap_point(right_point)),
+                    right_point
+                );
+
+                if c == '\n' {
+                    block_point.0 += Point::new(1, 0);
+                } else {
+                    block_point.column += c.len_utf8() as u32;
+                }
+            }
+        }
+    }
+}

crates/editor/src/display_map/fold_map.rs šŸ”—

@@ -1,8 +1,5 @@
 use gpui::{AppContext, ModelHandle};
-use language::{
-    Anchor, AnchorRangeExt, Buffer, HighlightId, HighlightedChunk, Point, PointUtf16, TextSummary,
-    ToOffset,
-};
+use language::{Anchor, AnchorRangeExt, Buffer, Chunk, Point, PointUtf16, TextSummary, ToOffset};
 use parking_lot::Mutex;
 use std::{
     cmp::{self, Ordering},
@@ -11,6 +8,7 @@ use std::{
     sync::atomic::{AtomicUsize, Ordering::SeqCst},
 };
 use sum_tree::{Bias, Cursor, FilterCursor, SumTree};
+use theme::SyntaxTheme;
 
 pub trait ToFoldPoint {
     fn to_fold_point(&self, snapshot: &Snapshot, bias: Bias) -> FoldPoint;
@@ -499,7 +497,9 @@ pub struct Snapshot {
 impl Snapshot {
     #[cfg(test)]
     pub fn text(&self) -> String {
-        self.chunks_at(FoldOffset(0)).collect()
+        self.chunks(FoldOffset(0)..self.len(), None)
+            .map(|c| c.text)
+            .collect()
     }
 
     #[cfg(test)]
@@ -551,7 +551,6 @@ impl Snapshot {
         summary
     }
 
-    #[cfg(test)]
     pub fn len(&self) -> FoldOffset {
         FoldOffset(self.transforms.summary().output.bytes)
     }
@@ -628,21 +627,17 @@ impl Snapshot {
         false
     }
 
-    pub fn chunks_at(&self, offset: FoldOffset) -> Chunks {
-        let mut transform_cursor = self.transforms.cursor::<(FoldOffset, usize)>();
-        transform_cursor.seek(&offset, Bias::Right, &());
-        let overshoot = offset.0 - transform_cursor.start().0 .0;
-        let buffer_offset = transform_cursor.start().1 + overshoot;
-        Chunks {
-            transform_cursor,
-            buffer_offset,
-            buffer_chunks: self
-                .buffer_snapshot
-                .text_for_range(buffer_offset..self.buffer_snapshot.len()),
-        }
+    pub fn chars_at(&self, start: FoldPoint) -> impl '_ + Iterator<Item = char> {
+        let start = start.to_offset(self);
+        self.chunks(start..self.len(), None)
+            .flat_map(|chunk| chunk.text.chars())
     }
 
-    pub fn highlighted_chunks(&mut self, range: Range<FoldOffset>) -> HighlightedChunks {
+    pub fn chunks<'a>(
+        &'a self,
+        range: Range<FoldOffset>,
+        theme: Option<&'a SyntaxTheme>,
+    ) -> Chunks<'a> {
         let mut transform_cursor = self.transforms.cursor::<(FoldOffset, usize)>();
 
         transform_cursor.seek(&range.end, Bias::Right, &());
@@ -653,21 +648,16 @@ impl Snapshot {
         let overshoot = range.start.0 - transform_cursor.start().0 .0;
         let buffer_start = transform_cursor.start().1 + overshoot;
 
-        HighlightedChunks {
+        Chunks {
             transform_cursor,
-            buffer_offset: buffer_start,
-            buffer_chunks: self
-                .buffer_snapshot
-                .highlighted_text_for_range(buffer_start..buffer_end),
+            buffer_chunks: self.buffer_snapshot.chunks(buffer_start..buffer_end, theme),
             buffer_chunk: None,
+            buffer_offset: buffer_start,
+            output_offset: range.start.0,
+            max_output_offset: range.end.0,
         }
     }
 
-    pub fn chars_at<'a>(&'a self, point: FoldPoint) -> impl Iterator<Item = char> + 'a {
-        let offset = point.to_offset(self);
-        self.chunks_at(offset).flat_map(str::chars)
-    }
-
     #[cfg(test)]
     pub fn clip_offset(&self, offset: FoldOffset, bias: Bias) -> FoldOffset {
         let mut cursor = self.transforms.cursor::<(FoldOffset, usize)>();
@@ -948,68 +938,21 @@ impl<'a> Iterator for BufferRows<'a> {
 
 pub struct Chunks<'a> {
     transform_cursor: Cursor<'a, Transform, (FoldOffset, usize)>,
-    buffer_chunks: buffer::Chunks<'a>,
+    buffer_chunks: language::Chunks<'a>,
+    buffer_chunk: Option<(usize, Chunk<'a>)>,
     buffer_offset: usize,
+    output_offset: usize,
+    max_output_offset: usize,
 }
 
 impl<'a> Iterator for Chunks<'a> {
-    type Item = &'a str;
+    type Item = Chunk<'a>;
 
     fn next(&mut self) -> Option<Self::Item> {
-        let transform = if let Some(item) = self.transform_cursor.item() {
-            item
-        } else {
+        if self.output_offset >= self.max_output_offset {
             return None;
-        };
-
-        // If we're in a fold, then return the fold's display text and
-        // advance the transform and buffer cursors to the end of the fold.
-        if let Some(output_text) = transform.output_text {
-            self.buffer_offset += transform.summary.input.bytes;
-            self.buffer_chunks.seek(self.buffer_offset);
-
-            while self.buffer_offset >= self.transform_cursor.end(&()).1
-                && self.transform_cursor.item().is_some()
-            {
-                self.transform_cursor.next(&());
-            }
-
-            return Some(output_text);
-        }
-
-        // Otherwise, take a chunk from the buffer's text.
-        if let Some(mut chunk) = self.buffer_chunks.peek() {
-            let offset_in_chunk = self.buffer_offset - self.buffer_chunks.offset();
-            chunk = &chunk[offset_in_chunk..];
-
-            // Truncate the chunk so that it ends at the next fold.
-            let region_end = self.transform_cursor.end(&()).1 - self.buffer_offset;
-            if chunk.len() >= region_end {
-                chunk = &chunk[0..region_end];
-                self.transform_cursor.next(&());
-            } else {
-                self.buffer_chunks.next();
-            }
-
-            self.buffer_offset += chunk.len();
-            return Some(chunk);
         }
 
-        None
-    }
-}
-
-pub struct HighlightedChunks<'a> {
-    transform_cursor: Cursor<'a, Transform, (FoldOffset, usize)>,
-    buffer_chunks: language::HighlightedChunks<'a>,
-    buffer_chunk: Option<(usize, HighlightedChunk<'a>)>,
-    buffer_offset: usize,
-}
-
-impl<'a> Iterator for HighlightedChunks<'a> {
-    type Item = HighlightedChunk<'a>;
-
-    fn next(&mut self) -> Option<Self::Item> {
         let transform = if let Some(item) = self.transform_cursor.item() {
             item
         } else {
@@ -1029,9 +972,10 @@ impl<'a> Iterator for HighlightedChunks<'a> {
                 self.transform_cursor.next(&());
             }
 
-            return Some(HighlightedChunk {
+            self.output_offset += output_text.len();
+            return Some(Chunk {
                 text: output_text,
-                highlight_id: HighlightId::default(),
+                highlight_style: None,
                 diagnostic: None,
             });
         }
@@ -1057,6 +1001,7 @@ impl<'a> Iterator for HighlightedChunks<'a> {
             }
 
             self.buffer_offset += chunk.text.len();
+            self.output_offset += chunk.text.len();
             return Some(chunk);
         }
 
@@ -1352,7 +1297,7 @@ mod tests {
             }
 
             let buffer = map.buffer.read(cx).snapshot();
-            let mut expected_text: String = buffer.text().into();
+            let mut expected_text: String = buffer.text().to_string();
             let mut expected_buffer_rows = Vec::new();
             let mut next_row = buffer.max_point().row;
             for fold_range in map.merged_fold_ranges(cx.as_ref()).into_iter().rev() {
@@ -1428,11 +1373,22 @@ mod tests {
             }
 
             for _ in 0..5 {
-                let offset = snapshot
+                let mut start = snapshot
+                    .clip_offset(FoldOffset(rng.gen_range(0..=snapshot.len().0)), Bias::Left);
+                let mut end = snapshot
                     .clip_offset(FoldOffset(rng.gen_range(0..=snapshot.len().0)), Bias::Right);
+                if start > end {
+                    mem::swap(&mut start, &mut end);
+                }
+
+                let text = &expected_text[start.0..end.0];
+                log::info!("slicing {:?}..{:?} (text: {:?})", start, end, text);
                 assert_eq!(
-                    snapshot.chunks_at(offset).collect::<String>(),
-                    &expected_text[offset.0..],
+                    snapshot
+                        .chunks(start..end, None)
+                        .map(|c| c.text)
+                        .collect::<String>(),
+                    text,
                 );
             }
 

crates/editor/src/display_map/patch.rs šŸ”—

@@ -0,0 +1,511 @@
+use std::{cmp, mem};
+
+type Edit = buffer::Edit<u32>;
+
+#[derive(Default, Debug, PartialEq, Eq)]
+pub struct Patch(Vec<Edit>);
+
+impl Patch {
+    pub unsafe fn new_unchecked(edits: Vec<Edit>) -> Self {
+        Self(edits)
+    }
+
+    pub fn into_inner(self) -> Vec<Edit> {
+        self.0
+    }
+
+    pub fn compose(&self, other: &Self) -> Self {
+        let mut old_edits_iter = self.0.iter().cloned().peekable();
+        let mut new_edits_iter = other.0.iter().cloned().peekable();
+        let mut composed = Patch(Vec::new());
+
+        let mut old_start = 0;
+        let mut new_start = 0;
+        loop {
+            let old_edit = old_edits_iter.peek_mut();
+            let new_edit = new_edits_iter.peek_mut();
+
+            // Push the old edit if its new end is before the new edit's old start.
+            if let Some(old_edit) = old_edit.as_ref() {
+                let new_edit = new_edit.as_ref();
+                if new_edit.map_or(true, |new_edit| old_edit.new.end < new_edit.old.start) {
+                    let catchup = old_edit.old.start - old_start;
+                    old_start += catchup;
+                    new_start += catchup;
+
+                    let old_end = old_start + old_edit.old.len() as u32;
+                    let new_end = new_start + old_edit.new.len() as u32;
+                    composed.push(Edit {
+                        old: old_start..old_end,
+                        new: new_start..new_end,
+                    });
+                    old_start = old_end;
+                    new_start = new_end;
+                    old_edits_iter.next();
+                    continue;
+                }
+            }
+
+            // Push the new edit if its old end is before the old edit's new start.
+            if let Some(new_edit) = new_edit.as_ref() {
+                let old_edit = old_edit.as_ref();
+                if old_edit.map_or(true, |old_edit| new_edit.old.end < old_edit.new.start) {
+                    let catchup = new_edit.new.start - new_start;
+                    old_start += catchup;
+                    new_start += catchup;
+
+                    let old_end = old_start + new_edit.old.len() as u32;
+                    let new_end = new_start + new_edit.new.len() as u32;
+                    composed.push(Edit {
+                        old: old_start..old_end,
+                        new: new_start..new_end,
+                    });
+                    old_start = old_end;
+                    new_start = new_end;
+                    new_edits_iter.next();
+                    continue;
+                }
+            }
+
+            // If we still have edits by this point then they must intersect, so we compose them.
+            if let Some((old_edit, new_edit)) = old_edit.zip(new_edit) {
+                if old_edit.new.start < new_edit.old.start {
+                    let catchup = old_edit.old.start - old_start;
+                    old_start += catchup;
+                    new_start += catchup;
+
+                    let overshoot = new_edit.old.start - old_edit.new.start;
+                    let old_end = cmp::min(old_start + overshoot, old_edit.old.end);
+                    let new_end = new_start + overshoot;
+                    composed.push(Edit {
+                        old: old_start..old_end,
+                        new: new_start..new_end,
+                    });
+
+                    old_edit.old.start += overshoot;
+                    old_edit.new.start += overshoot;
+                    old_start = old_end;
+                    new_start = new_end;
+                } else {
+                    let catchup = new_edit.new.start - new_start;
+                    old_start += catchup;
+                    new_start += catchup;
+
+                    let overshoot = old_edit.new.start - new_edit.old.start;
+                    let old_end = old_start + overshoot;
+                    let new_end = cmp::min(new_start + overshoot, new_edit.new.end);
+                    composed.push(Edit {
+                        old: old_start..old_end,
+                        new: new_start..new_end,
+                    });
+
+                    new_edit.old.start += overshoot;
+                    new_edit.new.start += overshoot;
+                    old_start = old_end;
+                    new_start = new_end;
+                }
+
+                if old_edit.new.end > new_edit.old.end {
+                    let old_end =
+                        old_start + cmp::min(old_edit.old.len() as u32, new_edit.old.len() as u32);
+                    let new_end = new_start + new_edit.new.len() as u32;
+                    composed.push(Edit {
+                        old: old_start..old_end,
+                        new: new_start..new_end,
+                    });
+
+                    old_edit.old.start = old_end;
+                    old_edit.new.start = new_edit.old.end;
+                    old_start = old_end;
+                    new_start = new_end;
+                    new_edits_iter.next();
+                } else {
+                    let old_end = old_start + old_edit.old.len() as u32;
+                    let new_end =
+                        new_start + cmp::min(old_edit.new.len() as u32, new_edit.new.len() as u32);
+                    composed.push(Edit {
+                        old: old_start..old_end,
+                        new: new_start..new_end,
+                    });
+
+                    new_edit.old.start = old_edit.new.end;
+                    new_edit.new.start = new_end;
+                    old_start = old_end;
+                    new_start = new_end;
+                    old_edits_iter.next();
+                }
+            } else {
+                break;
+            }
+        }
+
+        composed
+    }
+
+    pub fn invert(&mut self) -> &mut Self {
+        for edit in &mut self.0 {
+            mem::swap(&mut edit.old, &mut edit.new);
+        }
+        self
+    }
+
+    pub fn clear(&mut self) {
+        self.0.clear();
+    }
+
+    fn push(&mut self, edit: Edit) {
+        if edit.old.len() == 0 && edit.new.len() == 0 {
+            return;
+        }
+
+        if let Some(last) = self.0.last_mut() {
+            if last.old.end >= edit.old.start {
+                last.old.end = edit.old.end;
+                last.new.end = edit.new.end;
+            } else {
+                self.0.push(edit);
+            }
+        } else {
+            self.0.push(edit);
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use rand::prelude::*;
+    use std::env;
+
+    #[gpui::test]
+    fn test_one_disjoint_edit() {
+        assert_patch_composition(
+            Patch(vec![Edit {
+                old: 1..3,
+                new: 1..4,
+            }]),
+            Patch(vec![Edit {
+                old: 0..0,
+                new: 0..4,
+            }]),
+            Patch(vec![
+                Edit {
+                    old: 0..0,
+                    new: 0..4,
+                },
+                Edit {
+                    old: 1..3,
+                    new: 5..8,
+                },
+            ]),
+        );
+
+        assert_patch_composition(
+            Patch(vec![Edit {
+                old: 1..3,
+                new: 1..4,
+            }]),
+            Patch(vec![Edit {
+                old: 5..9,
+                new: 5..7,
+            }]),
+            Patch(vec![
+                Edit {
+                    old: 1..3,
+                    new: 1..4,
+                },
+                Edit {
+                    old: 4..8,
+                    new: 5..7,
+                },
+            ]),
+        );
+    }
+
+    #[gpui::test]
+    fn test_one_overlapping_edit() {
+        assert_patch_composition(
+            Patch(vec![Edit {
+                old: 1..3,
+                new: 1..4,
+            }]),
+            Patch(vec![Edit {
+                old: 3..5,
+                new: 3..6,
+            }]),
+            Patch(vec![Edit {
+                old: 1..4,
+                new: 1..6,
+            }]),
+        );
+    }
+
+    #[gpui::test]
+    fn test_two_disjoint_and_overlapping() {
+        assert_patch_composition(
+            Patch(vec![
+                Edit {
+                    old: 1..3,
+                    new: 1..4,
+                },
+                Edit {
+                    old: 8..12,
+                    new: 9..11,
+                },
+            ]),
+            Patch(vec![
+                Edit {
+                    old: 0..0,
+                    new: 0..4,
+                },
+                Edit {
+                    old: 3..10,
+                    new: 7..9,
+                },
+            ]),
+            Patch(vec![
+                Edit {
+                    old: 0..0,
+                    new: 0..4,
+                },
+                Edit {
+                    old: 1..12,
+                    new: 5..10,
+                },
+            ]),
+        );
+    }
+
+    #[gpui::test]
+    fn test_two_new_edits_overlapping_one_old_edit() {
+        assert_patch_composition(
+            Patch(vec![Edit {
+                old: 0..0,
+                new: 0..3,
+            }]),
+            Patch(vec![
+                Edit {
+                    old: 0..0,
+                    new: 0..1,
+                },
+                Edit {
+                    old: 1..2,
+                    new: 2..2,
+                },
+            ]),
+            Patch(vec![Edit {
+                old: 0..0,
+                new: 0..3,
+            }]),
+        );
+
+        assert_patch_composition(
+            Patch(vec![Edit {
+                old: 2..3,
+                new: 2..4,
+            }]),
+            Patch(vec![
+                Edit {
+                    old: 0..2,
+                    new: 0..1,
+                },
+                Edit {
+                    old: 3..3,
+                    new: 2..5,
+                },
+            ]),
+            Patch(vec![Edit {
+                old: 0..3,
+                new: 0..6,
+            }]),
+        );
+
+        assert_patch_composition(
+            Patch(vec![Edit {
+                old: 0..0,
+                new: 0..2,
+            }]),
+            Patch(vec![
+                Edit {
+                    old: 0..0,
+                    new: 0..2,
+                },
+                Edit {
+                    old: 2..5,
+                    new: 4..4,
+                },
+            ]),
+            Patch(vec![Edit {
+                old: 0..3,
+                new: 0..4,
+            }]),
+        );
+    }
+
+    // #[test]
+    // fn test_compose_edits() {
+    //     assert_eq!(
+    //         compose_edits(
+    //             &Edit {
+    //                 old: 3..3,
+    //                 new: 3..6,
+    //             },
+    //             &Edit {
+    //                 old: 2..7,
+    //                 new: 2..4,
+    //             },
+    //         ),
+    //         Edit {
+    //             old: 2..4,
+    //             new: 2..4
+    //         }
+    //     );
+    // }
+
+    #[gpui::test]
+    fn test_two_new_edits_touching_one_old_edit() {
+        assert_patch_composition(
+            Patch(vec![
+                Edit {
+                    old: 2..3,
+                    new: 2..4,
+                },
+                Edit {
+                    old: 7..7,
+                    new: 8..11,
+                },
+            ]),
+            Patch(vec![
+                Edit {
+                    old: 2..3,
+                    new: 2..2,
+                },
+                Edit {
+                    old: 4..4,
+                    new: 3..4,
+                },
+            ]),
+            Patch(vec![
+                Edit {
+                    old: 2..3,
+                    new: 2..4,
+                },
+                Edit {
+                    old: 7..7,
+                    new: 8..11,
+                },
+            ]),
+        );
+    }
+
+    #[gpui::test(iterations = 100)]
+    fn test_random_patch_compositions(mut rng: StdRng) {
+        let operations = env::var("OPERATIONS")
+            .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
+            .unwrap_or(20);
+
+        let initial_chars = (0..rng.gen_range(0..=100))
+            .map(|_| rng.gen_range(b'a'..=b'z') as char)
+            .collect::<Vec<_>>();
+        log::info!("initial chars: {:?}", initial_chars);
+
+        // Generate two sequential patches
+        let mut patches = Vec::new();
+        let mut expected_chars = initial_chars.clone();
+        for i in 0..2 {
+            log::info!("patch {}:", i);
+
+            let mut delta = 0i32;
+            let mut last_edit_end = 0;
+            let mut edits = Vec::new();
+
+            for _ in 0..operations {
+                if last_edit_end >= expected_chars.len() {
+                    break;
+                }
+
+                let end = rng.gen_range(last_edit_end..=expected_chars.len());
+                let start = rng.gen_range(last_edit_end..=end);
+                let old_len = end - start;
+
+                let mut new_len = rng.gen_range(0..=3);
+                if start == end && new_len == 0 {
+                    new_len += 1;
+                }
+
+                last_edit_end = start + new_len + 1;
+
+                let new_chars = (0..new_len)
+                    .map(|_| rng.gen_range(b'A'..=b'Z') as char)
+                    .collect::<Vec<_>>();
+                log::info!(
+                    "  editing {:?}: {:?}",
+                    start..end,
+                    new_chars.iter().collect::<String>()
+                );
+                edits.push(Edit {
+                    old: (start as i32 - delta) as u32..(end as i32 - delta) as u32,
+                    new: start as u32..(start + new_len) as u32,
+                });
+                expected_chars.splice(start..end, new_chars);
+
+                delta += new_len as i32 - old_len as i32;
+            }
+
+            patches.push(Patch(edits));
+        }
+
+        log::info!("old patch: {:?}", &patches[0]);
+        log::info!("new patch: {:?}", &patches[1]);
+        log::info!("initial chars: {:?}", initial_chars);
+        log::info!("final chars: {:?}", expected_chars);
+
+        // Compose the patches, and verify that it has the same effect as applying the
+        // two patches separately.
+        let composed = patches[0].compose(&patches[1]);
+        log::info!("composed patch: {:?}", &composed);
+
+        let mut actual_chars = initial_chars.clone();
+        for edit in composed.0 {
+            actual_chars.splice(
+                edit.new.start as usize..edit.new.start as usize + edit.old.len(),
+                expected_chars[edit.new.start as usize..edit.new.end as usize]
+                    .iter()
+                    .copied(),
+            );
+        }
+
+        assert_eq!(actual_chars, expected_chars);
+    }
+
+    #[track_caller]
+    fn assert_patch_composition(old: Patch, new: Patch, composed: Patch) {
+        let original = ('a'..'z').collect::<Vec<_>>();
+        let inserted = ('A'..'Z').collect::<Vec<_>>();
+
+        let mut expected = original.clone();
+        apply_patch(&mut expected, &old, &inserted);
+        apply_patch(&mut expected, &new, &inserted);
+
+        let mut actual = original.clone();
+        apply_patch(&mut actual, &composed, &expected);
+        assert_eq!(
+            actual.into_iter().collect::<String>(),
+            expected.into_iter().collect::<String>(),
+            "expected patch is incorrect"
+        );
+
+        assert_eq!(old.compose(&new), composed);
+    }
+
+    fn apply_patch(text: &mut Vec<char>, patch: &Patch, new_text: &[char]) {
+        for edit in patch.0.iter().rev() {
+            text.splice(
+                edit.old.start as usize..edit.old.end as usize,
+                new_text[edit.new.start as usize..edit.new.end as usize]
+                    .iter()
+                    .copied(),
+            );
+        }
+    }
+}

crates/editor/src/display_map/tab_map.rs šŸ”—

@@ -1,8 +1,10 @@
-use super::fold_map::{self, FoldEdit, FoldPoint, Snapshot as FoldSnapshot};
-use language::{rope, HighlightedChunk};
+use super::fold_map::{self, FoldEdit, FoldPoint, Snapshot as FoldSnapshot, ToFoldPoint};
+use buffer::Point;
+use language::{rope, Chunk};
 use parking_lot::Mutex;
-use std::{mem, ops::Range};
+use std::{cmp, mem, ops::Range};
 use sum_tree::Bias;
+use theme::SyntaxTheme;
 
 pub struct TabMap(Mutex<Snapshot>);
 
@@ -21,6 +23,7 @@ impl TabMap {
         mut fold_edits: Vec<FoldEdit>,
     ) -> (Snapshot, Vec<Edit>) {
         let mut old_snapshot = self.0.lock();
+        let max_offset = old_snapshot.fold_snapshot.len();
         let new_snapshot = Snapshot {
             fold_snapshot,
             tab_size: old_snapshot.tab_size,
@@ -31,11 +34,11 @@ impl TabMap {
             let mut delta = 0;
             for chunk in old_snapshot
                 .fold_snapshot
-                .chunks_at(fold_edit.old_bytes.end)
+                .chunks(fold_edit.old_bytes.end..max_offset, None)
             {
                 let patterns: &[_] = &['\t', '\n'];
-                if let Some(ix) = chunk.find(patterns) {
-                    if &chunk[ix..ix + 1] == "\t" {
+                if let Some(ix) = chunk.text.find(patterns) {
+                    if &chunk.text[ix..ix + 1] == "\t" {
                         fold_edit.old_bytes.end.0 += delta + ix + 1;
                         fold_edit.new_bytes.end.0 += delta + ix + 1;
                     }
@@ -43,7 +46,7 @@ impl TabMap {
                     break;
                 }
 
-                delta += chunk.len();
+                delta += chunk.text.len();
             }
         }
 
@@ -108,28 +111,31 @@ impl Snapshot {
             .text_summary_for_range(input_start..input_end);
 
         let mut first_line_chars = 0;
-        let mut first_line_bytes = 0;
-        for c in self.chunks_at(range.start).flat_map(|chunk| chunk.chars()) {
-            if c == '\n'
-                || (range.start.row() == range.end.row() && first_line_bytes == range.end.column())
-            {
+        let line_end = if range.start.row() == range.end.row() {
+            range.end
+        } else {
+            self.max_point()
+        };
+        for c in self
+            .chunks(range.start..line_end, None)
+            .flat_map(|chunk| chunk.text.chars())
+        {
+            if c == '\n' {
                 break;
             }
             first_line_chars += 1;
-            first_line_bytes += c.len_utf8() as u32;
         }
 
         let mut last_line_chars = 0;
-        let mut last_line_bytes = 0;
-        for c in self
-            .chunks_at(TabPoint::new(range.end.row(), 0).max(range.start))
-            .flat_map(|chunk| chunk.chars())
-        {
-            if last_line_bytes == range.end.column() {
-                break;
+        if range.start.row() == range.end.row() {
+            last_line_chars = first_line_chars;
+        } else {
+            for _ in self
+                .chunks(TabPoint::new(range.end.row(), 0)..range.end, None)
+                .flat_map(|chunk| chunk.text.chars())
+            {
+                last_line_chars += 1;
             }
-            last_line_chars += 1;
-            last_line_bytes += c.len_utf8() as u32;
         }
 
         TextSummary {
@@ -145,21 +151,11 @@ impl Snapshot {
         self.fold_snapshot.version
     }
 
-    pub fn chunks_at(&self, point: TabPoint) -> Chunks {
-        let (point, expanded_char_column, to_next_stop) = self.to_fold_point(point, Bias::Left);
-        let fold_chunks = self
-            .fold_snapshot
-            .chunks_at(point.to_offset(&self.fold_snapshot));
-        Chunks {
-            fold_chunks,
-            column: expanded_char_column,
-            tab_size: self.tab_size,
-            chunk: &SPACES[0..to_next_stop],
-            skip_leading_tab: to_next_stop > 0,
-        }
-    }
-
-    pub fn highlighted_chunks(&mut self, range: Range<TabPoint>) -> HighlightedChunks {
+    pub fn chunks<'a>(
+        &'a self,
+        range: Range<TabPoint>,
+        theme: Option<&'a SyntaxTheme>,
+    ) -> Chunks<'a> {
         let (input_start, expanded_char_column, to_next_stop) =
             self.to_fold_point(range.start, Bias::Left);
         let input_start = input_start.to_offset(&self.fold_snapshot);
@@ -167,13 +163,19 @@ impl Snapshot {
             .to_fold_point(range.end, Bias::Right)
             .0
             .to_offset(&self.fold_snapshot);
-        HighlightedChunks {
-            fold_chunks: self
-                .fold_snapshot
-                .highlighted_chunks(input_start..input_end),
+        let to_next_stop = if range.start.0 + Point::new(0, to_next_stop as u32) > range.end.0 {
+            (range.end.column() - range.start.column()) as usize
+        } else {
+            to_next_stop
+        };
+
+        Chunks {
+            fold_chunks: self.fold_snapshot.chunks(input_start..input_end, theme),
             column: expanded_char_column,
+            output_position: range.start.0,
+            max_output_position: range.end.0,
             tab_size: self.tab_size,
-            chunk: HighlightedChunk {
+            chunk: Chunk {
                 text: &SPACES[0..to_next_stop],
                 ..Default::default()
             },
@@ -187,7 +189,9 @@ impl Snapshot {
 
     #[cfg(test)]
     pub fn text(&self) -> String {
-        self.chunks_at(Default::default()).collect()
+        self.chunks(TabPoint::zero()..self.max_point(), None)
+            .map(|chunk| chunk.text)
+            .collect()
     }
 
     pub fn max_point(&self) -> TabPoint {
@@ -207,6 +211,10 @@ impl Snapshot {
         TabPoint::new(input.row(), expanded as u32)
     }
 
+    pub fn from_point(&self, point: Point, bias: Bias) -> TabPoint {
+        self.to_tab_point(point.to_fold_point(&self.fold_snapshot, bias))
+    }
+
     pub fn to_fold_point(&self, output: TabPoint, bias: Bias) -> (FoldPoint, usize, usize) {
         let chars = self.fold_snapshot.chars_at(FoldPoint::new(output.row(), 0));
         let expanded = output.column() as usize;
@@ -219,6 +227,12 @@ impl Snapshot {
         )
     }
 
+    pub fn to_point(&self, point: TabPoint, bias: Bias) -> Point {
+        self.to_fold_point(point, bias)
+            .0
+            .to_buffer_point(&self.fold_snapshot)
+    }
+
     fn expand_tabs(chars: impl Iterator<Item = char>, column: usize, tab_size: usize) -> usize {
         let mut expanded_chars = 0;
         let mut expanded_bytes = 0;
@@ -368,63 +382,16 @@ const SPACES: &'static str = "                ";
 
 pub struct Chunks<'a> {
     fold_chunks: fold_map::Chunks<'a>,
-    chunk: &'a str,
+    chunk: Chunk<'a>,
     column: usize,
+    output_position: Point,
+    max_output_position: Point,
     tab_size: usize,
     skip_leading_tab: bool,
 }
 
 impl<'a> Iterator for Chunks<'a> {
-    type Item = &'a str;
-
-    fn next(&mut self) -> Option<Self::Item> {
-        if self.chunk.is_empty() {
-            if let Some(chunk) = self.fold_chunks.next() {
-                self.chunk = chunk;
-                if self.skip_leading_tab {
-                    self.chunk = &self.chunk[1..];
-                    self.skip_leading_tab = false;
-                }
-            } else {
-                return None;
-            }
-        }
-
-        for (ix, c) in self.chunk.char_indices() {
-            match c {
-                '\t' => {
-                    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
-    }
-}
-
-pub struct HighlightedChunks<'a> {
-    fold_chunks: fold_map::HighlightedChunks<'a>,
-    chunk: HighlightedChunk<'a>,
-    column: usize,
-    tab_size: usize,
-    skip_leading_tab: bool,
-}
-
-impl<'a> Iterator for HighlightedChunks<'a> {
-    type Item = HighlightedChunk<'a>;
+    type Item = Chunk<'a>;
 
     fn next(&mut self) -> Option<Self::Item> {
         if self.chunk.text.is_empty() {
@@ -445,22 +412,34 @@ impl<'a> Iterator for HighlightedChunks<'a> {
                     if ix > 0 {
                         let (prefix, suffix) = self.chunk.text.split_at(ix);
                         self.chunk.text = suffix;
-                        return Some(HighlightedChunk {
+                        return Some(Chunk {
                             text: prefix,
                             ..self.chunk
                         });
                     } else {
                         self.chunk.text = &self.chunk.text[1..];
-                        let len = self.tab_size - self.column % self.tab_size;
+                        let mut len = self.tab_size - self.column % self.tab_size;
+                        let next_output_position = cmp::min(
+                            self.output_position + Point::new(0, len as u32),
+                            self.max_output_position,
+                        );
+                        len = (next_output_position.column - self.output_position.column) as usize;
                         self.column += len;
-                        return Some(HighlightedChunk {
+                        self.output_position = next_output_position;
+                        return Some(Chunk {
                             text: &SPACES[0..len],
                             ..self.chunk
                         });
                     }
                 }
-                '\n' => self.column = 0,
-                _ => self.column += 1,
+                '\n' => {
+                    self.column = 0;
+                    self.output_position += Point::new(1, 0);
+                }
+                _ => {
+                    self.column += 1;
+                    self.output_position.column += c.len_utf8() as u32;
+                }
             }
         }
 
@@ -471,6 +450,10 @@ impl<'a> Iterator for HighlightedChunks<'a> {
 #[cfg(test)]
 mod tests {
     use super::*;
+    use crate::display_map::fold_map::FoldMap;
+    use buffer::{RandomCharIter, Rope};
+    use language::Buffer;
+    use rand::{prelude::StdRng, Rng};
 
     #[test]
     fn test_expand_tabs() {
@@ -478,4 +461,62 @@ mod tests {
         assert_eq!(Snapshot::expand_tabs("\t".chars(), 1, 4), 4);
         assert_eq!(Snapshot::expand_tabs("\ta".chars(), 2, 4), 5);
     }
+
+    #[gpui::test(iterations = 100)]
+    fn test_random(cx: &mut gpui::MutableAppContext, mut rng: StdRng) {
+        let tab_size = rng.gen_range(1..=4);
+        let buffer = cx.add_model(|cx| {
+            let len = rng.gen_range(0..30);
+            let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
+            Buffer::new(0, text, cx)
+        });
+        log::info!("Buffer text: {:?}", buffer.read(cx).text());
+
+        let (mut fold_map, _) = FoldMap::new(buffer.clone(), cx);
+        fold_map.randomly_mutate(&mut rng, cx);
+        let (folds_snapshot, _) = fold_map.read(cx);
+        log::info!("FoldMap text: {:?}", folds_snapshot.text());
+
+        let (_, tabs_snapshot) = TabMap::new(folds_snapshot.clone(), tab_size);
+        let text = Rope::from(tabs_snapshot.text().as_str());
+        log::info!(
+            "TabMap text (tab size: {}): {:?}",
+            tab_size,
+            tabs_snapshot.text(),
+        );
+
+        for _ in 0..5 {
+            let end_row = rng.gen_range(0..=text.max_point().row);
+            let end_column = rng.gen_range(0..=text.line_len(end_row));
+            let mut end = TabPoint(text.clip_point(Point::new(end_row, end_column), Bias::Right));
+            let start_row = rng.gen_range(0..=text.max_point().row);
+            let start_column = rng.gen_range(0..=text.line_len(start_row));
+            let mut start =
+                TabPoint(text.clip_point(Point::new(start_row, start_column), Bias::Left));
+            if start > end {
+                mem::swap(&mut start, &mut end);
+            }
+
+            let expected_text = text
+                .chunks_in_range(text.point_to_offset(start.0)..text.point_to_offset(end.0))
+                .collect::<String>();
+            let expected_summary = TextSummary::from(expected_text.as_str());
+            log::info!("slicing {:?}..{:?} (text: {:?})", start, end, text);
+            assert_eq!(
+                expected_text,
+                tabs_snapshot
+                    .chunks(start..end, None)
+                    .map(|c| c.text)
+                    .collect::<String>()
+            );
+
+            let mut actual_summary = tabs_snapshot.text_summary_for_range(start..end);
+            if tab_size > 1 && folds_snapshot.text().contains('\t') {
+                actual_summary.longest_row = expected_summary.longest_row;
+                actual_summary.longest_row_chars = expected_summary.longest_row_chars;
+            }
+
+            assert_eq!(actual_summary, expected_summary,);
+        }
+    }
 }

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

@@ -1,17 +1,28 @@
 use super::{
     fold_map,
-    tab_map::{self, Edit as TabEdit, Snapshot as TabSnapshot, TabPoint, TextSummary},
+    patch::Patch,
+    tab_map::{self, Edit as TabEdit, Snapshot as TabSnapshot, TabPoint},
+    DisplayRow,
 };
-use gpui::{fonts::FontId, text_layout::LineWrapper, Entity, ModelContext, Task};
-use language::{HighlightedChunk, Point};
+use gpui::{
+    fonts::FontId, text_layout::LineWrapper, Entity, ModelContext, ModelHandle, MutableAppContext,
+    Task,
+};
+use language::{Chunk, Point};
 use lazy_static::lazy_static;
 use smol::future::yield_now;
-use std::{collections::VecDeque, ops::Range, time::Duration};
+use std::{collections::VecDeque, mem, ops::Range, time::Duration};
 use sum_tree::{Bias, Cursor, SumTree};
+use theme::SyntaxTheme;
+
+pub use super::tab_map::TextSummary;
+pub type Edit = buffer::Edit<u32>;
 
 pub struct WrapMap {
     snapshot: Snapshot,
     pending_edits: VecDeque<(TabSnapshot, Vec<TabEdit>)>,
+    interpolated_edits: Patch,
+    edits_since_sync: Patch,
     wrap_width: Option<f32>,
     background_task: Option<Task<()>>,
     font: (FontId, f32),
@@ -41,18 +52,11 @@ struct TransformSummary {
 }
 
 #[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
-pub struct WrapPoint(super::Point);
+pub struct WrapPoint(pub super::Point);
 
 pub struct Chunks<'a> {
     input_chunks: tab_map::Chunks<'a>,
-    input_chunk: &'a str,
-    output_position: WrapPoint,
-    transforms: Cursor<'a, Transform, (WrapPoint, TabPoint)>,
-}
-
-pub struct HighlightedChunks<'a> {
-    input_chunks: tab_map::HighlightedChunks<'a>,
-    input_chunk: HighlightedChunk<'a>,
+    input_chunk: Chunk<'a>,
     output_position: WrapPoint,
     max_output_row: u32,
     transforms: Cursor<'a, Transform, (WrapPoint, TabPoint)>,
@@ -73,18 +77,24 @@ impl WrapMap {
         font_id: FontId,
         font_size: f32,
         wrap_width: Option<f32>,
-        cx: &mut ModelContext<Self>,
-    ) -> Self {
-        let mut this = Self {
-            font: (font_id, font_size),
-            wrap_width: None,
-            pending_edits: Default::default(),
-            snapshot: Snapshot::new(tab_snapshot),
-            background_task: None,
-        };
-        this.set_wrap_width(wrap_width, cx);
-
-        this
+        cx: &mut MutableAppContext,
+    ) -> (ModelHandle<Self>, Snapshot) {
+        let handle = cx.add_model(|cx| {
+            let mut this = Self {
+                font: (font_id, font_size),
+                wrap_width: None,
+                pending_edits: Default::default(),
+                interpolated_edits: Default::default(),
+                edits_since_sync: Default::default(),
+                snapshot: Snapshot::new(tab_snapshot),
+                background_task: None,
+            };
+            this.set_wrap_width(wrap_width, cx);
+            mem::take(&mut this.edits_since_sync);
+            this
+        });
+        let snapshot = handle.read(cx).snapshot.clone();
+        (handle, snapshot)
     }
 
     #[cfg(test)]
@@ -97,10 +107,13 @@ impl WrapMap {
         tab_snapshot: TabSnapshot,
         edits: Vec<TabEdit>,
         cx: &mut ModelContext<Self>,
-    ) -> Snapshot {
+    ) -> (Snapshot, Vec<Edit>) {
         self.pending_edits.push_back((tab_snapshot, edits));
         self.flush_edits(cx);
-        self.snapshot.clone()
+        (
+            self.snapshot.clone(),
+            mem::take(&mut self.edits_since_sync).into_inner(),
+        )
     }
 
     pub fn set_font(&mut self, font_id: FontId, font_size: f32, cx: &mut ModelContext<Self>) {
@@ -122,6 +135,8 @@ impl WrapMap {
 
     fn rewrap(&mut self, cx: &mut ModelContext<Self>) {
         self.background_task.take();
+        self.interpolated_edits.clear();
+        self.pending_edits.clear();
 
         if let Some(wrap_width) = self.wrap_width {
             let mut new_snapshot = self.snapshot.clone();
@@ -131,7 +146,7 @@ impl WrapMap {
                 let mut line_wrapper = font_cache.line_wrapper(font_id, font_size);
                 let tab_snapshot = new_snapshot.tab_snapshot.clone();
                 let range = TabPoint::zero()..tab_snapshot.max_point();
-                new_snapshot
+                let edits = new_snapshot
                     .update(
                         tab_snapshot,
                         &[TabEdit {
@@ -142,22 +157,27 @@ impl WrapMap {
                         &mut line_wrapper,
                     )
                     .await;
-                new_snapshot
+                (new_snapshot, edits)
             });
 
             match cx
                 .background()
                 .block_with_timeout(Duration::from_millis(5), task)
             {
-                Ok(snapshot) => {
+                Ok((snapshot, edits)) => {
                     self.snapshot = snapshot;
+                    self.edits_since_sync = self.edits_since_sync.compose(&edits);
                     cx.notify();
                 }
                 Err(wrap_task) => {
                     self.background_task = Some(cx.spawn(|this, mut cx| async move {
-                        let snapshot = wrap_task.await;
+                        let (snapshot, edits) = wrap_task.await;
                         this.update(&mut cx, |this, cx| {
                             this.snapshot = snapshot;
+                            this.edits_since_sync = this
+                                .edits_since_sync
+                                .compose(mem::take(&mut this.interpolated_edits).invert())
+                                .compose(&edits);
                             this.background_task = None;
                             this.flush_edits(cx);
                             cx.notify();
@@ -166,6 +186,7 @@ impl WrapMap {
                 }
             }
         } else {
+            let old_rows = self.snapshot.transforms.summary().output.lines.row + 1;
             self.snapshot.transforms = SumTree::new();
             let summary = self.snapshot.tab_snapshot.text_summary();
             if !summary.lines.is_zero() {
@@ -173,6 +194,14 @@ impl WrapMap {
                     .transforms
                     .push(Transform::isomorphic(summary), &());
             }
+            let new_rows = self.snapshot.transforms.summary().output.lines.row + 1;
+            self.snapshot.interpolated = false;
+            self.edits_since_sync = self.edits_since_sync.compose(&unsafe {
+                Patch::new_unchecked(vec![Edit {
+                    old: 0..old_rows,
+                    new: 0..new_rows,
+                }])
+            });
         }
     }
 
@@ -202,26 +231,33 @@ impl WrapMap {
                 let update_task = cx.background().spawn(async move {
                     let mut line_wrapper = font_cache.line_wrapper(font_id, font_size);
 
-                    for (tab_snapshot, edits) in pending_edits {
-                        snapshot
-                            .update(tab_snapshot, &edits, wrap_width, &mut line_wrapper)
+                    let mut edits = Patch::default();
+                    for (tab_snapshot, tab_edits) in pending_edits {
+                        let wrap_edits = snapshot
+                            .update(tab_snapshot, &tab_edits, wrap_width, &mut line_wrapper)
                             .await;
+                        edits = edits.compose(&wrap_edits);
                     }
-                    snapshot
+                    (snapshot, edits)
                 });
 
                 match cx
                     .background()
                     .block_with_timeout(Duration::from_millis(1), update_task)
                 {
-                    Ok(snapshot) => {
+                    Ok((snapshot, output_edits)) => {
                         self.snapshot = snapshot;
+                        self.edits_since_sync = self.edits_since_sync.compose(&output_edits);
                     }
                     Err(update_task) => {
                         self.background_task = Some(cx.spawn(|this, mut cx| async move {
-                            let snapshot = update_task.await;
+                            let (snapshot, edits) = update_task.await;
                             this.update(&mut cx, |this, cx| {
                                 this.snapshot = snapshot;
+                                this.edits_since_sync = this
+                                    .edits_since_sync
+                                    .compose(mem::take(&mut this.interpolated_edits).invert())
+                                    .compose(&edits);
                                 this.background_task = None;
                                 this.flush_edits(cx);
                                 cx.notify();
@@ -238,7 +274,9 @@ impl WrapMap {
             if tab_snapshot.version() <= self.snapshot.tab_snapshot.version() {
                 to_remove_len += 1;
             } else {
-                self.snapshot.interpolate(tab_snapshot.clone(), &edits);
+                let interpolated_edits = self.snapshot.interpolate(tab_snapshot.clone(), &edits);
+                self.edits_since_sync = self.edits_since_sync.compose(&interpolated_edits);
+                self.interpolated_edits = self.interpolated_edits.compose(&interpolated_edits);
             }
         }
 
@@ -262,17 +300,21 @@ impl Snapshot {
         }
     }
 
-    fn interpolate(&mut self, new_tab_snapshot: TabSnapshot, edits: &[TabEdit]) {
+    fn interpolate(&mut self, new_tab_snapshot: TabSnapshot, tab_edits: &[TabEdit]) -> Patch {
         let mut new_transforms;
-        if edits.is_empty() {
+        if tab_edits.is_empty() {
             new_transforms = self.transforms.clone();
         } else {
             let mut old_cursor = self.transforms.cursor::<TabPoint>();
-            let mut edits = edits.into_iter().peekable();
-            new_transforms =
-                old_cursor.slice(&edits.peek().unwrap().old_lines.start, Bias::Right, &());
 
-            while let Some(edit) = edits.next() {
+            let mut tab_edits_iter = tab_edits.iter().peekable();
+            new_transforms = old_cursor.slice(
+                &tab_edits_iter.peek().unwrap().old_lines.start,
+                Bias::Right,
+                &(),
+            );
+
+            while let Some(edit) = tab_edits_iter.next() {
                 if edit.new_lines.start > TabPoint::from(new_transforms.summary().input.lines) {
                     let summary = new_tab_snapshot.text_summary_for_range(
                         TabPoint::from(new_transforms.summary().input.lines)..edit.new_lines.start,
@@ -287,7 +329,7 @@ impl Snapshot {
                 }
 
                 old_cursor.seek_forward(&edit.old_lines.end, Bias::Right, &());
-                if let Some(next_edit) = edits.peek() {
+                if let Some(next_edit) = tab_edits_iter.peek() {
                     if next_edit.old_lines.start > old_cursor.end(&()) {
                         if old_cursor.end(&()) > edit.old_lines.end {
                             let summary = self
@@ -295,6 +337,7 @@ impl Snapshot {
                                 .text_summary_for_range(edit.old_lines.end..old_cursor.end(&()));
                             new_transforms.push_or_extend(Transform::isomorphic(summary));
                         }
+
                         old_cursor.next(&());
                         new_transforms.push_tree(
                             old_cursor.slice(&next_edit.old_lines.start, Bias::Right, &()),
@@ -314,38 +357,44 @@ impl Snapshot {
             }
         }
 
-        self.transforms = new_transforms;
-        self.tab_snapshot = new_tab_snapshot;
-        self.interpolated = true;
+        let old_snapshot = mem::replace(
+            self,
+            Snapshot {
+                tab_snapshot: new_tab_snapshot,
+                transforms: new_transforms,
+                interpolated: true,
+            },
+        );
         self.check_invariants();
+        old_snapshot.compute_edits(tab_edits, self)
     }
 
     async fn update(
         &mut self,
         new_tab_snapshot: TabSnapshot,
-        edits: &[TabEdit],
+        tab_edits: &[TabEdit],
         wrap_width: f32,
         line_wrapper: &mut LineWrapper,
-    ) {
+    ) -> Patch {
         #[derive(Debug)]
         struct RowEdit {
             old_rows: Range<u32>,
             new_rows: Range<u32>,
         }
 
-        let mut edits = edits.into_iter().peekable();
+        let mut tab_edits_iter = tab_edits.into_iter().peekable();
         let mut row_edits = Vec::new();
-        while let Some(edit) = edits.next() {
+        while let Some(edit) = tab_edits_iter.next() {
             let mut row_edit = RowEdit {
                 old_rows: edit.old_lines.start.row()..edit.old_lines.end.row() + 1,
                 new_rows: edit.new_lines.start.row()..edit.new_lines.end.row() + 1,
             };
 
-            while let Some(next_edit) = edits.peek() {
+            while let Some(next_edit) = tab_edits_iter.peek() {
                 if next_edit.old_lines.start.row() <= row_edit.old_rows.end {
                     row_edit.old_rows.end = next_edit.old_lines.end.row() + 1;
                     row_edit.new_rows.end = next_edit.new_lines.end.row() + 1;
-                    edits.next();
+                    tab_edits_iter.next();
                 } else {
                     break;
                 }
@@ -370,7 +419,7 @@ impl Snapshot {
             while let Some(edit) = row_edits.next() {
                 if edit.new_rows.start > new_transforms.summary().input.lines.row {
                     let summary = new_tab_snapshot.text_summary_for_range(
-                        TabPoint::new(new_transforms.summary().input.lines.row, 0)
+                        TabPoint(new_transforms.summary().input.lines)
                             ..TabPoint::new(edit.new_rows.start, 0),
                     );
                     new_transforms.push_or_extend(Transform::isomorphic(summary));
@@ -378,10 +427,15 @@ impl Snapshot {
 
                 let mut line = String::new();
                 let mut remaining = None;
-                let mut chunks = new_tab_snapshot.chunks_at(TabPoint::new(edit.new_rows.start, 0));
+                let mut chunks = new_tab_snapshot.chunks(
+                    TabPoint::new(edit.new_rows.start, 0)..new_tab_snapshot.max_point(),
+                    None,
+                );
                 let mut edit_transforms = Vec::<Transform>::new();
                 for _ in edit.new_rows.start..edit.new_rows.end {
-                    while let Some(chunk) = remaining.take().or_else(|| chunks.next()) {
+                    while let Some(chunk) =
+                        remaining.take().or_else(|| chunks.next().map(|c| c.text))
+                    {
                         if let Some(ix) = chunk.find('\n') {
                             line.push_str(&chunk[..ix + 1]);
                             remaining = Some(&chunk[ix + 1..]);
@@ -452,30 +506,60 @@ impl Snapshot {
             }
         }
 
-        self.transforms = new_transforms;
-        self.tab_snapshot = new_tab_snapshot;
-        self.interpolated = false;
+        let old_snapshot = mem::replace(
+            self,
+            Snapshot {
+                tab_snapshot: new_tab_snapshot,
+                transforms: new_transforms,
+                interpolated: false,
+            },
+        );
         self.check_invariants();
+        old_snapshot.compute_edits(tab_edits, self)
     }
 
-    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 mut input_position = TabPoint(transforms.start().1 .0);
-        if transforms.item().map_or(false, |t| t.is_isomorphic()) {
-            input_position.0 += point.0 - transforms.start().0 .0;
-        }
-        let input_chunks = self.tab_snapshot.chunks_at(input_position);
-        Chunks {
-            input_chunks,
-            transforms,
-            output_position: point,
-            input_chunk: "",
+    fn compute_edits(&self, tab_edits: &[TabEdit], new_snapshot: &Snapshot) -> Patch {
+        let mut wrap_edits = Vec::new();
+        let mut old_cursor = self.transforms.cursor::<TransformSummary>();
+        let mut new_cursor = new_snapshot.transforms.cursor::<TransformSummary>();
+        for mut tab_edit in tab_edits.iter().cloned() {
+            tab_edit.old_lines.start.0.column = 0;
+            tab_edit.old_lines.end.0 += Point::new(1, 0);
+            tab_edit.new_lines.start.0.column = 0;
+            tab_edit.new_lines.end.0 += Point::new(1, 0);
+
+            old_cursor.seek(&tab_edit.old_lines.start, Bias::Right, &());
+            let mut old_start = old_cursor.start().output.lines;
+            old_start += tab_edit.old_lines.start.0 - old_cursor.start().input.lines;
+
+            old_cursor.seek(&tab_edit.old_lines.end, Bias::Right, &());
+            let mut old_end = old_cursor.start().output.lines;
+            old_end += tab_edit.old_lines.end.0 - old_cursor.start().input.lines;
+
+            new_cursor.seek(&tab_edit.new_lines.start, Bias::Right, &());
+            let mut new_start = new_cursor.start().output.lines;
+            new_start += tab_edit.new_lines.start.0 - new_cursor.start().input.lines;
+
+            new_cursor.seek(&tab_edit.new_lines.end, Bias::Right, &());
+            let mut new_end = new_cursor.start().output.lines;
+            new_end += tab_edit.new_lines.end.0 - new_cursor.start().input.lines;
+
+            wrap_edits.push(Edit {
+                old: old_start.row..old_end.row,
+                new: new_start.row..new_end.row,
+            });
         }
+
+        consolidate_wrap_edits(&mut wrap_edits);
+        unsafe { Patch::new_unchecked(wrap_edits) }
+    }
+
+    pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator<Item = &str> {
+        self.chunks(wrap_row..self.max_point().row() + 1, None)
+            .map(|h| h.text)
     }
 
-    pub fn highlighted_chunks_for_rows(&mut self, rows: Range<u32>) -> HighlightedChunks {
+    pub fn chunks<'a>(&'a self, rows: Range<u32>, theme: Option<&'a SyntaxTheme>) -> Chunks<'a> {
         let output_start = WrapPoint::new(rows.start, 0);
         let output_end = WrapPoint::new(rows.end, 0);
         let mut transforms = self.transforms.cursor::<(WrapPoint, TabPoint)>();
@@ -487,8 +571,8 @@ impl Snapshot {
         let input_end = self
             .to_tab_point(output_end)
             .min(self.tab_snapshot.max_point());
-        HighlightedChunks {
-            input_chunks: self.tab_snapshot.highlighted_chunks(input_start..input_end),
+        Chunks {
+            input_chunks: self.tab_snapshot.chunks(input_start..input_end, theme),
             input_chunk: Default::default(),
             output_position: output_start,
             max_output_row: rows.end,
@@ -496,13 +580,17 @@ impl Snapshot {
         }
     }
 
+    pub fn text_summary(&self) -> TextSummary {
+        self.transforms.summary().output
+    }
+
     pub fn max_point(&self) -> WrapPoint {
-        self.to_wrap_point(self.tab_snapshot.max_point())
+        WrapPoint(self.transforms.summary().output.lines)
     }
 
     pub fn line_len(&self, row: u32) -> u32 {
         let mut len = 0;
-        for chunk in self.chunks_at(row) {
+        for chunk in self.text_chunks(row) {
             if let Some(newline_ix) = chunk.find('\n') {
                 len += newline_ix;
                 break;
@@ -513,6 +601,13 @@ impl Snapshot {
         len as u32
     }
 
+    pub fn line_char_count(&self, row: u32) -> u32 {
+        self.text_chunks(row)
+            .flat_map(|c| c.chars())
+            .take_while(|c| *c != '\n')
+            .count() as u32
+    }
+
     pub fn soft_wrap_indent(&self, row: u32) -> Option<u32> {
         let mut cursor = self.transforms.cursor::<WrapPoint>();
         cursor.seek(&WrapPoint::new(row + 1, 0), Bias::Right, &());
@@ -559,7 +654,15 @@ impl Snapshot {
         TabPoint(tab_point)
     }
 
-    pub fn to_wrap_point(&self, point: TabPoint) -> WrapPoint {
+    pub fn to_point(&self, point: WrapPoint, bias: Bias) -> Point {
+        self.tab_snapshot.to_point(self.to_tab_point(point), bias)
+    }
+
+    pub fn from_point(&self, point: Point, bias: Bias) -> WrapPoint {
+        self.from_tab_point(self.tab_snapshot.from_point(point, bias))
+    }
+
+    pub fn from_tab_point(&self, point: TabPoint) -> WrapPoint {
         let mut cursor = self.transforms.cursor::<(TabPoint, WrapPoint)>();
         cursor.seek(&point, Bias::Right, &());
         WrapPoint(cursor.start().1 .0 + (point.0 - cursor.start().0 .0))
@@ -575,7 +678,7 @@ impl Snapshot {
             }
         }
 
-        self.to_wrap_point(self.tab_snapshot.clip_point(self.to_tab_point(point), bias))
+        self.from_tab_point(self.tab_snapshot.clip_point(self.to_tab_point(point), bias))
     }
 
     fn check_invariants(&self) {
@@ -610,7 +713,11 @@ impl Snapshot {
                     prev_tab_row = tab_point.row();
                     soft_wrapped = false;
                 }
-                expected_buffer_rows.push((buffer_row, soft_wrapped));
+                expected_buffer_rows.push(if soft_wrapped {
+                    DisplayRow::Wrap
+                } else {
+                    DisplayRow::Buffer(buffer_row)
+                });
             }
 
             for start_display_row in 0..expected_buffer_rows.len() {
@@ -627,52 +734,7 @@ impl Snapshot {
 }
 
 impl<'a> Iterator for Chunks<'a> {
-    type Item = &'a str;
-
-    fn next(&mut self) -> Option<Self::Item> {
-        let transform = self.transforms.item()?;
-        if let Some(display_text) = transform.display_text {
-            if self.output_position > self.transforms.start().0 {
-                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() {
-            self.input_chunk = self.input_chunks.next().unwrap();
-        }
-
-        let mut input_len = 0;
-        let transform_end = self.transforms.end(&()).0;
-        for c in self.input_chunk.chars() {
-            let char_len = c.len_utf8();
-            input_len += char_len;
-            if c == '\n' {
-                *self.output_position.row_mut() += 1;
-                *self.output_position.column_mut() = 0;
-            } else {
-                *self.output_position.column_mut() += char_len as u32;
-            }
-
-            if self.output_position >= transform_end {
-                self.transforms.next(&());
-                break;
-            }
-        }
-
-        let (prefix, suffix) = self.input_chunk.split_at(input_len);
-        self.input_chunk = suffix;
-        Some(prefix)
-    }
-}
-
-impl<'a> Iterator for HighlightedChunks<'a> {
-    type Item = HighlightedChunk<'a>;
+    type Item = Chunk<'a>;
 
     fn next(&mut self) -> Option<Self::Item> {
         if self.output_position.row() >= self.max_output_row {
@@ -697,7 +759,7 @@ impl<'a> Iterator for HighlightedChunks<'a> {
 
             self.output_position.0 += summary;
             self.transforms.next(&());
-            return Some(HighlightedChunk {
+            return Some(Chunk {
                 text: &display_text[start_ix..end_ix],
                 ..self.input_chunk
             });
@@ -727,7 +789,7 @@ impl<'a> Iterator for HighlightedChunks<'a> {
 
         let (prefix, suffix) = self.input_chunk.text.split_at(input_len);
         self.input_chunk.text = suffix;
-        Some(HighlightedChunk {
+        Some(Chunk {
             text: prefix,
             ..self.input_chunk
         })
@@ -735,7 +797,7 @@ impl<'a> Iterator for HighlightedChunks<'a> {
 }
 
 impl<'a> Iterator for BufferRows<'a> {
-    type Item = (u32, bool);
+    type Item = DisplayRow;
 
     fn next(&mut self) -> Option<Self::Item> {
         if self.output_row > self.max_output_row {
@@ -755,7 +817,11 @@ impl<'a> Iterator for BufferRows<'a> {
             self.soft_wrapped = true;
         }
 
-        Some((buffer_row, soft_wrapped))
+        Some(if soft_wrapped {
+            DisplayRow::Wrap
+        } else {
+            DisplayRow::Buffer(buffer_row)
+        })
     }
 }
 
@@ -851,23 +917,18 @@ impl WrapPoint {
         Self(super::Point::new(row, column))
     }
 
-    #[cfg(test)]
-    pub fn is_zero(&self) -> bool {
-        self.0.is_zero()
-    }
-
     pub fn row(self) -> u32 {
         self.0.row
     }
 
-    pub fn column(self) -> u32 {
-        self.0.column
-    }
-
     pub fn row_mut(&mut self) -> &mut u32 {
         &mut self.0.row
     }
 
+    pub fn column(&self) -> u32 {
+        self.0.column
+    }
+
     pub fn column_mut(&mut self) -> &mut u32 {
         &mut self.0.column
     }
@@ -888,12 +949,33 @@ impl<'a> sum_tree::Dimension<'a, TransformSummary> for TabPoint {
     }
 }
 
+impl<'a> sum_tree::SeekTarget<'a, TransformSummary, TransformSummary> for TabPoint {
+    fn cmp(&self, cursor_location: &TransformSummary, _: &()) -> std::cmp::Ordering {
+        Ord::cmp(&self.0, &cursor_location.input.lines)
+    }
+}
+
 impl<'a> sum_tree::Dimension<'a, TransformSummary> for WrapPoint {
     fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
         self.0 += summary.output.lines;
     }
 }
 
+fn consolidate_wrap_edits(edits: &mut Vec<Edit>) {
+    let mut i = 1;
+    while i < edits.len() {
+        let edit = edits[i].clone();
+        let prev_edit = &mut edits[i - 1];
+        if prev_edit.old.end >= edit.old.start {
+            prev_edit.old.end = edit.old.end;
+            prev_edit.new.end = edit.new.end;
+            edits.remove(i);
+            continue;
+        }
+        i += 1;
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -901,9 +983,10 @@ mod tests {
         display_map::{fold_map::FoldMap, tab_map::TabMap},
         test::Observer,
     };
+    use buffer::Rope;
     use language::{Buffer, RandomCharIter};
     use rand::prelude::*;
-    use std::env;
+    use std::{cmp, env};
 
     #[gpui::test(iterations = 100)]
     async fn test_random_wraps(mut cx: gpui::TestAppContext, mut rng: StdRng) {
@@ -951,17 +1034,20 @@ mod tests {
         let unwrapped_text = tabs_snapshot.text();
         let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper);
 
-        let wrap_map = cx.add_model(|cx| {
-            WrapMap::new(tabs_snapshot.clone(), font_id, font_size, wrap_width, cx)
-        });
+        let (wrap_map, _) =
+            cx.update(|cx| WrapMap::new(tabs_snapshot.clone(), font_id, font_size, wrap_width, cx));
         let (_observer, notifications) = Observer::new(&wrap_map, &mut cx);
 
         if wrap_map.read_with(&cx, |map, _| map.is_rewrapping()) {
             notifications.recv().await.unwrap();
         }
 
-        let snapshot = wrap_map.update(&mut cx, |map, cx| map.sync(tabs_snapshot, Vec::new(), cx));
-        let actual_text = snapshot.text();
+        let (initial_snapshot, _) = wrap_map.update(&mut cx, |map, cx| {
+            assert!(!map.is_rewrapping());
+            map.sync(tabs_snapshot.clone(), Vec::new(), cx)
+        });
+
+        let actual_text = initial_snapshot.text();
         assert_eq!(
             actual_text, expected_text,
             "unwrapped text is: {:?}",
@@ -969,7 +1055,10 @@ mod tests {
         );
         log::info!("Wrapped text: {:?}", actual_text);
 
+        let mut edits = Vec::new();
         for _i in 0..operations {
+            log::info!("{} ==============================================", _i);
+
             match rng.gen_range(0..=100) {
                 0..=19 => {
                     wrap_width = if rng.gen_bool(0.2) {
@@ -981,14 +1070,15 @@ mod tests {
                     wrap_map.update(&mut cx, |map, cx| map.set_wrap_width(wrap_width, cx));
                 }
                 20..=39 => {
-                    for (folds_snapshot, edits) in
+                    for (folds_snapshot, fold_edits) in
                         cx.read(|cx| fold_map.randomly_mutate(&mut rng, cx))
                     {
-                        let (tabs_snapshot, edits) = tab_map.sync(folds_snapshot, edits);
-                        let mut snapshot =
-                            wrap_map.update(&mut cx, |map, cx| map.sync(tabs_snapshot, edits, cx));
+                        let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits);
+                        let (mut snapshot, wrap_edits) = wrap_map
+                            .update(&mut cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx));
                         snapshot.check_invariants();
                         snapshot.verify_chunks(&mut rng);
+                        edits.push((snapshot, wrap_edits));
                     }
                 }
                 _ => {
@@ -1000,21 +1090,22 @@ mod tests {
                 "Unwrapped text (no folds): {:?}",
                 buffer.read_with(&cx, |buf, _| buf.text())
             );
-            let (folds_snapshot, edits) = cx.read(|cx| fold_map.read(cx));
+            let (folds_snapshot, fold_edits) = cx.read(|cx| fold_map.read(cx));
             log::info!(
                 "Unwrapped text (unexpanded tabs): {:?}",
                 folds_snapshot.text()
             );
-            let (tabs_snapshot, edits) = tab_map.sync(folds_snapshot, edits);
+            let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits);
             log::info!("Unwrapped text (expanded tabs): {:?}", tabs_snapshot.text());
 
             let unwrapped_text = tabs_snapshot.text();
             let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper);
-            let mut snapshot = wrap_map.update(&mut cx, |map, cx| {
-                map.sync(tabs_snapshot.clone(), edits, cx)
+            let (mut snapshot, wrap_edits) = wrap_map.update(&mut cx, |map, cx| {
+                map.sync(tabs_snapshot.clone(), tab_edits, cx)
             });
             snapshot.check_invariants();
             snapshot.verify_chunks(&mut rng);
+            edits.push((snapshot, wrap_edits));
 
             if wrap_map.read_with(&cx, |map, _| map.is_rewrapping()) && rng.gen_bool(0.4) {
                 log::info!("Waiting for wrapping to finish");
@@ -1024,18 +1115,83 @@ mod tests {
             }
 
             if !wrap_map.read_with(&cx, |map, _| map.is_rewrapping()) {
-                let mut wrapped_snapshot =
+                let (mut wrapped_snapshot, wrap_edits) =
                     wrap_map.update(&mut cx, |map, cx| map.sync(tabs_snapshot, Vec::new(), cx));
                 let actual_text = wrapped_snapshot.text();
+                let actual_longest_row = wrapped_snapshot.longest_row();
                 log::info!("Wrapping finished: {:?}", actual_text);
                 wrapped_snapshot.check_invariants();
                 wrapped_snapshot.verify_chunks(&mut rng);
+                edits.push((wrapped_snapshot.clone(), wrap_edits));
                 assert_eq!(
                     actual_text, expected_text,
                     "unwrapped text is: {:?}",
                     unwrapped_text
                 );
+
+                let mut summary = TextSummary::default();
+                for (ix, item) in wrapped_snapshot
+                    .transforms
+                    .items(&())
+                    .into_iter()
+                    .enumerate()
+                {
+                    summary += &item.summary.output;
+                    log::info!("{} summary: {:?}", ix, item.summary.output,);
+                }
+
+                if tab_size == 1
+                    || !wrapped_snapshot
+                        .tab_snapshot
+                        .fold_snapshot
+                        .text()
+                        .contains('\t')
+                {
+                    let mut expected_longest_rows = Vec::new();
+                    let mut longest_line_len = -1;
+                    for (row, line) in expected_text.split('\n').enumerate() {
+                        let line_char_count = line.chars().count() as isize;
+                        if line_char_count > longest_line_len {
+                            expected_longest_rows.clear();
+                            longest_line_len = line_char_count;
+                        }
+                        if line_char_count >= longest_line_len {
+                            expected_longest_rows.push(row as u32);
+                        }
+                    }
+
+                    assert!(
+                        expected_longest_rows.contains(&actual_longest_row),
+                        "incorrect longest row {}. expected {:?} with length {}",
+                        actual_longest_row,
+                        expected_longest_rows,
+                        longest_line_len,
+                    )
+                }
+            }
+        }
+
+        let mut initial_text = Rope::from(initial_snapshot.text().as_str());
+        for (snapshot, patch) in edits {
+            let snapshot_text = Rope::from(snapshot.text().as_str());
+            for edit in &patch {
+                let old_start = initial_text.point_to_offset(Point::new(edit.new.start, 0));
+                let old_end = initial_text.point_to_offset(cmp::min(
+                    Point::new(edit.new.start + edit.old.len() as u32, 0),
+                    initial_text.max_point(),
+                ));
+                let new_start = snapshot_text.point_to_offset(Point::new(edit.new.start, 0));
+                let new_end = snapshot_text.point_to_offset(cmp::min(
+                    Point::new(edit.new.end, 0),
+                    snapshot_text.max_point(),
+                ));
+                let new_text = snapshot_text
+                    .chunks_in_range(new_start..new_end)
+                    .collect::<String>();
+
+                initial_text.replace(old_start..old_end, &new_text);
             }
+            assert_eq!(initial_text.to_string(), snapshot_text.to_string());
         }
     }
 
@@ -1067,8 +1223,8 @@ mod tests {
     }
 
     impl Snapshot {
-        fn text(&self) -> String {
-            self.chunks_at(0).collect()
+        pub fn text(&self) -> String {
+            self.text_chunks(0).collect()
         }
 
         fn verify_chunks(&mut self, rng: &mut impl Rng) {
@@ -1077,7 +1233,7 @@ mod tests {
                 let start_row = rng.gen_range(0..=end_row);
                 end_row += 1;
 
-                let mut expected_text = self.chunks_at(start_row).collect::<String>();
+                let mut expected_text = self.text_chunks(start_row).collect::<String>();
                 if expected_text.ends_with("\n") {
                     expected_text.push('\n');
                 }
@@ -1091,7 +1247,7 @@ mod tests {
                 }
 
                 let actual_text = self
-                    .highlighted_chunks_for_rows(start_row..end_row)
+                    .chunks(start_row..end_row, None)
                     .map(|c| c.text)
                     .collect::<String>();
                 assert_eq!(

crates/editor/src/element.rs šŸ”—

@@ -1,6 +1,6 @@
 use super::{
-    DisplayPoint, Editor, EditorMode, EditorSettings, EditorStyle, Input, Scroll, Select,
-    SelectPhase, Snapshot, MAX_LINE_LEN,
+    DisplayPoint, DisplayRow, Editor, EditorMode, EditorSettings, EditorStyle, Input, Scroll,
+    Select, SelectPhase, Snapshot, MAX_LINE_LEN,
 };
 use clock::ReplicaId;
 use gpui::{
@@ -17,7 +17,7 @@ use gpui::{
     MutableAppContext, PaintContext, Quad, Scene, SizeConstraint, ViewContext, WeakViewHandle,
 };
 use json::json;
-use language::{DiagnosticSeverity, HighlightedChunk};
+use language::Chunk;
 use smallvec::SmallVec;
 use std::{
     cmp::{self, Ordering},
@@ -25,6 +25,7 @@ use std::{
     fmt::Write,
     ops::Range,
 };
+use theme::BlockStyle;
 
 pub struct EditorElement {
     view: WeakViewHandle<Editor>,
@@ -195,6 +196,7 @@ impl EditorElement {
     ) {
         let bounds = gutter_bounds.union_rect(text_bounds);
         let scroll_top = layout.snapshot.scroll_position().y() * layout.line_height;
+        let start_row = layout.snapshot.scroll_position().y() as u32;
         let editor = self.view(cx.app);
         let style = &self.settings.style;
         cx.scene.push_quad(Quad {
@@ -239,6 +241,51 @@ impl EditorElement {
                 }
             }
         }
+
+        // Draw block backgrounds
+        for (ixs, block_style) in &layout.block_layouts {
+            let row = start_row + ixs.start;
+            let offset = vec2f(0., row as f32 * layout.line_height - scroll_top);
+            let height = ixs.len() as f32 * layout.line_height;
+            cx.scene.push_quad(Quad {
+                bounds: RectF::new(
+                    text_bounds.origin() + offset,
+                    vec2f(text_bounds.width(), height),
+                ),
+                background: block_style.background,
+                border: block_style
+                    .border
+                    .map_or(Default::default(), |color| Border {
+                        width: 1.,
+                        color,
+                        overlay: true,
+                        top: true,
+                        right: false,
+                        bottom: true,
+                        left: false,
+                    }),
+                corner_radius: 0.,
+            });
+            cx.scene.push_quad(Quad {
+                bounds: RectF::new(
+                    gutter_bounds.origin() + offset,
+                    vec2f(gutter_bounds.width(), height),
+                ),
+                background: block_style.gutter_background,
+                border: block_style
+                    .gutter_border
+                    .map_or(Default::default(), |color| Border {
+                        width: 1.,
+                        color,
+                        overlay: true,
+                        top: true,
+                        right: false,
+                        bottom: true,
+                        left: false,
+                    }),
+                corner_radius: 0.,
+            });
+        }
     }
 
     fn paint_gutter(
@@ -401,18 +448,24 @@ impl EditorElement {
             .width()
     }
 
-    fn layout_line_numbers(
+    fn layout_rows(
         &self,
         rows: Range<u32>,
         active_rows: &BTreeMap<u32, bool>,
         snapshot: &Snapshot,
         cx: &LayoutContext,
-    ) -> Vec<Option<text_layout::Line>> {
+    ) -> (
+        Vec<Option<text_layout::Line>>,
+        Vec<(Range<u32>, BlockStyle)>,
+    ) {
         let style = &self.settings.style;
-        let mut layouts = Vec::with_capacity(rows.len());
+        let include_line_numbers = snapshot.mode == EditorMode::Full;
+        let mut last_block_id = None;
+        let mut blocks = Vec::<(Range<u32>, BlockStyle)>::new();
+        let mut line_number_layouts = Vec::with_capacity(rows.len());
         let mut line_number = String::new();
-        for (ix, (buffer_row, soft_wrapped)) in snapshot
-            .buffer_rows(rows.start)
+        for (ix, row) in snapshot
+            .buffer_rows(rows.start, cx)
             .take((rows.end - rows.start) as usize)
             .enumerate()
         {
@@ -422,27 +475,46 @@ impl EditorElement {
             } else {
                 style.line_number
             };
-            if soft_wrapped {
-                layouts.push(None);
-            } else {
-                line_number.clear();
-                write!(&mut line_number, "{}", buffer_row + 1).unwrap();
-                layouts.push(Some(cx.text_layout_cache.layout_str(
-                    &line_number,
-                    style.text.font_size,
-                    &[(
-                        line_number.len(),
-                        RunStyle {
-                            font_id: style.text.font_id,
-                            color,
-                            underline: None,
-                        },
-                    )],
-                )));
+            match row {
+                DisplayRow::Buffer(buffer_row) => {
+                    if include_line_numbers {
+                        line_number.clear();
+                        write!(&mut line_number, "{}", buffer_row + 1).unwrap();
+                        line_number_layouts.push(Some(cx.text_layout_cache.layout_str(
+                            &line_number,
+                            style.text.font_size,
+                            &[(
+                                line_number.len(),
+                                RunStyle {
+                                    font_id: style.text.font_id,
+                                    color,
+                                    underline: None,
+                                },
+                            )],
+                        )));
+                    }
+                    last_block_id = None;
+                }
+                DisplayRow::Block(block_id, style) => {
+                    let ix = ix as u32;
+                    if last_block_id == Some(block_id) {
+                        if let Some((row_range, _)) = blocks.last_mut() {
+                            row_range.end += 1;
+                        }
+                    } else if let Some(style) = style {
+                        blocks.push((ix..ix + 1, style));
+                    }
+                    line_number_layouts.push(None);
+                    last_block_id = Some(block_id);
+                }
+                DisplayRow::Wrap => {
+                    line_number_layouts.push(None);
+                    last_block_id = None;
+                }
             }
         }
 
-        layouts
+        (line_number_layouts, blocks)
     }
 
     fn layout_lines(
@@ -493,9 +565,9 @@ impl EditorElement {
         let mut styles = Vec::new();
         let mut row = rows.start;
         let mut line_exceeded_max_len = false;
-        let chunks = snapshot.highlighted_chunks_for_rows(rows.clone());
+        let chunks = snapshot.chunks(rows.clone(), Some(&style.syntax), cx);
 
-        let newline_chunk = HighlightedChunk {
+        let newline_chunk = Chunk {
             text: "\n",
             ..Default::default()
         };
@@ -517,10 +589,8 @@ impl EditorElement {
                 }
 
                 if !line_chunk.is_empty() && !line_exceeded_max_len {
-                    let highlight_style = chunk
-                        .highlight_id
-                        .style(&style.syntax)
-                        .unwrap_or(style.text.clone().into());
+                    let highlight_style =
+                        chunk.highlight_style.unwrap_or(style.text.clone().into());
                     // Avoid a lookup if the font properties match the previous ones.
                     let font_id = if highlight_style.font_properties == prev_font_properties {
                         prev_font_id
@@ -543,13 +613,7 @@ impl EditorElement {
                     }
 
                     let underline = if let Some(severity) = chunk.diagnostic {
-                        match severity {
-                            DiagnosticSeverity::ERROR => Some(style.error_underline),
-                            DiagnosticSeverity::WARNING => Some(style.warning_underline),
-                            DiagnosticSeverity::INFORMATION => Some(style.information_underline),
-                            DiagnosticSeverity::HINT => Some(style.hint_underline),
-                            _ => highlight_style.underline,
-                        }
+                        Some(super::diagnostic_style(severity, true, style).text)
                     } else {
                         highlight_style.underline
                     };
@@ -677,11 +741,8 @@ impl Element for EditorElement {
             }
         });
 
-        let line_number_layouts = if snapshot.mode == EditorMode::Full {
-            self.layout_line_numbers(start_row..end_row, &active_rows, &snapshot, cx)
-        } else {
-            Vec::new()
-        };
+        let (line_number_layouts, block_layouts) =
+            self.layout_rows(start_row..end_row, &active_rows, &snapshot, cx);
 
         let mut max_visible_line_width = 0.0;
         let line_layouts = self.layout_lines(start_row..end_row, &mut snapshot, cx);
@@ -703,6 +764,7 @@ impl Element for EditorElement {
             active_rows,
             line_layouts,
             line_number_layouts,
+            block_layouts,
             line_height,
             em_width,
             selections,
@@ -825,6 +887,7 @@ pub struct LayoutState {
     active_rows: BTreeMap<u32, bool>,
     line_layouts: Vec<text_layout::Line>,
     line_number_layouts: Vec<Option<text_layout::Line>>,
+    block_layouts: Vec<(Range<u32>, BlockStyle)>,
     line_height: f32,
     em_width: f32,
     selections: HashMap<ReplicaId, Vec<Range<DisplayPoint>>>,
@@ -1079,11 +1142,11 @@ mod tests {
         });
         let element = EditorElement::new(editor.downgrade(), settings);
 
-        let layouts = editor.update(cx, |editor, cx| {
+        let (layouts, _) = editor.update(cx, |editor, cx| {
             let snapshot = editor.snapshot(cx);
             let mut presenter = cx.build_presenter(window_id, 30.);
             let mut layout_cx = presenter.build_layout_context(false, cx);
-            element.layout_line_numbers(0..6, &Default::default(), &snapshot, &mut layout_cx)
+            element.layout_rows(0..6, &Default::default(), &snapshot, &mut layout_cx)
         });
         assert_eq!(layouts.len(), 6);
     }

crates/editor/src/lib.rs šŸ”—

@@ -7,13 +7,15 @@ mod test;
 
 use buffer::rope::TextDimension;
 use clock::ReplicaId;
-pub use display_map::DisplayPoint;
 use display_map::*;
+pub use display_map::{DisplayPoint, DisplayRow};
 pub use element::*;
 use gpui::{
-    action, geometry::vector::Vector2F, keymap::Binding, text_layout, AppContext, ClipboardItem,
-    Element, ElementBox, Entity, ModelHandle, MutableAppContext, RenderContext, View, ViewContext,
-    WeakViewHandle,
+    action,
+    geometry::vector::{vec2f, Vector2F},
+    keymap::Binding,
+    text_layout, AppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle,
+    MutableAppContext, RenderContext, View, ViewContext, WeakViewHandle,
 };
 use language::*;
 use serde::{Deserialize, Serialize};
@@ -22,6 +24,7 @@ use smol::Timer;
 use std::{
     cell::RefCell,
     cmp::{self, Ordering},
+    collections::HashMap,
     iter, mem,
     ops::{Range, RangeInclusive},
     rc::Rc,
@@ -29,7 +32,7 @@ use std::{
     time::Duration,
 };
 use sum_tree::Bias;
-use theme::EditorStyle;
+use theme::{DiagnosticStyle, EditorStyle, SyntaxTheme};
 use util::post_inc;
 
 const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
@@ -83,6 +86,7 @@ action!(AddSelectionBelow);
 action!(SelectLargerSyntaxNode);
 action!(SelectSmallerSyntaxNode);
 action!(MoveToEnclosingBracket);
+action!(ShowNextDiagnostic);
 action!(PageUp);
 action!(PageDown);
 action!(Fold);
@@ -184,6 +188,7 @@ pub fn init(cx: &mut MutableAppContext) {
         Binding::new("ctrl-w", SelectLargerSyntaxNode, Some("Editor")),
         Binding::new("alt-down", SelectSmallerSyntaxNode, Some("Editor")),
         Binding::new("ctrl-shift-W", SelectSmallerSyntaxNode, Some("Editor")),
+        Binding::new("f8", ShowNextDiagnostic, Some("Editor")),
         Binding::new("ctrl-m", MoveToEnclosingBracket, Some("Editor")),
         Binding::new("pageup", PageUp, Some("Editor")),
         Binding::new("pagedown", PageDown, Some("Editor")),
@@ -242,6 +247,7 @@ pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(Editor::select_larger_syntax_node);
     cx.add_action(Editor::select_smaller_syntax_node);
     cx.add_action(Editor::move_to_enclosing_bracket);
+    cx.add_action(Editor::show_next_diagnostic);
     cx.add_action(Editor::page_up);
     cx.add_action(Editor::page_down);
     cx.add_action(Editor::fold);
@@ -299,6 +305,7 @@ pub struct Editor {
     add_selections_state: Option<AddSelectionsState>,
     autoclose_stack: Vec<BracketPairState>,
     select_larger_syntax_node_stack: Vec<Box<[Selection<usize>]>>,
+    active_diagnostics: Option<ActiveDiagnosticGroup>,
     scroll_position: Vector2F,
     scroll_top_anchor: Anchor,
     autoscroll_requested: bool,
@@ -331,6 +338,14 @@ struct BracketPairState {
     pair: BracketPair,
 }
 
+#[derive(Debug)]
+struct ActiveDiagnosticGroup {
+    primary_range: Range<Anchor>,
+    primary_message: String,
+    blocks: HashMap<BlockId, Diagnostic>,
+    is_valid: bool,
+}
+
 #[derive(Serialize, Deserialize)]
 struct ClipboardSelection {
     len: usize,
@@ -418,6 +433,7 @@ impl Editor {
             add_selections_state: None,
             autoclose_stack: Default::default(),
             select_larger_syntax_node_stack: Vec::new(),
+            active_diagnostics: None,
             build_settings,
             scroll_position: Vector2F::zero(),
             scroll_top_anchor: Anchor::min(),
@@ -466,16 +482,24 @@ impl Editor {
         cx.notify();
     }
 
-    fn set_scroll_position(&mut self, mut scroll_position: Vector2F, cx: &mut ViewContext<Self>) {
+    fn set_scroll_position(&mut self, scroll_position: Vector2F, cx: &mut ViewContext<Self>) {
         let map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         let scroll_top_buffer_offset =
-            DisplayPoint::new(scroll_position.y() as u32, 0).to_buffer_offset(&map, Bias::Right);
+            DisplayPoint::new(scroll_position.y() as u32, 0).to_offset(&map, Bias::Right);
         self.scroll_top_anchor = self
             .buffer
             .read(cx)
             .anchor_at(scroll_top_buffer_offset, Bias::Right);
-        scroll_position.set_y(scroll_position.y().fract());
-        self.scroll_position = scroll_position;
+        self.scroll_position = vec2f(
+            scroll_position.x(),
+            scroll_position.y() - self.scroll_top_anchor.to_display_point(&map).row() as f32,
+        );
+
+        debug_assert_eq!(
+            compute_scroll_position(&map, self.scroll_position, &self.scroll_top_anchor),
+            scroll_position
+        );
+
         cx.notify();
     }
 
@@ -519,13 +543,13 @@ impl Editor {
             .peek()
             .unwrap()
             .head()
-            .to_display_point(&display_map, Bias::Left)
+            .to_display_point(&display_map)
             .row() as f32;
         let last_cursor_bottom = selections
             .last()
             .unwrap()
             .head()
-            .to_display_point(&display_map, Bias::Right)
+            .to_display_point(&display_map)
             .row() as f32
             + 1.0;
 
@@ -570,7 +594,7 @@ impl Editor {
         let mut target_left = std::f32::INFINITY;
         let mut target_right = 0.0_f32;
         for selection in selections {
-            let head = selection.head().to_display_point(&display_map, Bias::Left);
+            let head = selection.head().to_display_point(&display_map);
             let start_column = head.column().saturating_sub(3);
             let end_column = cmp::min(display_map.line_len(head.row()), head.column() + 3);
             target_left = target_left
@@ -620,7 +644,7 @@ impl Editor {
 
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         let buffer = self.buffer.read(cx);
-        let cursor = buffer.anchor_before(position.to_buffer_point(&display_map, Bias::Left));
+        let cursor = buffer.anchor_before(position.to_point(&display_map));
         let selection = Selection {
             id: post_inc(&mut self.next_selection_id),
             start: cursor.clone(),
@@ -646,7 +670,7 @@ impl Editor {
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         if let Some(pending_selection) = self.pending_selection.as_mut() {
             let buffer = self.buffer.read(cx);
-            let cursor = buffer.anchor_before(position.to_buffer_point(&display_map, Bias::Left));
+            let cursor = buffer.anchor_before(position.to_point(&display_map));
             if cursor.cmp(&pending_selection.tail(), buffer).unwrap() < Ordering::Equal {
                 if !pending_selection.reversed {
                     pending_selection.end = pending_selection.start.clone();
@@ -681,7 +705,9 @@ impl Editor {
     }
 
     pub fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
-        if let Some(pending_selection) = self.pending_selection.take() {
+        if self.active_diagnostics.is_some() {
+            self.dismiss_diagnostics(cx);
+        } else if let Some(pending_selection) = self.pending_selection.take() {
             let buffer = self.buffer.read(cx);
             let pending_selection = Selection {
                 id: pending_selection.id,
@@ -694,16 +720,8 @@ impl Editor {
                 self.update_selections(vec![pending_selection], true, cx);
             }
         } else {
-            let selections = self.selections::<Point>(cx);
-            let mut selection_count = 0;
-            let mut oldest_selection = selections
-                .min_by_key(|s| {
-                    selection_count += 1;
-                    s.id
-                })
-                .unwrap()
-                .clone();
-            if selection_count == 1 {
+            let mut oldest_selection = self.oldest_selection::<usize>(cx);
+            if self.selection_count(cx) == 1 {
                 oldest_selection.start = oldest_selection.head().clone();
                 oldest_selection.end = oldest_selection.head().clone();
             }
@@ -763,8 +781,8 @@ impl Editor {
                 };
                 Selection {
                     id: post_inc(&mut self.next_selection_id),
-                    start: start.to_buffer_point(&display_map, Bias::Left),
-                    end: end.to_buffer_point(&display_map, Bias::Left),
+                    start: start.to_point(&display_map),
+                    end: end.to_point(&display_map),
                     reversed,
                     goal: SelectionGoal::None,
                 }
@@ -1052,10 +1070,10 @@ impl Editor {
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         for selection in &mut selections {
             if selection.is_empty() {
-                let head = selection.head().to_display_point(&display_map, Bias::Left);
+                let head = selection.head().to_display_point(&display_map);
                 let cursor = movement::left(&display_map, head)
                     .unwrap()
-                    .to_buffer_point(&display_map, Bias::Left);
+                    .to_point(&display_map);
                 selection.set_head(cursor);
                 selection.goal = SelectionGoal::None;
             }
@@ -1071,10 +1089,10 @@ impl Editor {
         let mut selections = self.selections::<Point>(cx).collect::<Vec<_>>();
         for selection in &mut selections {
             if selection.is_empty() {
-                let head = selection.head().to_display_point(&display_map, Bias::Left);
+                let head = selection.head().to_display_point(&display_map);
                 let cursor = movement::right(&display_map, head)
                     .unwrap()
-                    .to_buffer_point(&display_map, Bias::Right);
+                    .to_point(&display_map);
                 selection.set_head(cursor);
                 selection.goal = SelectionGoal::None;
             }
@@ -1138,10 +1156,7 @@ impl Editor {
         let mut selections = selections.iter().peekable();
         while let Some(selection) = selections.next() {
             let mut rows = selection.spanned_rows(false, &display_map).buffer_rows;
-            let goal_display_column = selection
-                .head()
-                .to_display_point(&display_map, Bias::Left)
-                .column();
+            let goal_display_column = selection.head().to_display_point(&display_map).column();
 
             // Accumulate contiguous regions of rows that we want to delete.
             while let Some(next_selection) = selections.peek() {
@@ -1170,16 +1185,13 @@ impl Editor {
                 cursor_buffer_row = rows.start.saturating_sub(1);
             }
 
-            let mut cursor = Point::new(cursor_buffer_row - row_delta, 0)
-                .to_display_point(&display_map, Bias::Left);
+            let mut cursor =
+                Point::new(cursor_buffer_row - row_delta, 0).to_display_point(&display_map);
             *cursor.column_mut() =
                 cmp::min(goal_display_column, display_map.line_len(cursor.row()));
             row_delta += rows.len() as u32;
 
-            new_cursors.push((
-                selection.id,
-                cursor.to_buffer_point(&display_map, Bias::Left),
-            ));
+            new_cursors.push((selection.id, cursor.to_point(&display_map)));
             edit_ranges.push(edit_start..edit_end);
         }
 
@@ -1566,15 +1578,15 @@ impl Editor {
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         let mut selections = self.selections::<Point>(cx).collect::<Vec<_>>();
         for selection in &mut selections {
-            let start = selection.start.to_display_point(&display_map, Bias::Left);
-            let end = selection.end.to_display_point(&display_map, Bias::Left);
+            let start = selection.start.to_display_point(&display_map);
+            let end = selection.end.to_display_point(&display_map);
 
             if start != end {
                 selection.end = selection.start.clone();
             } else {
                 let cursor = movement::left(&display_map, start)
                     .unwrap()
-                    .to_buffer_point(&display_map, Bias::Left);
+                    .to_point(&display_map);
                 selection.start = cursor.clone();
                 selection.end = cursor;
             }
@@ -1588,10 +1600,10 @@ impl Editor {
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         let mut selections = self.selections::<Point>(cx).collect::<Vec<_>>();
         for selection in &mut selections {
-            let head = selection.head().to_display_point(&display_map, Bias::Left);
+            let head = selection.head().to_display_point(&display_map);
             let cursor = movement::left(&display_map, head)
                 .unwrap()
-                .to_buffer_point(&display_map, Bias::Left);
+                .to_point(&display_map);
             selection.set_head(cursor);
             selection.goal = SelectionGoal::None;
         }
@@ -1602,15 +1614,15 @@ impl Editor {
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         let mut selections = self.selections::<Point>(cx).collect::<Vec<_>>();
         for selection in &mut selections {
-            let start = selection.start.to_display_point(&display_map, Bias::Left);
-            let end = selection.end.to_display_point(&display_map, Bias::Left);
+            let start = selection.start.to_display_point(&display_map);
+            let end = selection.end.to_display_point(&display_map);
 
             if start != end {
                 selection.start = selection.end.clone();
             } else {
                 let cursor = movement::right(&display_map, end)
                     .unwrap()
-                    .to_buffer_point(&display_map, Bias::Right);
+                    .to_point(&display_map);
                 selection.start = cursor;
                 selection.end = cursor;
             }
@@ -1624,10 +1636,10 @@ impl Editor {
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         let mut selections = self.selections::<Point>(cx).collect::<Vec<_>>();
         for selection in &mut selections {
-            let head = selection.head().to_display_point(&display_map, Bias::Left);
+            let head = selection.head().to_display_point(&display_map);
             let cursor = movement::right(&display_map, head)
                 .unwrap()
-                .to_buffer_point(&display_map, Bias::Right);
+                .to_point(&display_map);
             selection.set_head(cursor);
             selection.goal = SelectionGoal::None;
         }
@@ -1643,14 +1655,14 @@ impl Editor {
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         let mut selections = self.selections::<Point>(cx).collect::<Vec<_>>();
         for selection in &mut selections {
-            let start = selection.start.to_display_point(&display_map, Bias::Left);
-            let end = selection.end.to_display_point(&display_map, Bias::Left);
+            let start = selection.start.to_display_point(&display_map);
+            let end = selection.end.to_display_point(&display_map);
             if start != end {
                 selection.goal = SelectionGoal::None;
             }
 
             let (start, goal) = movement::up(&display_map, start, selection.goal).unwrap();
-            let cursor = start.to_buffer_point(&display_map, Bias::Left);
+            let cursor = start.to_point(&display_map);
             selection.start = cursor;
             selection.end = cursor;
             selection.goal = goal;
@@ -1663,9 +1675,9 @@ impl Editor {
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         let mut selections = self.selections::<Point>(cx).collect::<Vec<_>>();
         for selection in &mut selections {
-            let head = selection.head().to_display_point(&display_map, Bias::Left);
+            let head = selection.head().to_display_point(&display_map);
             let (head, goal) = movement::up(&display_map, head, selection.goal).unwrap();
-            let cursor = head.to_buffer_point(&display_map, Bias::Left);
+            let cursor = head.to_point(&display_map);
             selection.set_head(cursor);
             selection.goal = goal;
         }
@@ -1681,14 +1693,14 @@ impl Editor {
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         let mut selections = self.selections::<Point>(cx).collect::<Vec<_>>();
         for selection in &mut selections {
-            let start = selection.start.to_display_point(&display_map, Bias::Left);
-            let end = selection.end.to_display_point(&display_map, Bias::Left);
+            let start = selection.start.to_display_point(&display_map);
+            let end = selection.end.to_display_point(&display_map);
             if start != end {
                 selection.goal = SelectionGoal::None;
             }
 
             let (start, goal) = movement::down(&display_map, end, selection.goal).unwrap();
-            let cursor = start.to_buffer_point(&display_map, Bias::Right);
+            let cursor = start.to_point(&display_map);
             selection.start = cursor;
             selection.end = cursor;
             selection.goal = goal;
@@ -1701,9 +1713,9 @@ impl Editor {
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         let mut selections = self.selections::<Point>(cx).collect::<Vec<_>>();
         for selection in &mut selections {
-            let head = selection.head().to_display_point(&display_map, Bias::Left);
+            let head = selection.head().to_display_point(&display_map);
             let (head, goal) = movement::down(&display_map, head, selection.goal).unwrap();
-            let cursor = head.to_buffer_point(&display_map, Bias::Right);
+            let cursor = head.to_point(&display_map);
             selection.set_head(cursor);
             selection.goal = goal;
         }
@@ -1718,9 +1730,9 @@ impl Editor {
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         let mut selections = self.selections::<Point>(cx).collect::<Vec<_>>();
         for selection in &mut selections {
-            let head = selection.head().to_display_point(&display_map, Bias::Left);
+            let head = selection.head().to_display_point(&display_map);
             let new_head = movement::prev_word_boundary(&display_map, head).unwrap();
-            let cursor = new_head.to_buffer_point(&display_map, Bias::Left);
+            let cursor = new_head.to_point(&display_map);
             selection.start = cursor.clone();
             selection.end = cursor;
             selection.reversed = false;
@@ -1737,9 +1749,9 @@ impl Editor {
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         let mut selections = self.selections::<Point>(cx).collect::<Vec<_>>();
         for selection in &mut selections {
-            let head = selection.head().to_display_point(&display_map, Bias::Left);
+            let head = selection.head().to_display_point(&display_map);
             let new_head = movement::prev_word_boundary(&display_map, head).unwrap();
-            let cursor = new_head.to_buffer_point(&display_map, Bias::Left);
+            let cursor = new_head.to_point(&display_map);
             selection.set_head(cursor);
             selection.goal = SelectionGoal::None;
         }
@@ -1756,9 +1768,9 @@ impl Editor {
         let mut selections = self.selections::<Point>(cx).collect::<Vec<_>>();
         for selection in &mut selections {
             if selection.is_empty() {
-                let head = selection.head().to_display_point(&display_map, Bias::Left);
+                let head = selection.head().to_display_point(&display_map);
                 let new_head = movement::prev_word_boundary(&display_map, head).unwrap();
-                let cursor = new_head.to_buffer_point(&display_map, Bias::Right);
+                let cursor = new_head.to_point(&display_map);
                 selection.set_head(cursor);
                 selection.goal = SelectionGoal::None;
             }
@@ -1776,9 +1788,9 @@ impl Editor {
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         let mut selections = self.selections::<Point>(cx).collect::<Vec<_>>();
         for selection in &mut selections {
-            let head = selection.head().to_display_point(&display_map, Bias::Left);
+            let head = selection.head().to_display_point(&display_map);
             let new_head = movement::next_word_boundary(&display_map, head).unwrap();
-            let cursor = new_head.to_buffer_point(&display_map, Bias::Left);
+            let cursor = new_head.to_point(&display_map);
             selection.start = cursor;
             selection.end = cursor;
             selection.reversed = false;
@@ -1795,9 +1807,9 @@ impl Editor {
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         let mut selections = self.selections::<Point>(cx).collect::<Vec<_>>();
         for selection in &mut selections {
-            let head = selection.head().to_display_point(&display_map, Bias::Left);
+            let head = selection.head().to_display_point(&display_map);
             let new_head = movement::next_word_boundary(&display_map, head).unwrap();
-            let cursor = new_head.to_buffer_point(&display_map, Bias::Left);
+            let cursor = new_head.to_point(&display_map);
             selection.set_head(cursor);
             selection.goal = SelectionGoal::None;
         }
@@ -1814,9 +1826,9 @@ impl Editor {
         let mut selections = self.selections::<Point>(cx).collect::<Vec<_>>();
         for selection in &mut selections {
             if selection.is_empty() {
-                let head = selection.head().to_display_point(&display_map, Bias::Left);
+                let head = selection.head().to_display_point(&display_map);
                 let new_head = movement::next_word_boundary(&display_map, head).unwrap();
-                let cursor = new_head.to_buffer_point(&display_map, Bias::Right);
+                let cursor = new_head.to_point(&display_map);
                 selection.set_head(cursor);
                 selection.goal = SelectionGoal::None;
             }
@@ -1834,9 +1846,9 @@ impl Editor {
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         let mut selections = self.selections::<Point>(cx).collect::<Vec<_>>();
         for selection in &mut selections {
-            let head = selection.head().to_display_point(&display_map, Bias::Left);
+            let head = selection.head().to_display_point(&display_map);
             let new_head = movement::line_beginning(&display_map, head, true).unwrap();
-            let cursor = new_head.to_buffer_point(&display_map, Bias::Left);
+            let cursor = new_head.to_point(&display_map);
             selection.start = cursor;
             selection.end = cursor;
             selection.reversed = false;
@@ -1853,9 +1865,9 @@ impl Editor {
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         let mut selections = self.selections::<Point>(cx).collect::<Vec<_>>();
         for selection in &mut selections {
-            let head = selection.head().to_display_point(&display_map, Bias::Left);
+            let head = selection.head().to_display_point(&display_map);
             let new_head = movement::line_beginning(&display_map, head, *toggle_indent).unwrap();
-            selection.set_head(new_head.to_buffer_point(&display_map, Bias::Left));
+            selection.set_head(new_head.to_point(&display_map));
             selection.goal = SelectionGoal::None;
         }
         self.update_selections(selections, true, cx);
@@ -1877,9 +1889,9 @@ impl Editor {
         let mut selections = self.selections::<Point>(cx).collect::<Vec<_>>();
         {
             for selection in &mut selections {
-                let head = selection.head().to_display_point(&display_map, Bias::Left);
+                let head = selection.head().to_display_point(&display_map);
                 let new_head = movement::line_end(&display_map, head).unwrap();
-                let anchor = new_head.to_buffer_point(&display_map, Bias::Left);
+                let anchor = new_head.to_point(&display_map);
                 selection.start = anchor.clone();
                 selection.end = anchor;
                 selection.reversed = false;
@@ -1893,9 +1905,9 @@ impl Editor {
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         let mut selections = self.selections::<Point>(cx).collect::<Vec<_>>();
         for selection in &mut selections {
-            let head = selection.head().to_display_point(&display_map, Bias::Left);
+            let head = selection.head().to_display_point(&display_map);
             let new_head = movement::line_end(&display_map, head).unwrap();
-            selection.set_head(new_head.to_buffer_point(&display_map, Bias::Left));
+            selection.set_head(new_head.to_point(&display_map));
             selection.goal = SelectionGoal::None;
         }
         self.update_selections(selections, true, cx);
@@ -2205,6 +2217,197 @@ impl Editor {
         self.update_selections(selections, true, cx);
     }
 
+    pub fn show_next_diagnostic(&mut self, _: &ShowNextDiagnostic, cx: &mut ViewContext<Self>) {
+        let selection = self.newest_selection::<usize>(cx);
+        let buffer = self.buffer.read(cx.as_ref());
+        let active_primary_range = self.active_diagnostics.as_ref().map(|active_diagnostics| {
+            active_diagnostics
+                .primary_range
+                .to_offset(buffer)
+                .to_inclusive()
+        });
+        let mut search_start = if let Some(active_primary_range) = active_primary_range.as_ref() {
+            if active_primary_range.contains(&selection.head()) {
+                *active_primary_range.end()
+            } else {
+                selection.head()
+            }
+        } else {
+            selection.head()
+        };
+
+        loop {
+            let next_group = buffer
+                .diagnostics_in_range::<_, usize>(search_start..buffer.len())
+                .find_map(|(range, diagnostic)| {
+                    if diagnostic.is_primary
+                        && !range.is_empty()
+                        && Some(range.end) != active_primary_range.as_ref().map(|r| *r.end())
+                    {
+                        Some((range, diagnostic.group_id))
+                    } else {
+                        None
+                    }
+                });
+
+            if let Some((primary_range, group_id)) = next_group {
+                self.activate_diagnostics(group_id, cx);
+                self.update_selections(
+                    vec![Selection {
+                        id: selection.id,
+                        start: primary_range.start,
+                        end: primary_range.start,
+                        reversed: false,
+                        goal: SelectionGoal::None,
+                    }],
+                    true,
+                    cx,
+                );
+                break;
+            } else if search_start == 0 {
+                break;
+            } else {
+                // Cycle around to the start of the buffer.
+                search_start = 0;
+            }
+        }
+    }
+
+    fn refresh_active_diagnostics(&mut self, cx: &mut ViewContext<Editor>) {
+        if let Some(active_diagnostics) = self.active_diagnostics.as_mut() {
+            let buffer = self.buffer.read(cx);
+            let primary_range_start = active_diagnostics.primary_range.start.to_offset(buffer);
+            let is_valid = buffer
+                .diagnostics_in_range::<_, usize>(active_diagnostics.primary_range.clone())
+                .any(|(range, diagnostic)| {
+                    diagnostic.is_primary
+                        && !range.is_empty()
+                        && range.start == primary_range_start
+                        && diagnostic.message == active_diagnostics.primary_message
+                });
+
+            if is_valid != active_diagnostics.is_valid {
+                active_diagnostics.is_valid = is_valid;
+                let mut new_styles = HashMap::new();
+                for (block_id, diagnostic) in &active_diagnostics.blocks {
+                    let severity = diagnostic.severity;
+                    let message_len = diagnostic.message.len();
+                    new_styles.insert(
+                        *block_id,
+                        (
+                            Some({
+                                let build_settings = self.build_settings.clone();
+                                move |cx: &AppContext| {
+                                    let settings = build_settings.borrow()(cx);
+                                    vec![(
+                                        message_len,
+                                        diagnostic_style(severity, is_valid, &settings.style)
+                                            .text
+                                            .into(),
+                                    )]
+                                }
+                            }),
+                            Some({
+                                let build_settings = self.build_settings.clone();
+                                move |cx: &AppContext| {
+                                    let settings = build_settings.borrow()(cx);
+                                    diagnostic_style(severity, is_valid, &settings.style).block
+                                }
+                            }),
+                        ),
+                    );
+                }
+                self.display_map
+                    .update(cx, |display_map, _| display_map.restyle_blocks(new_styles));
+            }
+        }
+    }
+
+    fn activate_diagnostics(&mut self, group_id: usize, cx: &mut ViewContext<Self>) {
+        self.dismiss_diagnostics(cx);
+        self.active_diagnostics = self.display_map.update(cx, |display_map, cx| {
+            let buffer = self.buffer.read(cx);
+
+            let mut primary_range = None;
+            let mut primary_message = None;
+            let mut group_end = Point::zero();
+            let diagnostic_group = buffer
+                .diagnostic_group::<Point>(group_id)
+                .map(|(range, diagnostic)| {
+                    if range.end > group_end {
+                        group_end = range.end;
+                    }
+                    if diagnostic.is_primary {
+                        primary_range = Some(range.clone());
+                        primary_message = Some(diagnostic.message.clone());
+                    }
+                    (range, diagnostic.clone())
+                })
+                .collect::<Vec<_>>();
+            let primary_range = primary_range.unwrap();
+            let primary_message = primary_message.unwrap();
+            let primary_range =
+                buffer.anchor_after(primary_range.start)..buffer.anchor_before(primary_range.end);
+
+            let blocks = display_map
+                .insert_blocks(
+                    diagnostic_group.iter().map(|(range, diagnostic)| {
+                        let build_settings = self.build_settings.clone();
+                        let message_len = diagnostic.message.len();
+                        let severity = diagnostic.severity;
+                        BlockProperties {
+                            position: range.start,
+                            text: diagnostic.message.as_str(),
+                            build_runs: Some(Arc::new({
+                                let build_settings = build_settings.clone();
+                                move |cx| {
+                                    let settings = build_settings.borrow()(cx);
+                                    vec![(
+                                        message_len,
+                                        diagnostic_style(severity, true, &settings.style)
+                                            .text
+                                            .into(),
+                                    )]
+                                }
+                            })),
+                            build_style: Some(Arc::new({
+                                let build_settings = build_settings.clone();
+                                move |cx| {
+                                    let settings = build_settings.borrow()(cx);
+                                    diagnostic_style(severity, true, &settings.style).block
+                                }
+                            })),
+                            disposition: BlockDisposition::Below,
+                        }
+                    }),
+                    cx,
+                )
+                .into_iter()
+                .zip(
+                    diagnostic_group
+                        .into_iter()
+                        .map(|(_, diagnostic)| diagnostic),
+                )
+                .collect();
+
+            Some(ActiveDiagnosticGroup {
+                primary_range,
+                primary_message,
+                blocks,
+                is_valid: true,
+            })
+        });
+    }
+
+    fn dismiss_diagnostics(&mut self, cx: &mut ViewContext<Self>) {
+        if let Some(active_diagnostic_group) = self.active_diagnostics.take() {
+            self.display_map.update(cx, |display_map, cx| {
+                display_map.remove_blocks(active_diagnostic_group.blocks.into_keys().collect(), cx);
+            });
+            cx.notify();
+        }
+    }
+
     fn build_columnar_selection(
         &mut self,
         display_map: &DisplayMapSnapshot,
@@ -2219,8 +2422,8 @@ impl Editor {
             let end = DisplayPoint::new(row, cmp::min(columns.end, line_len));
             Some(Selection {
                 id: post_inc(&mut self.next_selection_id),
-                start: start.to_buffer_point(display_map, Bias::Left),
-                end: end.to_buffer_point(display_map, Bias::Left),
+                start: start.to_point(display_map),
+                end: end.to_point(display_map),
                 reversed,
                 goal: SelectionGoal::ColumnRange {
                     start: columns.start,
@@ -2254,17 +2457,16 @@ impl Editor {
     ) -> impl 'a + Iterator<Item = Range<DisplayPoint>> {
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         let buffer = self.buffer.read(cx);
-        let selections = buffer
-            .selection_set(set_id)
-            .unwrap()
+        let selections = self
+            .selection_set(cx)
             .selections::<Point, _>(buffer)
             .collect::<Vec<_>>();
-        let start = range.start.to_buffer_point(&display_map, Bias::Left);
+        let start = range.start.to_point(&display_map);
         let start_index = self.selection_insertion_index(&selections, start);
         let pending_selection = if set_id.replica_id == self.buffer.read(cx).replica_id() {
             self.pending_selection.as_ref().and_then(|pending| {
-                let mut selection_start = pending.start.to_display_point(&display_map, Bias::Left);
-                let mut selection_end = pending.end.to_display_point(&display_map, Bias::Left);
+                let mut selection_start = pending.start.to_display_point(&display_map);
+                let mut selection_end = pending.end.to_display_point(&display_map);
                 if pending.reversed {
                     mem::swap(&mut selection_start, &mut selection_end);
                 }
@@ -2303,18 +2505,8 @@ impl Editor {
         D: 'a + TextDimension<'a> + Ord,
     {
         let buffer = self.buffer.read(cx);
-        let mut selections = buffer
-            .selection_set(self.selection_set_id)
-            .unwrap()
-            .selections::<D, _>(buffer)
-            .peekable();
-        let mut pending_selection = self.pending_selection.clone().map(|selection| Selection {
-            id: selection.id,
-            start: selection.start.summary::<D, _>(buffer),
-            end: selection.end.summary::<D, _>(buffer),
-            reversed: selection.reversed,
-            goal: selection.goal,
-        });
+        let mut selections = self.selection_set(cx).selections::<D, _>(buffer).peekable();
+        let mut pending_selection = self.pending_selection(cx);
         iter::from_fn(move || {
             if let Some(pending) = pending_selection.as_mut() {
                 while let Some(next_selection) = selections.peek() {
@@ -2340,6 +2532,56 @@ impl Editor {
         })
     }
 
+    fn pending_selection<'a, D>(&self, cx: &'a AppContext) -> Option<Selection<D>>
+    where
+        D: 'a + TextDimension<'a>,
+    {
+        let buffer = self.buffer.read(cx);
+        self.pending_selection.as_ref().map(|selection| Selection {
+            id: selection.id,
+            start: selection.start.summary::<D, _>(buffer),
+            end: selection.end.summary::<D, _>(buffer),
+            reversed: selection.reversed,
+            goal: selection.goal,
+        })
+    }
+
+    fn selection_count<'a>(&self, cx: &'a AppContext) -> usize {
+        let mut selection_count = self.selection_set(cx).len();
+        if self.pending_selection.is_some() {
+            selection_count += 1;
+        }
+        selection_count
+    }
+
+    pub fn oldest_selection<'a, T>(&self, cx: &'a AppContext) -> Selection<T>
+    where
+        T: 'a + TextDimension<'a>,
+    {
+        let buffer = self.buffer.read(cx);
+        self.selection_set(cx)
+            .oldest_selection(buffer)
+            .or_else(|| self.pending_selection(cx))
+            .unwrap()
+    }
+
+    pub fn newest_selection<'a, T>(&self, cx: &'a AppContext) -> Selection<T>
+    where
+        T: 'a + TextDimension<'a>,
+    {
+        let buffer = self.buffer.read(cx);
+        self.pending_selection(cx)
+            .or_else(|| self.selection_set(cx).newest_selection(buffer))
+            .unwrap()
+    }
+
+    fn selection_set<'a>(&self, cx: &'a AppContext) -> &'a SelectionSet {
+        self.buffer
+            .read(cx)
+            .selection_set(self.selection_set_id)
+            .unwrap()
+    }
+
     fn update_selections<T>(
         &mut self,
         mut selections: Vec<Selection<T>>,
@@ -2438,7 +2680,7 @@ impl Editor {
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         for selection in selections {
             let range = selection.display_range(&display_map).sorted();
-            let buffer_start_row = range.start.to_buffer_point(&display_map, Bias::Left).row;
+            let buffer_start_row = range.start.to_point(&display_map).row;
 
             for row in (0..=range.end.row()).rev() {
                 if self.is_line_foldable(&display_map, row) && !display_map.is_line_folded(row) {
@@ -2464,8 +2706,8 @@ impl Editor {
             .iter()
             .map(|s| {
                 let range = s.display_range(&display_map).sorted();
-                let mut start = range.start.to_buffer_point(&display_map, Bias::Left);
-                let mut end = range.end.to_buffer_point(&display_map, Bias::Left);
+                let mut start = range.start.to_point(&display_map);
+                let mut end = range.end.to_point(&display_map);
                 start.column = 0;
                 end.column = buffer.line_len(end.row);
                 start..end
@@ -2513,8 +2755,7 @@ impl Editor {
         }
 
         let end = end.unwrap_or(max_point);
-        return start.to_buffer_point(display_map, Bias::Left)
-            ..end.to_buffer_point(display_map, Bias::Left);
+        return start.to_point(display_map)..end.to_point(display_map);
     }
 
     pub fn fold_selected_ranges(&mut self, _: &FoldSelectedRanges, cx: &mut ViewContext<Self>) {
@@ -2624,6 +2865,7 @@ impl Editor {
     }
 
     fn on_buffer_changed(&mut self, _: ModelHandle<Buffer>, cx: &mut ViewContext<Self>) {
+        self.refresh_active_diagnostics(cx);
         cx.notify();
     }
 
@@ -2666,16 +2908,17 @@ impl Snapshot {
         self.display_snapshot.buffer_row_count()
     }
 
-    pub fn buffer_rows(&self, start_row: u32) -> BufferRows {
-        self.display_snapshot.buffer_rows(start_row)
+    pub fn buffer_rows<'a>(&'a self, start_row: u32, cx: &'a AppContext) -> BufferRows<'a> {
+        self.display_snapshot.buffer_rows(start_row, Some(cx))
     }
 
-    pub fn highlighted_chunks_for_rows(
-        &mut self,
+    pub fn chunks<'a>(
+        &'a self,
         display_rows: Range<u32>,
-    ) -> display_map::HighlightedChunks {
-        self.display_snapshot
-            .highlighted_chunks_for_rows(display_rows)
+        theme: Option<&'a SyntaxTheme>,
+        cx: &'a AppContext,
+    ) -> display_map::Chunks<'a> {
+        self.display_snapshot.chunks(display_rows, theme, cx)
     }
 
     pub fn scroll_position(&self) -> Vector2F {
@@ -2743,10 +2986,14 @@ impl EditorSettings {
                     selection: Default::default(),
                     guest_selections: Default::default(),
                     syntax: Default::default(),
-                    error_underline: Default::default(),
-                    warning_underline: Default::default(),
-                    information_underline: Default::default(),
-                    hint_underline: Default::default(),
+                    error_diagnostic: Default::default(),
+                    invalid_error_diagnostic: Default::default(),
+                    warning_diagnostic: Default::default(),
+                    invalid_warning_diagnostic: Default::default(),
+                    information_diagnostic: Default::default(),
+                    invalid_information_diagnostic: Default::default(),
+                    hint_diagnostic: Default::default(),
+                    invalid_hint_diagnostic: Default::default(),
                 }
             },
         }
@@ -2758,9 +3005,7 @@ fn compute_scroll_position(
     mut scroll_position: Vector2F,
     scroll_top_anchor: &Anchor,
 ) -> Vector2F {
-    let scroll_top = scroll_top_anchor
-        .to_display_point(snapshot, Bias::Left)
-        .row() as f32;
+    let scroll_top = scroll_top_anchor.to_display_point(snapshot).row() as f32;
     scroll_position.set_y(scroll_top + scroll_position.y());
     scroll_position
 }
@@ -2838,8 +3083,8 @@ impl View for Editor {
 
 impl SelectionExt for Selection<Point> {
     fn display_range(&self, map: &DisplayMapSnapshot) -> Range<DisplayPoint> {
-        let start = self.start.to_display_point(map, Bias::Left);
-        let end = self.end.to_display_point(map, Bias::Left);
+        let start = self.start.to_display_point(map);
+        let end = self.end.to_display_point(map);
         if self.reversed {
             end..start
         } else {
@@ -2852,8 +3097,8 @@ impl SelectionExt for Selection<Point> {
         include_end_if_at_line_start: bool,
         map: &DisplayMapSnapshot,
     ) -> SpannedRows {
-        let display_start = self.start.to_display_point(map, Bias::Left);
-        let mut display_end = self.end.to_display_point(map, Bias::Right);
+        let display_start = self.start.to_display_point(map);
+        let mut display_end = self.end.to_display_point(map);
         if !include_end_if_at_line_start
             && display_end.row() != map.max_point().row()
             && display_start.row() != display_end.row()
@@ -2872,6 +3117,24 @@ impl SelectionExt for Selection<Point> {
     }
 }
 
+pub fn diagnostic_style(
+    severity: DiagnosticSeverity,
+    valid: bool,
+    style: &EditorStyle,
+) -> DiagnosticStyle {
+    match (severity, valid) {
+        (DiagnosticSeverity::ERROR, true) => style.error_diagnostic,
+        (DiagnosticSeverity::ERROR, false) => style.invalid_error_diagnostic,
+        (DiagnosticSeverity::WARNING, true) => style.warning_diagnostic,
+        (DiagnosticSeverity::WARNING, false) => style.invalid_warning_diagnostic,
+        (DiagnosticSeverity::INFORMATION, true) => style.information_diagnostic,
+        (DiagnosticSeverity::INFORMATION, false) => style.invalid_information_diagnostic,
+        (DiagnosticSeverity::HINT, true) => style.hint_diagnostic,
+        (DiagnosticSeverity::HINT, false) => style.invalid_hint_diagnostic,
+        _ => Default::default(),
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;

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

@@ -33,11 +33,17 @@ pub fn up(
         map.column_to_chars(point.row(), point.column())
     };
 
-    if point.row() > 0 {
-        *point.row_mut() -= 1;
-        *point.column_mut() = map.column_from_chars(point.row(), goal_column);
-    } else {
-        point = DisplayPoint::new(0, 0);
+    loop {
+        if point.row() > 0 {
+            *point.row_mut() -= 1;
+            *point.column_mut() = map.column_from_chars(point.row(), goal_column);
+            if !map.is_block_line(point.row()) {
+                break;
+            }
+        } else {
+            point = DisplayPoint::new(0, 0);
+            break;
+        }
     }
 
     let clip_bias = if point.column() == map.line_len(point.row()) {
@@ -64,11 +70,17 @@ pub fn down(
         map.column_to_chars(point.row(), point.column())
     };
 
-    if point.row() < max_point.row() {
-        *point.row_mut() += 1;
-        *point.column_mut() = map.column_from_chars(point.row(), goal_column);
-    } else {
-        point = max_point;
+    loop {
+        if point.row() < max_point.row() {
+            *point.row_mut() += 1;
+            *point.column_mut() = map.column_from_chars(point.row(), goal_column);
+            if !map.is_block_line(point.row()) {
+                break;
+            }
+        } else {
+            point = max_point;
+            break;
+        }
     }
 
     let clip_bias = if point.column() == map.line_len(point.row()) {

crates/editor/src/test.rs šŸ”—

@@ -2,6 +2,13 @@ use gpui::{Entity, ModelHandle};
 use smol::channel;
 use std::marker::PhantomData;
 
+#[cfg(test)]
+#[ctor::ctor]
+fn init_logger() {
+    // std::env::set_var("RUST_LOG", "info");
+    env_logger::init();
+}
+
 pub fn sample_text(rows: usize, cols: usize) -> String {
     let mut text = String::new();
     for row in 0..rows {

crates/gpui/src/fonts.rs šŸ”—

@@ -30,7 +30,7 @@ pub struct TextStyle {
     pub underline: Option<Color>,
 }
 
-#[derive(Clone, Debug, Default)]
+#[derive(Copy, Clone, Debug, Default)]
 pub struct HighlightStyle {
     pub color: Color,
     pub font_properties: Properties,

crates/gpui/src/platform/mac/event.rs šŸ”—

@@ -1,10 +1,4 @@
 use crate::{geometry::vector::vec2f, keymap::Keystroke, platform::Event};
-use cocoa::appkit::{
-    NSDeleteFunctionKey as DELETE_KEY, NSDownArrowFunctionKey as ARROW_DOWN_KEY,
-    NSLeftArrowFunctionKey as ARROW_LEFT_KEY, NSPageDownFunctionKey as PAGE_DOWN_KEY,
-    NSPageUpFunctionKey as PAGE_UP_KEY, NSRightArrowFunctionKey as ARROW_RIGHT_KEY,
-    NSUpArrowFunctionKey as ARROW_UP_KEY,
-};
 use cocoa::{
     appkit::{NSEvent, NSEventModifierFlags, NSEventType},
     base::{id, nil, YES},
@@ -12,11 +6,6 @@ use cocoa::{
 };
 use std::{ffi::CStr, os::raw::c_char};
 
-const BACKSPACE_KEY: u16 = 0x7f;
-const ENTER_KEY: u16 = 0x0d;
-const ESCAPE_KEY: u16 = 0x1b;
-const TAB_KEY: u16 = 0x09;
-
 impl Event {
     pub unsafe fn from_native(native_event: id, window_height: Option<f32>) -> Option<Self> {
         let event_type = native_event.eventType();
@@ -39,18 +28,39 @@ impl Event {
                     .unwrap();
 
                 let unmodified_chars = if let Some(first_char) = unmodified_chars.chars().next() {
+                    use cocoa::appkit::*;
+                    const BACKSPACE_KEY: u16 = 0x7f;
+                    const ENTER_KEY: u16 = 0x0d;
+                    const ESCAPE_KEY: u16 = 0x1b;
+                    const TAB_KEY: u16 = 0x09;
+
+                    #[allow(non_upper_case_globals)]
                     match first_char as u16 {
-                        ARROW_UP_KEY => "up",
-                        ARROW_DOWN_KEY => "down",
-                        ARROW_LEFT_KEY => "left",
-                        ARROW_RIGHT_KEY => "right",
-                        PAGE_UP_KEY => "pageup",
-                        PAGE_DOWN_KEY => "pagedown",
                         BACKSPACE_KEY => "backspace",
                         ENTER_KEY => "enter",
-                        DELETE_KEY => "delete",
                         ESCAPE_KEY => "escape",
                         TAB_KEY => "tab",
+
+                        NSUpArrowFunctionKey => "up",
+                        NSDownArrowFunctionKey => "down",
+                        NSLeftArrowFunctionKey => "left",
+                        NSRightArrowFunctionKey => "right",
+                        NSPageUpFunctionKey => "pageup",
+                        NSPageDownFunctionKey => "pagedown",
+                        NSDeleteFunctionKey => "delete",
+                        NSF1FunctionKey => "f1",
+                        NSF2FunctionKey => "f2",
+                        NSF3FunctionKey => "f3",
+                        NSF4FunctionKey => "f4",
+                        NSF5FunctionKey => "f5",
+                        NSF6FunctionKey => "f6",
+                        NSF7FunctionKey => "f7",
+                        NSF8FunctionKey => "f8",
+                        NSF9FunctionKey => "f9",
+                        NSF10FunctionKey => "f10",
+                        NSF11FunctionKey => "f11",
+                        NSF12FunctionKey => "f12",
+
                         _ => unmodified_chars,
                     }
                 } else {

crates/language/src/lib.rs šŸ”—

@@ -12,7 +12,7 @@ use anyhow::{anyhow, Result};
 pub use buffer::{Buffer as TextBuffer, Operation as _, *};
 use clock::ReplicaId;
 use futures::FutureExt as _;
-use gpui::{AppContext, Entity, ModelContext, MutableAppContext, Task};
+use gpui::{fonts::HighlightStyle, AppContext, Entity, ModelContext, MutableAppContext, Task};
 use lazy_static::lazy_static;
 use lsp::LanguageServer;
 use parking_lot::Mutex;
@@ -34,6 +34,7 @@ use std::{
     time::{Duration, Instant, SystemTime, UNIX_EPOCH},
     vec,
 };
+use theme::SyntaxTheme;
 use tree_sitter::{InputEdit, Parser, QueryCursor, Tree};
 use util::{post_inc, TryFutureExt as _};
 
@@ -78,13 +79,14 @@ pub struct Snapshot {
     diagnostics: AnchorRangeMultimap<Diagnostic>,
     is_parsing: bool,
     language: Option<Arc<Language>>,
-    query_cursor: QueryCursorHandle,
 }
 
 #[derive(Clone, Debug, PartialEq, Eq)]
 pub struct Diagnostic {
     pub severity: DiagnosticSeverity,
     pub message: String,
+    pub group_id: usize,
+    pub is_primary: bool,
 }
 
 struct LanguageServerState {
@@ -190,11 +192,13 @@ struct Highlights<'a> {
     next_capture: Option<(tree_sitter::QueryMatch<'a, 'a>, usize)>,
     stack: Vec<(usize, HighlightId)>,
     highlight_map: HighlightMap,
+    theme: &'a SyntaxTheme,
+    _query_cursor: QueryCursorHandle,
 }
 
-pub struct HighlightedChunks<'a> {
+pub struct Chunks<'a> {
     range: Range<usize>,
-    chunks: Chunks<'a>,
+    chunks: rope::Chunks<'a>,
     diagnostic_endpoints: Peekable<vec::IntoIter<DiagnosticEndpoint>>,
     error_depth: usize,
     warning_depth: usize,
@@ -204,9 +208,9 @@ pub struct HighlightedChunks<'a> {
 }
 
 #[derive(Clone, Copy, Debug, Default)]
-pub struct HighlightedChunk<'a> {
+pub struct Chunk<'a> {
     pub text: &'a str,
-    pub highlight_id: HighlightId,
+    pub highlight_style: Option<HighlightStyle>,
     pub diagnostic: Option<DiagnosticSeverity>,
 }
 
@@ -341,7 +345,6 @@ impl Buffer {
             diagnostics: self.diagnostics.clone(),
             is_parsing: self.parsing_in_background,
             language: self.language.clone(),
-            query_cursor: QueryCursorHandle::new(),
         }
     }
 
@@ -438,7 +441,7 @@ impl Buffer {
                                                     uri,
                                                     Default::default(),
                                                     snapshot.version as i32,
-                                                    snapshot.buffer_snapshot.text().into(),
+                                                    snapshot.buffer_snapshot.text().to_string(),
                                                 ),
                                             },
                                         )
@@ -699,6 +702,7 @@ impl Buffer {
         } else {
             self.content()
         };
+        let abs_path = self.file.as_ref().and_then(|f| f.abs_path());
 
         let empty_set = HashSet::new();
         let disk_based_sources = self
@@ -714,56 +718,82 @@ impl Buffer {
                 .peekable();
             let mut last_edit_old_end = PointUtf16::zero();
             let mut last_edit_new_end = PointUtf16::zero();
-
-            content.anchor_range_multimap(
-                Bias::Left,
-                Bias::Right,
-                diagnostics.into_iter().filter_map(|diagnostic| {
-                    let mut start = PointUtf16::new(
-                        diagnostic.range.start.line,
-                        diagnostic.range.start.character,
-                    );
-                    let mut end =
-                        PointUtf16::new(diagnostic.range.end.line, diagnostic.range.end.character);
-                    if diagnostic
-                        .source
-                        .as_ref()
-                        .map_or(false, |source| disk_based_sources.contains(source))
-                    {
-                        while let Some(edit) = edits_since_save.peek() {
-                            if edit.old.end <= start {
-                                last_edit_old_end = edit.old.end;
-                                last_edit_new_end = edit.new.end;
-                                edits_since_save.next();
-                            } else if edit.old.start <= end && edit.old.end >= start {
-                                return None;
-                            } else {
-                                break;
-                            }
+            let mut group_ids_by_diagnostic_range = HashMap::new();
+            let mut diagnostics_by_group_id = HashMap::new();
+            let mut next_group_id = 0;
+            'outer: for diagnostic in &diagnostics {
+                let mut start = diagnostic.range.start.to_point_utf16();
+                let mut end = diagnostic.range.end.to_point_utf16();
+                let source = diagnostic.source.as_ref();
+                let code = diagnostic.code.as_ref();
+                let group_id = diagnostic_ranges(&diagnostic, abs_path.as_deref())
+                    .find_map(|range| group_ids_by_diagnostic_range.get(&(source, code, range)))
+                    .copied()
+                    .unwrap_or_else(|| {
+                        let group_id = post_inc(&mut next_group_id);
+                        for range in diagnostic_ranges(&diagnostic, abs_path.as_deref()) {
+                            group_ids_by_diagnostic_range.insert((source, code, range), group_id);
+                        }
+                        group_id
+                    });
+
+                if diagnostic
+                    .source
+                    .as_ref()
+                    .map_or(false, |source| disk_based_sources.contains(source))
+                {
+                    while let Some(edit) = edits_since_save.peek() {
+                        if edit.old.end <= start {
+                            last_edit_old_end = edit.old.end;
+                            last_edit_new_end = edit.new.end;
+                            edits_since_save.next();
+                        } else if edit.old.start <= end && edit.old.end >= start {
+                            continue 'outer;
+                        } else {
+                            break;
                         }
-
-                        start = last_edit_new_end + (start - last_edit_old_end);
-                        end = last_edit_new_end + (end - last_edit_old_end);
                     }
 
-                    let mut range = content.clip_point_utf16(start, Bias::Left)
-                        ..content.clip_point_utf16(end, Bias::Right);
-                    if range.start == range.end {
-                        range.end.column += 1;
-                        range.end = content.clip_point_utf16(range.end, Bias::Right);
-                        if range.start == range.end && range.end.column > 0 {
-                            range.start.column -= 1;
-                            range.start = content.clip_point_utf16(range.start, Bias::Left);
-                        }
+                    start = last_edit_new_end + (start - last_edit_old_end);
+                    end = last_edit_new_end + (end - last_edit_old_end);
+                }
+
+                let mut range = content.clip_point_utf16(start, Bias::Left)
+                    ..content.clip_point_utf16(end, Bias::Right);
+                if range.start == range.end {
+                    range.end.column += 1;
+                    range.end = content.clip_point_utf16(range.end, Bias::Right);
+                    if range.start == range.end && range.end.column > 0 {
+                        range.start.column -= 1;
+                        range.start = content.clip_point_utf16(range.start, Bias::Left);
                     }
-                    Some((
+                }
+
+                diagnostics_by_group_id
+                    .entry(group_id)
+                    .or_insert(Vec::new())
+                    .push((
                         range,
                         Diagnostic {
                             severity: diagnostic.severity.unwrap_or(DiagnosticSeverity::ERROR),
-                            message: diagnostic.message,
+                            message: diagnostic.message.clone(),
+                            group_id,
+                            is_primary: false,
                         },
-                    ))
-                }),
+                    ));
+            }
+
+            content.anchor_range_multimap(
+                Bias::Left,
+                Bias::Right,
+                diagnostics_by_group_id
+                    .into_values()
+                    .flat_map(|mut diagnostics| {
+                        let primary_diagnostic =
+                            diagnostics.iter_mut().min_by_key(|d| d.1.severity).unwrap();
+                        primary_diagnostic.1.is_primary = true;
+                        diagnostics
+                    }),
             )
         };
 
@@ -786,7 +816,7 @@ impl Buffer {
 
     pub fn diagnostics_in_range<'a, T, O>(
         &'a self,
-        range: Range<T>,
+        search_range: Range<T>,
     ) -> impl Iterator<Item = (Range<O>, &Diagnostic)> + 'a
     where
         T: 'a + ToOffset,
@@ -794,7 +824,20 @@ impl Buffer {
     {
         let content = self.content();
         self.diagnostics
-            .intersecting_ranges(range, content, true)
+            .intersecting_ranges(search_range, content, true)
+            .map(move |(_, range, diagnostic)| (range, diagnostic))
+    }
+
+    pub fn diagnostic_group<'a, O>(
+        &'a self,
+        group_id: usize,
+    ) -> impl Iterator<Item = (Range<O>, &Diagnostic)> + 'a
+    where
+        O: 'a + FromAnchor,
+    {
+        let content = self.content();
+        self.diagnostics
+            .filter(content, move |diagnostic| diagnostic.group_id == group_id)
             .map(move |(_, range, diagnostic)| (range, diagnostic))
     }
 
@@ -1608,51 +1651,61 @@ impl Snapshot {
             .all(|chunk| chunk.matches(|c: char| !c.is_whitespace()).next().is_none())
     }
 
-    pub fn highlighted_text_for_range<T: ToOffset>(
-        &mut self,
+    pub fn chunks<'a, T: ToOffset>(
+        &'a self,
         range: Range<T>,
-    ) -> HighlightedChunks {
+        theme: Option<&'a SyntaxTheme>,
+    ) -> Chunks<'a> {
         let range = range.start.to_offset(&*self)..range.end.to_offset(&*self);
 
+        let mut highlights = None;
         let mut diagnostic_endpoints = Vec::<DiagnosticEndpoint>::new();
-        for (_, range, diagnostic) in
-            self.diagnostics
-                .intersecting_ranges(range.clone(), self.content(), true)
-        {
-            diagnostic_endpoints.push(DiagnosticEndpoint {
-                offset: range.start,
-                is_start: true,
-                severity: diagnostic.severity,
-            });
-            diagnostic_endpoints.push(DiagnosticEndpoint {
-                offset: range.end,
-                is_start: false,
-                severity: diagnostic.severity,
-            });
-        }
-        diagnostic_endpoints.sort_unstable_by_key(|endpoint| (endpoint.offset, !endpoint.is_start));
-        let diagnostic_endpoints = diagnostic_endpoints.into_iter().peekable();
+        if let Some(theme) = theme {
+            for (_, range, diagnostic) in
+                self.diagnostics
+                    .intersecting_ranges(range.clone(), self.content(), true)
+            {
+                diagnostic_endpoints.push(DiagnosticEndpoint {
+                    offset: range.start,
+                    is_start: true,
+                    severity: diagnostic.severity,
+                });
+                diagnostic_endpoints.push(DiagnosticEndpoint {
+                    offset: range.end,
+                    is_start: false,
+                    severity: diagnostic.severity,
+                });
+            }
+            diagnostic_endpoints
+                .sort_unstable_by_key(|endpoint| (endpoint.offset, !endpoint.is_start));
 
-        let chunks = self.text.as_rope().chunks_in_range(range.clone());
-        let highlights =
             if let Some((language, tree)) = self.language.as_ref().zip(self.tree.as_ref()) {
-                let captures = self.query_cursor.set_byte_range(range.clone()).captures(
+                let mut query_cursor = QueryCursorHandle::new();
+
+                // TODO - add a Tree-sitter API to remove the need for this.
+                let cursor = unsafe {
+                    std::mem::transmute::<_, &'static mut QueryCursor>(query_cursor.deref_mut())
+                };
+                let captures = cursor.set_byte_range(range.clone()).captures(
                     &language.highlights_query,
                     tree.root_node(),
                     TextProvider(self.text.as_rope()),
                 );
-
-                Some(Highlights {
+                highlights = Some(Highlights {
                     captures,
                     next_capture: None,
                     stack: Default::default(),
                     highlight_map: language.highlight_map(),
+                    _query_cursor: query_cursor,
+                    theme,
                 })
-            } else {
-                None
-            };
+            }
+        }
 
-        HighlightedChunks {
+        let diagnostic_endpoints = diagnostic_endpoints.into_iter().peekable();
+        let chunks = self.text.as_rope().chunks_in_range(range.clone());
+
+        Chunks {
             range,
             chunks,
             diagnostic_endpoints,
@@ -1673,7 +1726,6 @@ impl Clone for Snapshot {
             diagnostics: self.diagnostics.clone(),
             is_parsing: self.is_parsing,
             language: self.language.clone(),
-            query_cursor: QueryCursorHandle::new(),
         }
     }
 }
@@ -1704,7 +1756,9 @@ impl<'a> Iterator for ByteChunks<'a> {
     }
 }
 
-impl<'a> HighlightedChunks<'a> {
+unsafe impl<'a> Send for Chunks<'a> {}
+
+impl<'a> Chunks<'a> {
     pub fn seek(&mut self, offset: usize) {
         self.range.start = offset;
         self.chunks.seek(self.range.start);
@@ -1763,8 +1817,8 @@ impl<'a> HighlightedChunks<'a> {
     }
 }
 
-impl<'a> Iterator for HighlightedChunks<'a> {
-    type Item = HighlightedChunk<'a>;
+impl<'a> Iterator for Chunks<'a> {
+    type Item = Chunk<'a>;
 
     fn next(&mut self) -> Option<Self::Item> {
         let mut next_capture_start = usize::MAX;
@@ -1813,12 +1867,12 @@ impl<'a> Iterator for HighlightedChunks<'a> {
             let mut chunk_end = (self.chunks.offset() + chunk.len())
                 .min(next_capture_start)
                 .min(next_diagnostic_endpoint);
-            let mut highlight_id = HighlightId::default();
-            if let Some((parent_capture_end, parent_highlight_id)) =
-                self.highlights.as_ref().and_then(|h| h.stack.last())
-            {
-                chunk_end = chunk_end.min(*parent_capture_end);
-                highlight_id = *parent_highlight_id;
+            let mut highlight_style = None;
+            if let Some(highlights) = self.highlights.as_ref() {
+                if let Some((parent_capture_end, parent_highlight_id)) = highlights.stack.last() {
+                    chunk_end = chunk_end.min(*parent_capture_end);
+                    highlight_style = parent_highlight_id.style(highlights.theme);
+                }
             }
 
             let slice =
@@ -1828,9 +1882,9 @@ impl<'a> Iterator for HighlightedChunks<'a> {
                 self.chunks.next().unwrap();
             }
 
-            Some(HighlightedChunk {
+            Some(Chunk {
                 text: slice,
-                highlight_id,
+                highlight_style,
                 diagnostic: self.current_diagnostic_severity(),
             })
         } else {
@@ -1888,6 +1942,44 @@ impl ToTreeSitterPoint for Point {
     }
 }
 
+trait ToPointUtf16 {
+    fn to_point_utf16(self) -> PointUtf16;
+}
+
+impl ToPointUtf16 for lsp::Position {
+    fn to_point_utf16(self) -> PointUtf16 {
+        PointUtf16::new(self.line, self.character)
+    }
+}
+
+fn diagnostic_ranges<'a>(
+    diagnostic: &'a lsp::Diagnostic,
+    abs_path: Option<&'a Path>,
+) -> impl 'a + Iterator<Item = Range<PointUtf16>> {
+    diagnostic
+        .related_information
+        .iter()
+        .flatten()
+        .filter_map(move |info| {
+            if info.location.uri.to_file_path().ok()? == abs_path? {
+                let info_start = PointUtf16::new(
+                    info.location.range.start.line,
+                    info.location.range.start.character,
+                );
+                let info_end = PointUtf16::new(
+                    info.location.range.end.line,
+                    info.location.range.end.character,
+                );
+                Some(info_start..info_end)
+            } else {
+                None
+            }
+        })
+        .chain(Some(
+            diagnostic.range.start.to_point_utf16()..diagnostic.range.end.to_point_utf16(),
+        ))
+}
+
 fn contiguous_ranges(
     values: impl IntoIterator<Item = u32>,
     max_len: usize,

crates/language/src/proto.rs šŸ”—

@@ -141,6 +141,8 @@ pub fn serialize_diagnostics(map: &AnchorRangeMultimap<Diagnostic>) -> proto::Di
                     DiagnosticSeverity::HINT => proto::diagnostic::Severity::Hint,
                     _ => proto::diagnostic::Severity::None,
                 } as i32,
+                group_id: diagnostic.group_id as u64,
+                is_primary: diagnostic.is_primary,
             })
             .collect(),
     }
@@ -308,6 +310,8 @@ pub fn deserialize_diagnostics(message: proto::DiagnosticSet) -> AnchorRangeMult
                         proto::diagnostic::Severity::None => return None,
                     },
                     message: diagnostic.message,
+                    group_id: diagnostic.group_id as usize,
+                    is_primary: diagnostic.is_primary,
                 },
             ))
         }),

crates/language/src/tests.rs šŸ”—

@@ -482,14 +482,18 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) {
                     Point::new(3, 9)..Point::new(3, 11),
                     &Diagnostic {
                         severity: DiagnosticSeverity::ERROR,
-                        message: "undefined variable 'BB'".to_string()
+                        message: "undefined variable 'BB'".to_string(),
+                        group_id: 1,
+                        is_primary: true,
                     },
                 ),
                 (
                     Point::new(4, 9)..Point::new(4, 12),
                     &Diagnostic {
                         severity: DiagnosticSeverity::ERROR,
-                        message: "undefined variable 'CCC'".to_string()
+                        message: "undefined variable 'CCC'".to_string(),
+                        group_id: 2,
+                        is_primary: true,
                     }
                 )
             ]
@@ -545,14 +549,18 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) {
                     Point::new(2, 9)..Point::new(2, 12),
                     &Diagnostic {
                         severity: DiagnosticSeverity::WARNING,
-                        message: "unreachable statement".to_string()
+                        message: "unreachable statement".to_string(),
+                        group_id: 1,
+                        is_primary: true,
                     }
                 ),
                 (
                     Point::new(2, 9)..Point::new(2, 10),
                     &Diagnostic {
                         severity: DiagnosticSeverity::ERROR,
-                        message: "undefined variable 'A'".to_string()
+                        message: "undefined variable 'A'".to_string(),
+                        group_id: 0,
+                        is_primary: true,
                     },
                 )
             ]
@@ -620,14 +628,18 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) {
                     Point::new(2, 21)..Point::new(2, 22),
                     &Diagnostic {
                         severity: DiagnosticSeverity::ERROR,
-                        message: "undefined variable 'A'".to_string()
+                        message: "undefined variable 'A'".to_string(),
+                        group_id: 0,
+                        is_primary: true,
                     }
                 ),
                 (
                     Point::new(3, 9)..Point::new(3, 11),
                     &Diagnostic {
                         severity: DiagnosticSeverity::ERROR,
-                        message: "undefined variable 'BB'".to_string()
+                        message: "undefined variable 'BB'".to_string(),
+                        group_id: 1,
+                        is_primary: true,
                     },
                 )
             ]
@@ -694,12 +706,223 @@ async fn test_empty_diagnostic_ranges(mut cx: gpui::TestAppContext) {
     });
 }
 
-fn chunks_with_diagnostics<T: ToOffset>(
+#[gpui::test]
+async fn test_grouped_diagnostics(mut cx: gpui::TestAppContext) {
+    cx.add_model(|cx| {
+        let text = "
+            fn foo(mut v: Vec<usize>) {
+                for x in &v {
+                    v.push(1);
+                }
+            }
+        "
+        .unindent();
+
+        let file = FakeFile::new("/example.rs");
+        let mut buffer = Buffer::from_file(0, text, Box::new(file.clone()), cx);
+        buffer.set_language(Some(Arc::new(rust_lang())), None, cx);
+        let diagnostics = vec![
+            lsp::Diagnostic {
+                range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
+                severity: Some(DiagnosticSeverity::WARNING),
+                message: "error 1".to_string(),
+                related_information: Some(vec![lsp::DiagnosticRelatedInformation {
+                    location: lsp::Location {
+                        uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
+                        range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
+                    },
+                    message: "error 1 hint 1".to_string(),
+                }]),
+                ..Default::default()
+            },
+            lsp::Diagnostic {
+                range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
+                severity: Some(DiagnosticSeverity::HINT),
+                message: "error 1 hint 1".to_string(),
+                related_information: Some(vec![lsp::DiagnosticRelatedInformation {
+                    location: lsp::Location {
+                        uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
+                        range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
+                    },
+                    message: "original diagnostic".to_string(),
+                }]),
+                ..Default::default()
+            },
+            lsp::Diagnostic {
+                range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
+                severity: Some(DiagnosticSeverity::ERROR),
+                message: "error 2".to_string(),
+                related_information: Some(vec![
+                    lsp::DiagnosticRelatedInformation {
+                        location: lsp::Location {
+                            uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
+                            range: lsp::Range::new(
+                                lsp::Position::new(1, 13),
+                                lsp::Position::new(1, 15),
+                            ),
+                        },
+                        message: "error 2 hint 1".to_string(),
+                    },
+                    lsp::DiagnosticRelatedInformation {
+                        location: lsp::Location {
+                            uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
+                            range: lsp::Range::new(
+                                lsp::Position::new(1, 13),
+                                lsp::Position::new(1, 15),
+                            ),
+                        },
+                        message: "error 2 hint 2".to_string(),
+                    },
+                ]),
+                ..Default::default()
+            },
+            lsp::Diagnostic {
+                range: lsp::Range::new(lsp::Position::new(1, 13), lsp::Position::new(1, 15)),
+                severity: Some(DiagnosticSeverity::HINT),
+                message: "error 2 hint 1".to_string(),
+                related_information: Some(vec![lsp::DiagnosticRelatedInformation {
+                    location: lsp::Location {
+                        uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
+                        range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
+                    },
+                    message: "original diagnostic".to_string(),
+                }]),
+                ..Default::default()
+            },
+            lsp::Diagnostic {
+                range: lsp::Range::new(lsp::Position::new(1, 13), lsp::Position::new(1, 15)),
+                severity: Some(DiagnosticSeverity::HINT),
+                message: "error 2 hint 2".to_string(),
+                related_information: Some(vec![lsp::DiagnosticRelatedInformation {
+                    location: lsp::Location {
+                        uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
+                        range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
+                    },
+                    message: "original diagnostic".to_string(),
+                }]),
+                ..Default::default()
+            },
+        ];
+        buffer.update_diagnostics(None, diagnostics, cx).unwrap();
+        assert_eq!(
+            buffer
+                .diagnostics_in_range::<_, Point>(0..buffer.len())
+                .collect::<Vec<_>>(),
+            &[
+                (
+                    Point::new(1, 8)..Point::new(1, 9),
+                    &Diagnostic {
+                        severity: DiagnosticSeverity::WARNING,
+                        message: "error 1".to_string(),
+                        group_id: 0,
+                        is_primary: true,
+                    }
+                ),
+                (
+                    Point::new(1, 8)..Point::new(1, 9),
+                    &Diagnostic {
+                        severity: DiagnosticSeverity::HINT,
+                        message: "error 1 hint 1".to_string(),
+                        group_id: 0,
+                        is_primary: false,
+                    }
+                ),
+                (
+                    Point::new(1, 13)..Point::new(1, 15),
+                    &Diagnostic {
+                        severity: DiagnosticSeverity::HINT,
+                        message: "error 2 hint 1".to_string(),
+                        group_id: 1,
+                        is_primary: false,
+                    }
+                ),
+                (
+                    Point::new(1, 13)..Point::new(1, 15),
+                    &Diagnostic {
+                        severity: DiagnosticSeverity::HINT,
+                        message: "error 2 hint 2".to_string(),
+                        group_id: 1,
+                        is_primary: false,
+                    }
+                ),
+                (
+                    Point::new(2, 8)..Point::new(2, 17),
+                    &Diagnostic {
+                        severity: DiagnosticSeverity::ERROR,
+                        message: "error 2".to_string(),
+                        group_id: 1,
+                        is_primary: true,
+                    }
+                )
+            ]
+        );
+
+        assert_eq!(
+            buffer.diagnostic_group(0).collect::<Vec<_>>(),
+            &[
+                (
+                    Point::new(1, 8)..Point::new(1, 9),
+                    &Diagnostic {
+                        severity: DiagnosticSeverity::WARNING,
+                        message: "error 1".to_string(),
+                        group_id: 0,
+                        is_primary: true,
+                    }
+                ),
+                (
+                    Point::new(1, 8)..Point::new(1, 9),
+                    &Diagnostic {
+                        severity: DiagnosticSeverity::HINT,
+                        message: "error 1 hint 1".to_string(),
+                        group_id: 0,
+                        is_primary: false,
+                    }
+                ),
+            ]
+        );
+        assert_eq!(
+            buffer.diagnostic_group(1).collect::<Vec<_>>(),
+            &[
+                (
+                    Point::new(1, 13)..Point::new(1, 15),
+                    &Diagnostic {
+                        severity: DiagnosticSeverity::HINT,
+                        message: "error 2 hint 1".to_string(),
+                        group_id: 1,
+                        is_primary: false,
+                    }
+                ),
+                (
+                    Point::new(1, 13)..Point::new(1, 15),
+                    &Diagnostic {
+                        severity: DiagnosticSeverity::HINT,
+                        message: "error 2 hint 2".to_string(),
+                        group_id: 1,
+                        is_primary: false,
+                    }
+                ),
+                (
+                    Point::new(2, 8)..Point::new(2, 17),
+                    &Diagnostic {
+                        severity: DiagnosticSeverity::ERROR,
+                        message: "error 2".to_string(),
+                        group_id: 1,
+                        is_primary: true,
+                    }
+                )
+            ]
+        );
+
+        buffer
+    });
+}
+
+fn chunks_with_diagnostics<T: ToOffset + ToPoint>(
     buffer: &Buffer,
     range: Range<T>,
 ) -> Vec<(String, Option<DiagnosticSeverity>)> {
     let mut chunks: Vec<(String, Option<DiagnosticSeverity>)> = Vec::new();
-    for chunk in buffer.snapshot().highlighted_text_for_range(range) {
+    for chunk in buffer.snapshot().chunks(range, Some(&Default::default())) {
         if chunks
             .last()
             .map_or(false, |prev_chunk| prev_chunk.1 == chunk.diagnostic)
@@ -765,3 +988,80 @@ fn rust_lang() -> Language {
 fn empty(point: Point) -> Range<Point> {
     point..point
 }
+
+#[derive(Clone)]
+struct FakeFile {
+    abs_path: PathBuf,
+}
+
+impl FakeFile {
+    fn new(abs_path: impl Into<PathBuf>) -> Self {
+        Self {
+            abs_path: abs_path.into(),
+        }
+    }
+}
+
+impl File for FakeFile {
+    fn worktree_id(&self) -> usize {
+        todo!()
+    }
+
+    fn entry_id(&self) -> Option<usize> {
+        todo!()
+    }
+
+    fn mtime(&self) -> SystemTime {
+        SystemTime::now()
+    }
+
+    fn path(&self) -> &Arc<Path> {
+        todo!()
+    }
+
+    fn abs_path(&self) -> Option<PathBuf> {
+        Some(self.abs_path.clone())
+    }
+
+    fn full_path(&self) -> PathBuf {
+        todo!()
+    }
+
+    fn file_name(&self) -> Option<OsString> {
+        todo!()
+    }
+
+    fn is_deleted(&self) -> bool {
+        todo!()
+    }
+
+    fn save(
+        &self,
+        _: u64,
+        _: Rope,
+        _: clock::Global,
+        _: &mut MutableAppContext,
+    ) -> Task<Result<(clock::Global, SystemTime)>> {
+        todo!()
+    }
+
+    fn load_local(&self, _: &AppContext) -> Option<Task<Result<String>>> {
+        todo!()
+    }
+
+    fn buffer_updated(&self, _: u64, _: super::Operation, _: &mut MutableAppContext) {
+        todo!()
+    }
+
+    fn buffer_removed(&self, _: u64, _: &mut MutableAppContext) {
+        todo!()
+    }
+
+    fn boxed_clone(&self) -> Box<dyn File> {
+        todo!()
+    }
+
+    fn as_any(&self) -> &dyn Any {
+        todo!()
+    }
+}

crates/project/src/worktree.rs šŸ”—

@@ -3633,7 +3633,9 @@ mod tests {
                     Point::new(0, 9)..Point::new(0, 10),
                     &Diagnostic {
                         severity: lsp::DiagnosticSeverity::ERROR,
-                        message: "undefined variable 'A'".to_string()
+                        message: "undefined variable 'A'".to_string(),
+                        group_id: 0,
+                        is_primary: true
                     }
                 )]
             )

crates/rpc/proto/zed.proto šŸ”—

@@ -256,6 +256,8 @@ message Diagnostic {
     uint64 end = 2;
     Severity severity = 3;
     string message = 4;
+    uint64 group_id = 5;
+    bool is_primary = 6;
     enum Severity {
         None = 0;
         Error = 1;

crates/server/src/rpc.rs šŸ”—

@@ -1713,15 +1713,19 @@ mod tests {
                     (
                         Point::new(0, 4)..Point::new(0, 7),
                         &Diagnostic {
+                            group_id: 0,
                             message: "message 1".to_string(),
                             severity: lsp::DiagnosticSeverity::ERROR,
+                            is_primary: true
                         }
                     ),
                     (
                         Point::new(0, 10)..Point::new(0, 13),
                         &Diagnostic {
+                            group_id: 1,
                             severity: lsp::DiagnosticSeverity::WARNING,
-                            message: "message 2".to_string()
+                            message: "message 2".to_string(),
+                            is_primary: true
                         }
                     )
                 ]

crates/sum_tree/src/lib.rs šŸ”—

@@ -31,6 +31,12 @@ pub trait Summary: Default + Clone + fmt::Debug {
 
 pub trait Dimension<'a, S: Summary>: Clone + fmt::Debug + Default {
     fn add_summary(&mut self, _summary: &'a S, _: &S::Context);
+
+    fn from_summary(summary: &'a S, cx: &S::Context) -> Self {
+        let mut dimension = Self::default();
+        dimension.add_summary(summary, cx);
+        dimension
+    }
 }
 
 impl<'a, T: Summary> Dimension<'a, T> for T {

crates/theme/src/lib.rs šŸ”—

@@ -227,12 +227,21 @@ pub struct EditorStyle {
     pub line_number_active: Color,
     pub guest_selections: Vec<SelectionStyle>,
     pub syntax: Arc<SyntaxTheme>,
-    pub error_underline: Color,
-    pub warning_underline: Color,
-    #[serde(default)]
-    pub information_underline: Color,
-    #[serde(default)]
-    pub hint_underline: Color,
+    pub error_diagnostic: DiagnosticStyle,
+    pub invalid_error_diagnostic: DiagnosticStyle,
+    pub warning_diagnostic: DiagnosticStyle,
+    pub invalid_warning_diagnostic: DiagnosticStyle,
+    pub information_diagnostic: DiagnosticStyle,
+    pub invalid_information_diagnostic: DiagnosticStyle,
+    pub hint_diagnostic: DiagnosticStyle,
+    pub invalid_hint_diagnostic: DiagnosticStyle,
+}
+
+#[derive(Copy, Clone, Deserialize, Default)]
+pub struct DiagnosticStyle {
+    pub text: Color,
+    #[serde(flatten)]
+    pub block: BlockStyle,
 }
 
 #[derive(Clone, Copy, Default, Deserialize)]
@@ -251,6 +260,14 @@ pub struct InputEditorStyle {
     pub selection: SelectionStyle,
 }
 
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq)]
+pub struct BlockStyle {
+    pub background: Option<Color>,
+    pub border: Option<Color>,
+    pub gutter_background: Option<Color>,
+    pub gutter_border: Option<Color>,
+}
+
 impl EditorStyle {
     pub fn placeholder_text(&self) -> &TextStyle {
         self.placeholder_text.as_ref().unwrap_or(&self.text)
@@ -273,10 +290,14 @@ impl InputEditorStyle {
             line_number_active: Default::default(),
             guest_selections: Default::default(),
             syntax: Default::default(),
-            error_underline: Default::default(),
-            warning_underline: Default::default(),
-            information_underline: Default::default(),
-            hint_underline: Default::default(),
+            error_diagnostic: Default::default(),
+            invalid_error_diagnostic: Default::default(),
+            warning_diagnostic: Default::default(),
+            invalid_warning_diagnostic: Default::default(),
+            information_diagnostic: Default::default(),
+            invalid_information_diagnostic: Default::default(),
+            hint_diagnostic: Default::default(),
+            invalid_hint_diagnostic: Default::default(),
         }
     }
 }

crates/workspace/src/items.rs šŸ”—

@@ -258,15 +258,12 @@ impl DiagnosticMessage {
 
     fn update(&mut self, editor: ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
         let editor = editor.read(cx);
-        let cursor_position = editor
-            .selections::<usize>(cx)
-            .max_by_key(|selection| selection.id)
-            .unwrap()
-            .head();
+        let cursor_position = editor.newest_selection(cx).head();
         let new_diagnostic = editor
             .buffer()
             .read(cx)
             .diagnostics_in_range::<usize, usize>(cursor_position..cursor_position)
+            .filter(|(range, _)| !range.is_empty())
             .min_by_key(|(range, diagnostic)| (diagnostic.severity, range.len()))
             .map(|(_, diagnostic)| diagnostic.clone());
         if new_diagnostic != self.diagnostic {

crates/zed/assets/themes/_base.toml šŸ”—

@@ -173,7 +173,7 @@ corner_radius = 6
 
 [project_panel]
 extends = "$panel"
-padding.top = 6    # ($workspace.tab.height - $project_panel.entry.height) / 2
+padding.top = 6 # ($workspace.tab.height - $project_panel.entry.height) / 2
 
 [project_panel.entry]
 text = "$text.1"
@@ -235,7 +235,12 @@ line_number = "$text.2.color"
 line_number_active = "$text.0.color"
 selection = "$selection.host"
 guest_selections = "$selection.guests"
-error_underline = "$status.bad"
-warning_underline = "$status.warn"
-info_underline = "$status.info"
-hint_underline = "$status.info"
+error_color = "$status.bad"
+error_diagnostic = { text = "$status.bad" }
+invalid_error_diagnostic = { text = "$text.3.color" }
+warning_diagnostic = { text = "$status.warn" }
+invalid_warning_diagnostic = { text = "$text.3.color" }
+information_diagnostic = { text = "$status.info" }
+invalid_information_diagnostic = { text = "$text.3.color" }
+hint_diagnostic = { text = "$status.info" }
+invalid_hint_diagnostic = { text = "$text.3.color" }