Expand selections to Replace block boundaries (#20092)

Richard Feldman , Antonio Scandurra , Max Brunsfeld , Antonio , and Max created

Release Notes:

- N/A

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Antonio <antonio@zed.dev>
Co-authored-by: Max <max@zed.dev>

Change summary

crates/diagnostics/src/diagnostics_tests.rs |   1 
crates/editor/src/display_map.rs            | 118 ++++++++++++++
crates/editor/src/display_map/block_map.rs  |  61 ++++---
crates/editor/src/editor.rs                 |  31 ++-
crates/editor/src/editor_tests.rs           |  76 +++++++++
crates/editor/src/element.rs                |  57 +++++-
crates/editor/src/selections_collection.rs  | 184 ++++++++++++++++-------
crates/multi_buffer/src/multi_buffer.rs     |  58 +++++++
8 files changed, 469 insertions(+), 117 deletions(-)

Detailed changes

crates/diagnostics/src/diagnostics_tests.rs ๐Ÿ”—

@@ -986,6 +986,7 @@ fn editor_blocks(
                                     em_width: px(0.),
                                     max_width: px(0.),
                                     block_id,
+                                    selected: false,
                                     editor_style: &editor::EditorStyle::default(),
                                 });
                                 let element = element.downcast_mut::<Stateful<Div>>().unwrap();

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

@@ -660,7 +660,7 @@ impl DisplaySnapshot {
         new_start..new_end
     }
 
-    fn point_to_display_point(&self, point: MultiBufferPoint, bias: Bias) -> DisplayPoint {
+    pub fn point_to_display_point(&self, point: MultiBufferPoint, bias: Bias) -> DisplayPoint {
         let inlay_point = self.inlay_snapshot.to_inlay_point(point);
         let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias);
         let tab_point = self.tab_snapshot.to_tab_point(fold_point);
@@ -669,7 +669,7 @@ impl DisplaySnapshot {
         DisplayPoint(block_point)
     }
 
-    fn display_point_to_point(&self, point: DisplayPoint, bias: Bias) -> Point {
+    pub fn display_point_to_point(&self, point: DisplayPoint, bias: Bias) -> Point {
         self.inlay_snapshot
             .to_buffer_point(self.display_point_to_inlay_point(point, bias))
     }
@@ -691,7 +691,7 @@ impl DisplaySnapshot {
 
     fn display_point_to_inlay_point(&self, point: DisplayPoint, bias: Bias) -> InlayPoint {
         let block_point = point.0;
-        let wrap_point = self.block_snapshot.to_wrap_point(block_point);
+        let wrap_point = self.block_snapshot.to_wrap_point(block_point, bias);
         let tab_point = self.wrap_snapshot.to_tab_point(wrap_point);
         let fold_point = self.tab_snapshot.to_fold_point(tab_point, bias).0;
         fold_point.to_inlay_point(&self.fold_snapshot)
@@ -699,7 +699,7 @@ impl DisplaySnapshot {
 
     pub fn display_point_to_fold_point(&self, point: DisplayPoint, bias: Bias) -> FoldPoint {
         let block_point = point.0;
-        let wrap_point = self.block_snapshot.to_wrap_point(block_point);
+        let wrap_point = self.block_snapshot.to_wrap_point(block_point, bias);
         let tab_point = self.wrap_snapshot.to_tab_point(wrap_point);
         self.tab_snapshot.to_fold_point(tab_point, bias).0
     }
@@ -990,7 +990,7 @@ impl DisplaySnapshot {
     pub fn soft_wrap_indent(&self, display_row: DisplayRow) -> Option<u32> {
         let wrap_row = self
             .block_snapshot
-            .to_wrap_point(BlockPoint::new(display_row.0, 0))
+            .to_wrap_point(BlockPoint::new(display_row.0, 0), Bias::Left)
             .row();
         self.wrap_snapshot.soft_wrap_indent(wrap_row)
     }
@@ -1222,7 +1222,7 @@ impl DisplayPoint {
     }
 
     pub fn to_offset(self, map: &DisplaySnapshot, bias: Bias) -> usize {
-        let wrap_point = map.block_snapshot.to_wrap_point(self.0);
+        let wrap_point = map.block_snapshot.to_wrap_point(self.0, bias);
         let tab_point = map.wrap_snapshot.to_tab_point(wrap_point);
         let fold_point = map.tab_snapshot.to_fold_point(tab_point, bias).0;
         let inlay_point = fold_point.to_inlay_point(&map.fold_snapshot);
@@ -2048,6 +2048,112 @@ pub mod tests {
         );
     }
 
+    #[gpui::test]
+    async fn test_point_translation_with_replace_blocks(cx: &mut gpui::TestAppContext) {
+        cx.background_executor
+            .set_block_on_ticks(usize::MAX..=usize::MAX);
+
+        cx.update(|cx| init_test(cx, |_| {}));
+
+        let buffer = cx.update(|cx| MultiBuffer::build_simple("abcde\nfghij\nklmno\npqrst", cx));
+        let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
+        let map = cx.new_model(|cx| {
+            DisplayMap::new(
+                buffer.clone(),
+                font("Courier"),
+                px(16.0),
+                None,
+                true,
+                1,
+                1,
+                0,
+                FoldPlaceholder::test(),
+                cx,
+            )
+        });
+
+        let snapshot = map.update(cx, |map, cx| {
+            map.insert_blocks(
+                [BlockProperties {
+                    placement: BlockPlacement::Replace(
+                        buffer_snapshot.anchor_before(Point::new(1, 2))
+                            ..buffer_snapshot.anchor_after(Point::new(2, 3)),
+                    ),
+                    height: 4,
+                    style: BlockStyle::Fixed,
+                    render: Box::new(|_| div().into_any()),
+                    priority: 0,
+                }],
+                cx,
+            );
+            map.snapshot(cx)
+        });
+
+        assert_eq!(snapshot.text(), "abcde\n\n\n\n\npqrst");
+
+        let point_to_display_points = [
+            (Point::new(1, 0), DisplayPoint::new(DisplayRow(1), 0)),
+            (Point::new(2, 0), DisplayPoint::new(DisplayRow(1), 0)),
+            (Point::new(3, 0), DisplayPoint::new(DisplayRow(5), 0)),
+        ];
+        for (buffer_point, display_point) in point_to_display_points {
+            assert_eq!(
+                snapshot.point_to_display_point(buffer_point, Bias::Left),
+                display_point,
+                "point_to_display_point({:?}, Bias::Left)",
+                buffer_point
+            );
+            assert_eq!(
+                snapshot.point_to_display_point(buffer_point, Bias::Right),
+                display_point,
+                "point_to_display_point({:?}, Bias::Right)",
+                buffer_point
+            );
+        }
+
+        let display_points_to_points = [
+            (
+                DisplayPoint::new(DisplayRow(1), 0),
+                Point::new(1, 0),
+                Point::new(2, 5),
+            ),
+            (
+                DisplayPoint::new(DisplayRow(2), 0),
+                Point::new(1, 0),
+                Point::new(2, 5),
+            ),
+            (
+                DisplayPoint::new(DisplayRow(3), 0),
+                Point::new(1, 0),
+                Point::new(2, 5),
+            ),
+            (
+                DisplayPoint::new(DisplayRow(4), 0),
+                Point::new(1, 0),
+                Point::new(2, 5),
+            ),
+            (
+                DisplayPoint::new(DisplayRow(5), 0),
+                Point::new(3, 0),
+                Point::new(3, 0),
+            ),
+        ];
+        for (display_point, left_buffer_point, right_buffer_point) in display_points_to_points {
+            assert_eq!(
+                snapshot.display_point_to_point(display_point, Bias::Left),
+                left_buffer_point,
+                "display_point_to_point({:?}, Bias::Left)",
+                display_point
+            );
+            assert_eq!(
+                snapshot.display_point_to_point(display_point, Bias::Right),
+                right_buffer_point,
+                "display_point_to_point({:?}, Bias::Right)",
+                display_point
+            );
+        }
+    }
+
     // todo(linux) fails due to pixel differences in text rendering
     #[cfg(target_os = "macos")]
     #[gpui::test]

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

@@ -265,6 +265,7 @@ pub struct BlockContext<'a, 'b> {
     pub em_width: Pixels,
     pub line_height: Pixels,
     pub block_id: BlockId,
+    pub selected: bool,
     pub editor_style: &'b EditorStyle,
 }
 
@@ -1311,7 +1312,6 @@ impl BlockSnapshot {
                 let (output_start_row, input_start_row) = cursor.start();
                 let (output_end_row, input_end_row) = cursor.end(&());
                 let output_start = Point::new(output_start_row.0, 0);
-                let output_end = Point::new(output_end_row.0, 0);
                 let input_start = Point::new(input_start_row.0, 0);
                 let input_end = Point::new(input_end_row.0, 0);
 
@@ -1319,10 +1319,10 @@ impl BlockSnapshot {
                     Some(Block::Custom(block))
                         if matches!(block.placement, BlockPlacement::Replace(_)) =>
                     {
-                        if bias == Bias::Left {
+                        if ((bias == Bias::Left || search_left) && output_start <= point.0)
+                            || (!search_left && output_start >= point.0)
+                        {
                             return BlockPoint(output_start);
-                        } else {
-                            return BlockPoint(Point::new(output_end.row - 1, 0));
                         }
                     }
                     None => {
@@ -1364,12 +1364,7 @@ impl BlockSnapshot {
         cursor.seek(&WrapRow(wrap_point.row()), Bias::Right, &());
         if let Some(transform) = cursor.item() {
             if transform.block.is_some() {
-                let wrap_start = WrapPoint::new(cursor.start().0 .0, 0);
-                if wrap_start == wrap_point {
-                    BlockPoint::new(cursor.start().1 .0, 0)
-                } else {
-                    BlockPoint::new(cursor.end(&()).1 .0 - 1, 0)
-                }
+                BlockPoint::new(cursor.start().1 .0, 0)
             } else {
                 let (input_start_row, output_start_row) = cursor.start();
                 let input_start = Point::new(input_start_row.0, 0);
@@ -1382,7 +1377,7 @@ impl BlockSnapshot {
         }
     }
 
-    pub fn to_wrap_point(&self, block_point: BlockPoint) -> WrapPoint {
+    pub fn to_wrap_point(&self, block_point: BlockPoint, bias: Bias) -> WrapPoint {
         let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&());
         cursor.seek(&BlockRow(block_point.row), Bias::Right, &());
         if let Some(transform) = cursor.item() {
@@ -1391,7 +1386,9 @@ impl BlockSnapshot {
                     if block.place_below() {
                         let wrap_row = cursor.start().1 .0 - 1;
                         WrapPoint::new(wrap_row, self.wrap_snapshot.line_len(wrap_row))
-                    } else if block.place_above() || block_point.row == cursor.start().0 .0 {
+                    } else if block.place_above() {
+                        WrapPoint::new(cursor.start().1 .0, 0)
+                    } else if bias == Bias::Left {
                         WrapPoint::new(cursor.start().1 .0, 0)
                     } else {
                         let wrap_row = cursor.end(&()).1 .0 - 1;
@@ -1766,19 +1763,19 @@ mod tests {
         );
 
         assert_eq!(
-            snapshot.to_wrap_point(BlockPoint::new(0, 3)),
+            snapshot.to_wrap_point(BlockPoint::new(0, 3), Bias::Left),
             WrapPoint::new(0, 3)
         );
         assert_eq!(
-            snapshot.to_wrap_point(BlockPoint::new(1, 0)),
+            snapshot.to_wrap_point(BlockPoint::new(1, 0), Bias::Left),
             WrapPoint::new(1, 0)
         );
         assert_eq!(
-            snapshot.to_wrap_point(BlockPoint::new(3, 0)),
+            snapshot.to_wrap_point(BlockPoint::new(3, 0), Bias::Left),
             WrapPoint::new(1, 0)
         );
         assert_eq!(
-            snapshot.to_wrap_point(BlockPoint::new(7, 0)),
+            snapshot.to_wrap_point(BlockPoint::new(7, 0), Bias::Left),
             WrapPoint::new(3, 3)
         );
 
@@ -2616,10 +2613,15 @@ mod tests {
 
             // Ensure that conversion between block points and wrap points is stable.
             for row in 0..=blocks_snapshot.wrap_snapshot.max_point().row() {
-                let original_wrap_point = WrapPoint::new(row, 0);
-                let block_point = blocks_snapshot.to_block_point(original_wrap_point);
-                let wrap_point = blocks_snapshot.to_wrap_point(block_point);
-                assert_eq!(blocks_snapshot.to_block_point(wrap_point), block_point);
+                let wrap_point = WrapPoint::new(row, 0);
+                let block_point = blocks_snapshot.to_block_point(wrap_point);
+                let left_wrap_point = blocks_snapshot.to_wrap_point(block_point, Bias::Left);
+                let right_wrap_point = blocks_snapshot.to_wrap_point(block_point, Bias::Right);
+                assert_eq!(blocks_snapshot.to_block_point(left_wrap_point), block_point);
+                assert_eq!(
+                    blocks_snapshot.to_block_point(right_wrap_point),
+                    block_point
+                );
             }
 
             let mut block_point = BlockPoint::new(0, 0);
@@ -2627,10 +2629,12 @@ mod tests {
                 let left_point = blocks_snapshot.clip_point(block_point, Bias::Left);
                 let left_buffer_point = blocks_snapshot.to_point(left_point, Bias::Left);
                 assert_eq!(
-                    blocks_snapshot.to_block_point(blocks_snapshot.to_wrap_point(left_point)),
+                    blocks_snapshot
+                        .to_block_point(blocks_snapshot.to_wrap_point(left_point, Bias::Left)),
                     left_point,
-                    "wrap point: {:?}",
-                    blocks_snapshot.to_wrap_point(left_point)
+                    "block point: {:?}, wrap point: {:?}",
+                    block_point,
+                    blocks_snapshot.to_wrap_point(left_point, Bias::Left)
                 );
                 assert_eq!(
                     left_buffer_point,
@@ -2642,10 +2646,12 @@ mod tests {
                 let right_point = blocks_snapshot.clip_point(block_point, Bias::Right);
                 let right_buffer_point = blocks_snapshot.to_point(right_point, Bias::Right);
                 assert_eq!(
-                    blocks_snapshot.to_block_point(blocks_snapshot.to_wrap_point(right_point)),
+                    blocks_snapshot
+                        .to_block_point(blocks_snapshot.to_wrap_point(right_point, Bias::Right)),
                     right_point,
-                    "wrap point: {:?}",
-                    blocks_snapshot.to_wrap_point(right_point)
+                    "block point: {:?}, wrap point: {:?}",
+                    block_point,
+                    blocks_snapshot.to_wrap_point(right_point, Bias::Right)
                 );
                 assert_eq!(
                     right_buffer_point,
@@ -2681,7 +2687,8 @@ mod tests {
 
     impl BlockSnapshot {
         fn to_point(&self, point: BlockPoint, bias: Bias) -> Point {
-            self.wrap_snapshot.to_point(self.to_wrap_point(point), bias)
+            self.wrap_snapshot
+                .to_point(self.to_wrap_point(point, bias), bias)
         }
     }
 }

crates/editor/src/editor.rs ๐Ÿ”—

@@ -131,7 +131,9 @@ use project::{
 use rand::prelude::*;
 use rpc::{proto::*, ErrorExt};
 use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide};
-use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection};
+use selections_collection::{
+    resolve_selections, MutableSelectionsCollection, SelectionsCollection,
+};
 use serde::{Deserialize, Serialize};
 use settings::{update_settings_file, Settings, SettingsLocation, SettingsStore};
 use smallvec::SmallVec;
@@ -3484,8 +3486,8 @@ impl Editor {
             }
             let new_anchor_selections = new_selections.iter().map(|e| &e.0);
             let new_selection_deltas = new_selections.iter().map(|e| e.1);
-            let snapshot = this.buffer.read(cx).read(cx);
-            let new_selections = resolve_multiple::<usize, _>(new_anchor_selections, &snapshot)
+            let map = this.display_map.update(cx, |map, cx| map.snapshot(cx));
+            let new_selections = resolve_selections::<usize, _>(new_anchor_selections, &map)
                 .zip(new_selection_deltas)
                 .map(|(selection, delta)| Selection {
                     id: selection.id,
@@ -3498,18 +3500,20 @@ impl Editor {
 
             let mut i = 0;
             for (position, delta, selection_id, pair) in new_autoclose_regions {
-                let position = position.to_offset(&snapshot) + delta;
-                let start = snapshot.anchor_before(position);
-                let end = snapshot.anchor_after(position);
+                let position = position.to_offset(&map.buffer_snapshot) + delta;
+                let start = map.buffer_snapshot.anchor_before(position);
+                let end = map.buffer_snapshot.anchor_after(position);
                 while let Some(existing_state) = this.autoclose_regions.get(i) {
-                    match existing_state.range.start.cmp(&start, &snapshot) {
+                    match existing_state.range.start.cmp(&start, &map.buffer_snapshot) {
                         Ordering::Less => i += 1,
                         Ordering::Greater => break,
-                        Ordering::Equal => match end.cmp(&existing_state.range.end, &snapshot) {
-                            Ordering::Less => i += 1,
-                            Ordering::Equal => break,
-                            Ordering::Greater => break,
-                        },
+                        Ordering::Equal => {
+                            match end.cmp(&existing_state.range.end, &map.buffer_snapshot) {
+                                Ordering::Less => i += 1,
+                                Ordering::Equal => break,
+                                Ordering::Greater => break,
+                            }
+                        }
                     }
                 }
                 this.autoclose_regions.insert(
@@ -3522,7 +3526,6 @@ impl Editor {
                 );
             }
 
-            drop(snapshot);
             let had_active_inline_completion = this.has_active_inline_completion(cx);
             this.change_selections_inner(Some(Autoscroll::fit()), false, cx, |s| {
                 s.select(new_selections)
@@ -4038,7 +4041,7 @@ impl Editor {
                 }
             }
 
-            (selection.clone(), enclosing)
+            (selection, enclosing)
         })
     }
 

crates/editor/src/editor_tests.rs ๐Ÿ”—

@@ -3989,6 +3989,76 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
     });
 }
 
+#[gpui::test]
+async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut cx = EditorTestContext::new(cx).await;
+    cx.set_state(
+        &"
+            ห‡zero
+            one
+            two
+            three
+            four
+            five
+        "
+        .unindent(),
+    );
+
+    // Create a four-line block that replaces three lines of text.
+    cx.update_editor(|editor, cx| {
+        let snapshot = editor.snapshot(cx);
+        let snapshot = &snapshot.buffer_snapshot;
+        let placement = BlockPlacement::Replace(
+            snapshot.anchor_after(Point::new(1, 0))..snapshot.anchor_after(Point::new(3, 0)),
+        );
+        editor.insert_blocks(
+            [BlockProperties {
+                placement,
+                height: 4,
+                style: BlockStyle::Sticky,
+                render: Box::new(|_| gpui::div().into_any_element()),
+                priority: 0,
+            }],
+            None,
+            cx,
+        );
+    });
+
+    // Move down so that the cursor touches the block.
+    cx.update_editor(|editor, cx| {
+        editor.move_down(&Default::default(), cx);
+    });
+    cx.assert_editor_state(
+        &"
+            zero
+            ยซone
+            two
+            threeห‡ยป
+            four
+            five
+        "
+        .unindent(),
+    );
+
+    // Move down past the block.
+    cx.update_editor(|editor, cx| {
+        editor.move_down(&Default::default(), cx);
+    });
+    cx.assert_editor_state(
+        &"
+            zero
+            one
+            two
+            three
+            ห‡four
+            five
+        "
+        .unindent(),
+    );
+}
+
 #[gpui::test]
 fn test_transpose(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
@@ -4182,7 +4252,7 @@ async fn test_rewrap(cx: &mut TestAppContext) {
             // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
             // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
             // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
-            // porttitor id. Aliquam id accumsan eros.ห‡ห‡ห‡ห‡
+            // porttitor id. Aliquam id accumsan eros.ห‡
         "};
 
         cx.set_state(unwrapped_text);
@@ -4212,7 +4282,7 @@ async fn test_rewrap(cx: &mut TestAppContext) {
         let wrapped_text = indoc! {"
             // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
             // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
-            // auctor, eu lacinia sapien scelerisque.ห‡ห‡
+            // auctor, eu lacinia sapien scelerisque.ห‡
             //
             // Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas
             // tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
@@ -4220,7 +4290,7 @@ async fn test_rewrap(cx: &mut TestAppContext) {
             // molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque
             // nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas
             // porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id
-            // vulputate turpis porttitor id. Aliquam id accumsan eros.ห‡ห‡
+            // vulputate turpis porttitor id. Aliquam id accumsan eros.ห‡
         "};
 
         cx.set_state(unwrapped_text);

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

@@ -25,7 +25,7 @@ use crate::{
     MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
 };
 use client::ParticipantIndex;
-use collections::{BTreeMap, HashMap};
+use collections::{BTreeMap, HashMap, HashSet};
 use git::{blame::BlameEntry, diff::DiffHunkStatus, Oid};
 use gpui::Subscription;
 use gpui::{
@@ -809,10 +809,12 @@ impl EditorElement {
         cx.notify()
     }
 
+    #[allow(clippy::too_many_arguments)]
     fn layout_selections(
         &self,
         start_anchor: Anchor,
         end_anchor: Anchor,
+        local_selections: &[Selection<Point>],
         snapshot: &EditorSnapshot,
         start_row: DisplayRow,
         end_row: DisplayRow,
@@ -827,13 +829,9 @@ impl EditorElement {
         let mut newest_selection_head = None;
         self.editor.update(cx, |editor, cx| {
             if editor.show_local_selections {
-                let mut local_selections: Vec<Selection<Point>> = editor
-                    .selections
-                    .disjoint_in_range(start_anchor..end_anchor, cx);
-                local_selections.extend(editor.selections.pending(cx));
                 let mut layouts = Vec::new();
                 let newest = editor.selections.newest(cx);
-                for selection in local_selections.drain(..) {
+                for selection in local_selections.iter().cloned() {
                     let is_empty = selection.start == selection.end;
                     let is_newest = selection == newest;
 
@@ -996,6 +994,7 @@ impl EditorElement {
         &self,
         snapshot: &EditorSnapshot,
         selections: &[(PlayerColor, Vec<SelectionLayout>)],
+        block_start_rows: &HashSet<DisplayRow>,
         visible_display_row_range: Range<DisplayRow>,
         line_layouts: &[LineWithInvisibles],
         text_hitbox: &Hitbox,
@@ -1015,7 +1014,10 @@ impl EditorElement {
                     let cursor_position = selection.head;
 
                     let in_range = visible_display_row_range.contains(&cursor_position.row());
-                    if (selection.is_local && !editor.show_local_cursors(cx)) || !in_range {
+                    if (selection.is_local && !editor.show_local_cursors(cx))
+                        || !in_range
+                        || block_start_rows.contains(&cursor_position.row())
+                    {
                         continue;
                     }
 
@@ -2068,14 +2070,14 @@ impl EditorElement {
         editor_width: Pixels,
         scroll_width: &mut Pixels,
         resized_blocks: &mut HashMap<CustomBlockId, u32>,
+        selections: &[Selection<Point>],
         cx: &mut WindowContext,
     ) -> (AnyElement, Size<Pixels>) {
         let mut element = match block {
             Block::Custom(block) => {
-                let align_to = block
-                    .start()
-                    .to_point(&snapshot.buffer_snapshot)
-                    .to_display_point(snapshot);
+                let block_start = block.start().to_point(&snapshot.buffer_snapshot);
+                let block_end = block.end().to_point(&snapshot.buffer_snapshot);
+                let align_to = block_start.to_display_point(snapshot);
                 let anchor_x = text_x
                     + if rows.contains(&align_to.row()) {
                         line_layouts[align_to.row().minus(rows.start) as usize]
@@ -2085,6 +2087,18 @@ impl EditorElement {
                             .x_for_index(align_to.column() as usize)
                     };
 
+                let selected = selections
+                    .binary_search_by(|selection| {
+                        if selection.end <= block_start {
+                            Ordering::Less
+                        } else if selection.start >= block_end {
+                            Ordering::Greater
+                        } else {
+                            Ordering::Equal
+                        }
+                    })
+                    .is_ok();
+
                 div()
                     .size_full()
                     .child(block.render(&mut BlockContext {
@@ -2094,6 +2108,7 @@ impl EditorElement {
                         line_height,
                         em_width,
                         block_id,
+                        selected,
                         max_width: text_hitbox.size.width.max(*scroll_width),
                         editor_style: &self.style,
                     }))
@@ -2431,6 +2446,7 @@ impl EditorElement {
         text_x: Pixels,
         line_height: Pixels,
         line_layouts: &[LineWithInvisibles],
+        selections: &[Selection<Point>],
         cx: &mut WindowContext,
     ) -> Result<Vec<BlockLayout>, HashMap<CustomBlockId, u32>> {
         let (fixed_blocks, non_fixed_blocks) = snapshot
@@ -2467,6 +2483,7 @@ impl EditorElement {
                 editor_width,
                 scroll_width,
                 &mut resized_blocks,
+                selections,
                 cx,
             );
             fixed_block_max_width = fixed_block_max_width.max(element_size.width + em_width);
@@ -2511,6 +2528,7 @@ impl EditorElement {
                 editor_width,
                 scroll_width,
                 &mut resized_blocks,
+                selections,
                 cx,
             );
 
@@ -2556,6 +2574,7 @@ impl EditorElement {
                             editor_width,
                             scroll_width,
                             &mut resized_blocks,
+                            selections,
                             cx,
                         );
 
@@ -2584,6 +2603,7 @@ impl EditorElement {
     fn layout_blocks(
         &self,
         blocks: &mut Vec<BlockLayout>,
+        block_starts: &mut HashSet<DisplayRow>,
         hitbox: &Hitbox,
         line_height: Pixels,
         scroll_pixel_position: gpui::Point<Pixels>,
@@ -2591,6 +2611,7 @@ impl EditorElement {
     ) {
         for block in blocks {
             let mut origin = if let Some(row) = block.row {
+                block_starts.insert(row);
                 hitbox.origin
                     + point(
                         Pixels::ZERO,
@@ -5101,9 +5122,19 @@ impl Element for EditorElement {
                         cx,
                     );
 
+                    let local_selections: Vec<Selection<Point>> =
+                        self.editor.update(cx, |editor, cx| {
+                            let mut selections = editor
+                                .selections
+                                .disjoint_in_range(start_anchor..end_anchor, cx);
+                            selections.extend(editor.selections.pending(cx));
+                            selections
+                        });
+
                     let (selections, active_rows, newest_selection_head) = self.layout_selections(
                         start_anchor,
                         end_anchor,
+                        &local_selections,
                         &snapshot,
                         start_row,
                         end_row,
@@ -5176,6 +5207,7 @@ impl Element for EditorElement {
                             gutter_dimensions.full_width(),
                             line_height,
                             &line_layouts,
+                            &local_selections,
                             cx,
                         )
                     });
@@ -5315,9 +5347,11 @@ impl Element for EditorElement {
                         cx,
                     );
 
+                    let mut block_start_rows = HashSet::default();
                     cx.with_element_namespace("blocks", |cx| {
                         self.layout_blocks(
                             &mut blocks,
+                            &mut block_start_rows,
                             &hitbox,
                             line_height,
                             scroll_pixel_position,
@@ -5334,6 +5368,7 @@ impl Element for EditorElement {
                     let visible_cursors = self.layout_visible_cursors(
                         &snapshot,
                         &selections,
+                        &block_start_rows,
                         start_row..end_row,
                         &line_layouts,
                         &text_hitbox,

crates/editor/src/selections_collection.rs ๐Ÿ”—

@@ -1,6 +1,6 @@
 use std::{
     cell::Ref,
-    iter, mem,
+    cmp, iter, mem,
     ops::{Deref, DerefMut, Range, Sub},
     sync::Arc,
 };
@@ -98,9 +98,9 @@ impl SelectionsCollection {
         &self,
         cx: &mut AppContext,
     ) -> Option<Selection<D>> {
-        self.pending_anchor()
-            .as_ref()
-            .map(|pending| pending.map(|p| p.summary::<D>(&self.buffer(cx))))
+        let map = self.display_map(cx);
+        let selection = resolve_selections(self.pending_anchor().as_ref(), &map).next();
+        selection
     }
 
     pub(crate) fn pending_mode(&self) -> Option<SelectMode> {
@@ -111,12 +111,10 @@ impl SelectionsCollection {
     where
         D: 'a + TextDimension + Ord + Sub<D, Output = D>,
     {
+        let map = self.display_map(cx);
         let disjoint_anchors = &self.disjoint;
-        let mut disjoint =
-            resolve_multiple::<D, _>(disjoint_anchors.iter(), &self.buffer(cx)).peekable();
-
+        let mut disjoint = resolve_selections::<D, _>(disjoint_anchors.iter(), &map).peekable();
         let mut pending_opt = self.pending::<D>(cx);
-
         iter::from_fn(move || {
             if let Some(pending) = pending_opt.as_mut() {
                 while let Some(next_selection) = disjoint.peek() {
@@ -199,34 +197,57 @@ impl SelectionsCollection {
     where
         D: 'a + TextDimension + Ord + Sub<D, Output = D> + std::fmt::Debug,
     {
-        let buffer = self.buffer(cx);
+        let map = self.display_map(cx);
         let start_ix = match self
             .disjoint
-            .binary_search_by(|probe| probe.end.cmp(&range.start, &buffer))
+            .binary_search_by(|probe| probe.end.cmp(&range.start, &map.buffer_snapshot))
         {
             Ok(ix) | Err(ix) => ix,
         };
         let end_ix = match self
             .disjoint
-            .binary_search_by(|probe| probe.start.cmp(&range.end, &buffer))
+            .binary_search_by(|probe| probe.start.cmp(&range.end, &map.buffer_snapshot))
         {
             Ok(ix) => ix + 1,
             Err(ix) => ix,
         };
-        resolve_multiple(&self.disjoint[start_ix..end_ix], &buffer).collect()
+        resolve_selections(&self.disjoint[start_ix..end_ix], &map).collect()
     }
 
     pub fn all_display(
         &self,
         cx: &mut AppContext,
     ) -> (DisplaySnapshot, Vec<Selection<DisplayPoint>>) {
-        let display_map = self.display_map(cx);
-        let selections = self
-            .all::<Point>(cx)
-            .into_iter()
-            .map(|selection| selection.map(|point| point.to_display_point(&display_map)))
-            .collect();
-        (display_map, selections)
+        let map = self.display_map(cx);
+        let disjoint_anchors = &self.disjoint;
+        let mut disjoint = resolve_selections_display(disjoint_anchors.iter(), &map).peekable();
+        let mut pending_opt =
+            resolve_selections_display(self.pending_anchor().as_ref(), &map).next();
+        let selections = iter::from_fn(move || {
+            if let Some(pending) = pending_opt.as_mut() {
+                while let Some(next_selection) = disjoint.peek() {
+                    if pending.start <= next_selection.end && pending.end >= next_selection.start {
+                        let next_selection = disjoint.next().unwrap();
+                        if next_selection.start < pending.start {
+                            pending.start = next_selection.start;
+                        }
+                        if next_selection.end > pending.end {
+                            pending.end = next_selection.end;
+                        }
+                    } else if next_selection.end < pending.start {
+                        return disjoint.next();
+                    } else {
+                        break;
+                    }
+                }
+
+                pending_opt.take()
+            } else {
+                disjoint.next()
+            }
+        })
+        .collect();
+        (map, selections)
     }
 
     pub fn newest_anchor(&self) -> &Selection<Anchor> {
@@ -241,15 +262,18 @@ impl SelectionsCollection {
         &self,
         cx: &mut AppContext,
     ) -> Selection<D> {
-        let buffer = self.buffer(cx);
-        self.newest_anchor().map(|p| p.summary::<D>(&buffer))
+        let map = self.display_map(cx);
+        let selection = resolve_selections([self.newest_anchor()], &map)
+            .next()
+            .unwrap();
+        selection
     }
 
     pub fn newest_display(&self, cx: &mut AppContext) -> Selection<DisplayPoint> {
-        let display_map = self.display_map(cx);
-        let selection = self
-            .newest_anchor()
-            .map(|point| point.to_display_point(&display_map));
+        let map = self.display_map(cx);
+        let selection = resolve_selections_display([self.newest_anchor()], &map)
+            .next()
+            .unwrap();
         selection
     }
 
@@ -265,8 +289,11 @@ impl SelectionsCollection {
         &self,
         cx: &mut AppContext,
     ) -> Selection<D> {
-        let buffer = self.buffer(cx);
-        self.oldest_anchor().map(|p| p.summary::<D>(&buffer))
+        let map = self.display_map(cx);
+        let selection = resolve_selections([self.oldest_anchor()], &map)
+            .next()
+            .unwrap();
+        selection
     }
 
     pub fn first_anchor(&self) -> Selection<Anchor> {
@@ -538,9 +565,9 @@ impl<'a> MutableSelectionsCollection<'a> {
     }
 
     pub fn select_anchors(&mut self, selections: Vec<Selection<Anchor>>) {
-        let buffer = self.buffer.read(self.cx).snapshot(self.cx);
+        let map = self.display_map();
         let resolved_selections =
-            resolve_multiple::<usize, _>(&selections, &buffer).collect::<Vec<_>>();
+            resolve_selections::<usize, _>(&selections, &map).collect::<Vec<_>>();
         self.select(resolved_selections);
     }
 
@@ -650,20 +677,16 @@ impl<'a> MutableSelectionsCollection<'a> {
     ) {
         let mut changed = false;
         let display_map = self.display_map();
-        let selections = self
-            .collection
-            .all::<Point>(self.cx)
+        let (_, selections) = self.collection.all_display(self.cx);
+        let selections = selections
             .into_iter()
             .map(|selection| {
-                let mut moved_selection =
-                    selection.map(|point| point.to_display_point(&display_map));
+                let mut moved_selection = selection.clone();
                 move_selection(&display_map, &mut moved_selection);
-                let moved_selection =
-                    moved_selection.map(|display_point| display_point.to_point(&display_map));
                 if selection != moved_selection {
                     changed = true;
                 }
-                moved_selection
+                moved_selection.map(|display_point| display_point.to_point(&display_map))
             })
             .collect();
 
@@ -804,8 +827,8 @@ impl<'a> MutableSelectionsCollection<'a> {
             .collect();
 
         if !adjusted_disjoint.is_empty() {
-            let resolved_selections =
-                resolve_multiple(adjusted_disjoint.iter(), &self.buffer()).collect();
+            let map = self.display_map();
+            let resolved_selections = resolve_selections(adjusted_disjoint.iter(), &map).collect();
             self.select::<usize>(resolved_selections);
         }
 
@@ -849,27 +872,76 @@ impl<'a> DerefMut for MutableSelectionsCollection<'a> {
 }
 
 // Panics if passed selections are not in order
-pub(crate) fn resolve_multiple<'a, D, I>(
+fn resolve_selections_display<'a>(
+    selections: impl 'a + IntoIterator<Item = &'a Selection<Anchor>>,
+    map: &'a DisplaySnapshot,
+) -> impl 'a + Iterator<Item = Selection<DisplayPoint>> {
+    let (to_summarize, selections) = selections.into_iter().tee();
+    let mut summaries = map
+        .buffer_snapshot
+        .summaries_for_anchors::<Point, _>(to_summarize.flat_map(|s| [&s.start, &s.end]))
+        .into_iter();
+    let mut selections = selections
+        .map(move |s| {
+            let start = summaries.next().unwrap();
+            let end = summaries.next().unwrap();
+
+            let display_start = map.point_to_display_point(start, Bias::Left);
+            let display_end = if start == end {
+                map.point_to_display_point(end, Bias::Right)
+            } else {
+                map.point_to_display_point(end, Bias::Left)
+            };
+
+            Selection {
+                id: s.id,
+                start: display_start,
+                end: display_end,
+                reversed: s.reversed,
+                goal: s.goal,
+            }
+        })
+        .peekable();
+    iter::from_fn(move || {
+        let mut selection = selections.next()?;
+        while let Some(next_selection) = selections.peek() {
+            if selection.end >= next_selection.start {
+                selection.end = cmp::max(selection.end, next_selection.end);
+                selections.next();
+            } else {
+                break;
+            }
+        }
+        Some(selection)
+    })
+}
+
+// Panics if passed selections are not in order
+pub(crate) fn resolve_selections<'a, D, I>(
     selections: I,
-    snapshot: &MultiBufferSnapshot,
+    map: &'a DisplaySnapshot,
 ) -> impl 'a + Iterator<Item = Selection<D>>
 where
-    D: TextDimension + Ord + Sub<D, Output = D>,
+    D: TextDimension + Clone + Ord + Sub<D, Output = D>,
     I: 'a + IntoIterator<Item = &'a Selection<Anchor>>,
 {
-    let (to_summarize, selections) = selections.into_iter().tee();
-    let mut summaries = snapshot
-        .summaries_for_anchors::<D, _>(
-            to_summarize
-                .flat_map(|s| [&s.start, &s.end])
-                .collect::<Vec<_>>(),
-        )
-        .into_iter();
-    selections.map(move |s| Selection {
-        id: s.id,
-        start: summaries.next().unwrap(),
-        end: summaries.next().unwrap(),
-        reversed: s.reversed,
-        goal: s.goal,
+    let (to_convert, selections) = resolve_selections_display(selections, map).tee();
+    let mut converted_endpoints =
+        map.buffer_snapshot
+            .dimensions_from_points::<D>(to_convert.flat_map(|s| {
+                let start = map.display_point_to_point(s.start, Bias::Left);
+                let end = map.display_point_to_point(s.end, Bias::Right);
+                [start, end]
+            }));
+    selections.map(move |s| {
+        let start = converted_endpoints.next().unwrap();
+        let end = converted_endpoints.next().unwrap();
+        Selection {
+            id: s.id,
+            start,
+            end,
+            reversed: s.reversed,
+            goal: s.goal,
+        }
     })
 }

crates/multi_buffer/src/multi_buffer.rs ๐Ÿ”—

@@ -3083,6 +3083,58 @@ impl MultiBufferSnapshot {
         summaries
     }
 
+    pub fn dimensions_from_points<'a, D>(
+        &'a self,
+        points: impl 'a + IntoIterator<Item = Point>,
+    ) -> impl 'a + Iterator<Item = D>
+    where
+        D: TextDimension,
+    {
+        let mut cursor = self.excerpts.cursor::<TextSummary>(&());
+        let mut memoized_source_start: Option<Point> = None;
+        let mut points = points.into_iter();
+        std::iter::from_fn(move || {
+            let point = points.next()?;
+
+            // Clear the memoized source start if the point is in a different excerpt than previous.
+            if memoized_source_start.map_or(false, |_| point >= cursor.end(&()).lines) {
+                memoized_source_start = None;
+            }
+
+            // Now determine where the excerpt containing the point starts in its source buffer.
+            // We'll use this value to calculate overshoot next.
+            let source_start = if let Some(source_start) = memoized_source_start {
+                source_start
+            } else {
+                cursor.seek_forward(&point, Bias::Right, &());
+                if let Some(excerpt) = cursor.item() {
+                    let source_start = excerpt.range.context.start.to_point(&excerpt.buffer);
+                    memoized_source_start = Some(source_start);
+                    source_start
+                } else {
+                    return Some(D::from_text_summary(cursor.start()));
+                }
+            };
+
+            // First, assume the output dimension is at least the start of the excerpt containing the point
+            let mut output = D::from_text_summary(cursor.start());
+
+            // If the point lands within its excerpt, calculate and add the overshoot in dimension D.
+            if let Some(excerpt) = cursor.item() {
+                let overshoot = point - cursor.start().lines;
+                if !overshoot.is_zero() {
+                    let end_in_excerpt = source_start + overshoot;
+                    output.add_assign(
+                        &excerpt
+                            .buffer
+                            .text_summary_for_range::<D, _>(source_start..end_in_excerpt),
+                    );
+                }
+            }
+            Some(output)
+        })
+    }
+
     pub fn refresh_anchors<'a, I>(&'a self, anchors: I) -> Vec<(usize, Anchor, bool)>
     where
         I: 'a + IntoIterator<Item = &'a Anchor>,
@@ -4706,6 +4758,12 @@ impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, ExcerptSummary> for usize {
     }
 }
 
+impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, TextSummary> for Point {
+    fn cmp(&self, cursor_location: &TextSummary, _: &()) -> cmp::Ordering {
+        Ord::cmp(self, &cursor_location.lines)
+    }
+}
+
 impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, Option<&'a Locator>> for Locator {
     fn cmp(&self, cursor_location: &Option<&'a Locator>, _: &()) -> cmp::Ordering {
         Ord::cmp(&Some(self), cursor_location)