Introduce "Near" block type (#28032)

Conrad Irwin and Antonio Scandurra created

A "Near" block acts similarly to a "Below" block, but can (if it's
height is <= one line height) be shown on the end of the preceding line
instead of adding an entire blank line to the editor.

You can test it out by pasting this into `go_to_diagnostic_impl` and
then press `F8`
```
        let buffer = self.buffer.read(cx).snapshot(cx);
        let selection = self.selections.newest_anchor();

        self.display_map.update(cx, |display_map, cx| {
            display_map.insert_blocks(
                [BlockProperties {
                    placement: BlockPlacement::Near(selection.start),
                    height: Some(1),
                    style: BlockStyle::Flex,
                    render: Arc::new(|_| {
                        div()
                            .w(px(100.))
                            .h(px(16.))
                            .bg(gpui::hsla(0., 0., 1., 0.5))
                            .into_any_element()
                    }),
                    priority: 0,
                }],
                cx,
            )
        });
        return;
```

Release Notes:

- N/A

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>

Change summary

crates/agent/src/inline_assistant.rs                  |   6 
crates/assistant/src/inline_assistant.rs              |   6 
crates/assistant_context_editor/src/context_editor.rs |   4 
crates/diagnostics/src/diagnostics.rs                 |  10 
crates/editor/src/display_map.rs                      |  14 
crates/editor/src/display_map/block_map.rs            | 225 ++++++------
crates/editor/src/display_map/fold_map.rs             |   1 
crates/editor/src/editor.rs                           |   7 
crates/editor/src/editor_tests.rs                     |   4 
crates/editor/src/element.rs                          | 154 ++++++--
crates/multi_buffer/src/anchor.rs                     |   4 
crates/repl/src/session.rs                            |   2 
12 files changed, 246 insertions(+), 191 deletions(-)

Detailed changes

crates/agent/src/inline_assistant.rs 🔗

@@ -621,14 +621,14 @@ impl InlineAssistant {
             BlockProperties {
                 style: BlockStyle::Sticky,
                 placement: BlockPlacement::Above(range.start),
-                height: prompt_editor_height,
+                height: Some(prompt_editor_height),
                 render: build_assist_editor_renderer(prompt_editor),
                 priority: 0,
             },
             BlockProperties {
                 style: BlockStyle::Sticky,
                 placement: BlockPlacement::Below(range.end),
-                height: 0,
+                height: None,
                 render: Arc::new(|cx| {
                     v_flex()
                         .h_full()
@@ -1392,7 +1392,7 @@ impl InlineAssistant {
                     deleted_lines_editor.update(cx, |editor, cx| editor.max_point(cx).row().0 + 1);
                 new_blocks.push(BlockProperties {
                     placement: BlockPlacement::Above(new_row),
-                    height,
+                    height: Some(height),
                     style: BlockStyle::Flex,
                     render: Arc::new(move |cx| {
                         div()

crates/assistant/src/inline_assistant.rs 🔗

@@ -527,14 +527,14 @@ impl InlineAssistant {
             BlockProperties {
                 style: BlockStyle::Sticky,
                 placement: BlockPlacement::Above(range.start),
-                height: prompt_editor_height,
+                height: Some(prompt_editor_height),
                 render: build_assist_editor_renderer(prompt_editor),
                 priority: 0,
             },
             BlockProperties {
                 style: BlockStyle::Sticky,
                 placement: BlockPlacement::Below(range.end),
-                height: 0,
+                height: None,
                 render: Arc::new(|cx| {
                     v_flex()
                         .h_full()
@@ -1301,7 +1301,7 @@ impl InlineAssistant {
                     deleted_lines_editor.update(cx, |editor, cx| editor.max_point(cx).row().0 + 1);
                 new_blocks.push(BlockProperties {
                     placement: BlockPlacement::Above(new_row),
-                    height,
+                    height: Some(height),
                     style: BlockStyle::Flex,
                     render: Arc::new(move |cx| {
                         div()

crates/assistant_context_editor/src/context_editor.rs 🔗

@@ -1562,7 +1562,7 @@ impl ContextEditor {
                 })
             };
             let create_block_properties = |message: &Message| BlockProperties {
-                height: 2,
+                height: Some(2),
                 style: BlockStyle::Sticky,
                 placement: BlockPlacement::Above(
                     buffer
@@ -2111,7 +2111,7 @@ impl ContextEditor {
                     let image = render_image.clone();
                     anchor.is_valid(&buffer).then(|| BlockProperties {
                         placement: BlockPlacement::Above(anchor),
-                        height: MAX_HEIGHT_IN_LINES,
+                        height: Some(MAX_HEIGHT_IN_LINES),
                         style: BlockStyle::Sticky,
                         render: Arc::new(move |cx| {
                             let image_size = size_for_image(

crates/diagnostics/src/diagnostics.rs 🔗

@@ -535,7 +535,7 @@ impl ProjectDiagnosticsEditor {
                                 group_state.block_count += 1;
                                 blocks_to_add.push(BlockProperties {
                                     placement: BlockPlacement::Above(header_position),
-                                    height: 2,
+                                    height: Some(2),
                                     style: BlockStyle::Sticky,
                                     render: diagnostic_header_renderer(primary),
                                     priority: 0,
@@ -557,7 +557,9 @@ impl ProjectDiagnosticsEditor {
                                             excerpt_id,
                                             entry.range.start,
                                         )),
-                                        height: diagnostic.message.matches('\n').count() as u32 + 1,
+                                        height: Some(
+                                            diagnostic.message.matches('\n').count() as u32 + 1,
+                                        ),
                                         style: BlockStyle::Fixed,
                                         render: diagnostic_block_renderer(diagnostic, None, true),
                                         priority: 0,
@@ -613,9 +615,9 @@ impl ProjectDiagnosticsEditor {
                                     excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor)?,
                                 )
                             }
-                            BlockPlacement::Replace(_) => {
+                            BlockPlacement::Replace(_) | BlockPlacement::Near(_) => {
                                 unreachable!(
-                                    "no Replace block should have been pushed to blocks_to_add"
+                                    "no Near/Replace block should have been pushed to blocks_to_add"
                                 )
                             }
                         };

crates/editor/src/display_map.rs 🔗

@@ -255,7 +255,7 @@ impl DisplayMap {
                     BlockProperties {
                         placement: BlockPlacement::Replace(start..=end),
                         render,
-                        height,
+                        height: Some(height),
                         style,
                         priority,
                     }
@@ -1591,7 +1591,7 @@ pub mod tests {
                                     BlockProperties {
                                         placement,
                                         style: BlockStyle::Fixed,
-                                        height,
+                                        height: Some(height),
                                         render: Arc::new(|_| div().into_any()),
                                         priority,
                                     }
@@ -1953,7 +1953,7 @@ pub mod tests {
                     placement: BlockPlacement::Above(
                         buffer_snapshot.anchor_before(Point::new(0, 0)),
                     ),
-                    height: 2,
+                    height: Some(2),
                     style: BlockStyle::Sticky,
                     render: Arc::new(|_| div().into_any()),
                     priority: 0,
@@ -2148,7 +2148,7 @@ pub mod tests {
                         placement: BlockPlacement::Below(
                             buffer_snapshot.anchor_before(Point::new(1, 0)),
                         ),
-                        height: 1,
+                        height: Some(1),
                         style: BlockStyle::Sticky,
                         render: Arc::new(|_| div().into_any()),
                         priority: 0,
@@ -2157,7 +2157,7 @@ pub mod tests {
                         placement: BlockPlacement::Below(
                             buffer_snapshot.anchor_before(Point::new(2, 0)),
                         ),
-                        height: 0,
+                        height: None,
                         style: BlockStyle::Sticky,
                         render: Arc::new(|_| div().into_any()),
                         priority: 0,
@@ -2262,7 +2262,7 @@ pub mod tests {
                     placement: BlockPlacement::Below(
                         buffer_snapshot.anchor_before(Point::new(1, 0)),
                     ),
-                    height: 1,
+                    height: Some(1),
                     style: BlockStyle::Sticky,
                     render: Arc::new(|_| div().into_any()),
                     priority: 0,
@@ -2336,7 +2336,7 @@ pub mod tests {
                         buffer_snapshot.anchor_before(Point::new(1, 2))
                             ..=buffer_snapshot.anchor_after(Point::new(2, 3)),
                     ),
-                    height: 4,
+                    height: Some(4),
                     style: BlockStyle::Fixed,
                     render: Arc::new(|_| div().into_any()),
                     priority: 0,

crates/editor/src/display_map/block_map.rs 🔗

@@ -84,6 +84,7 @@ pub type RenderBlock = Arc<dyn Send + Sync + Fn(&mut BlockContext) -> AnyElement
 pub enum BlockPlacement<T> {
     Above(T),
     Below(T),
+    Near(T),
     Replace(RangeInclusive<T>),
 }
 
@@ -92,6 +93,7 @@ impl<T> BlockPlacement<T> {
         match self {
             BlockPlacement::Above(position) => position,
             BlockPlacement::Below(position) => position,
+            BlockPlacement::Near(position) => position,
             BlockPlacement::Replace(range) => range.start(),
         }
     }
@@ -100,6 +102,7 @@ impl<T> BlockPlacement<T> {
         match self {
             BlockPlacement::Above(position) => position,
             BlockPlacement::Below(position) => position,
+            BlockPlacement::Near(position) => position,
             BlockPlacement::Replace(range) => range.end(),
         }
     }
@@ -108,6 +111,7 @@ impl<T> BlockPlacement<T> {
         match self {
             BlockPlacement::Above(position) => BlockPlacement::Above(position),
             BlockPlacement::Below(position) => BlockPlacement::Below(position),
+            BlockPlacement::Near(position) => BlockPlacement::Near(position),
             BlockPlacement::Replace(range) => BlockPlacement::Replace(range.start()..=range.end()),
         }
     }
@@ -116,44 +120,30 @@ impl<T> BlockPlacement<T> {
         match self {
             BlockPlacement::Above(position) => BlockPlacement::Above(f(position)),
             BlockPlacement::Below(position) => BlockPlacement::Below(f(position)),
+            BlockPlacement::Near(position) => BlockPlacement::Near(f(position)),
             BlockPlacement::Replace(range) => {
                 let (start, end) = range.into_inner();
                 BlockPlacement::Replace(f(start)..=f(end))
             }
         }
     }
+
+    fn sort_order(&self) -> u8 {
+        match self {
+            BlockPlacement::Above(_) => 0,
+            BlockPlacement::Replace(_) => 1,
+            BlockPlacement::Near(_) => 2,
+            BlockPlacement::Below(_) => 3,
+        }
+    }
 }
 
 impl BlockPlacement<Anchor> {
     fn cmp(&self, other: &Self, buffer: &MultiBufferSnapshot) -> Ordering {
-        match (self, other) {
-            (BlockPlacement::Above(anchor_a), BlockPlacement::Above(anchor_b))
-            | (BlockPlacement::Below(anchor_a), BlockPlacement::Below(anchor_b)) => {
-                anchor_a.cmp(anchor_b, buffer)
-            }
-            (BlockPlacement::Above(anchor_a), BlockPlacement::Below(anchor_b)) => {
-                anchor_a.cmp(anchor_b, buffer).then(Ordering::Less)
-            }
-            (BlockPlacement::Below(anchor_a), BlockPlacement::Above(anchor_b)) => {
-                anchor_a.cmp(anchor_b, buffer).then(Ordering::Greater)
-            }
-            (BlockPlacement::Above(anchor), BlockPlacement::Replace(range)) => {
-                anchor.cmp(range.start(), buffer).then(Ordering::Less)
-            }
-            (BlockPlacement::Replace(range), BlockPlacement::Above(anchor)) => {
-                range.start().cmp(anchor, buffer).then(Ordering::Greater)
-            }
-            (BlockPlacement::Below(anchor), BlockPlacement::Replace(range)) => {
-                anchor.cmp(range.start(), buffer).then(Ordering::Greater)
-            }
-            (BlockPlacement::Replace(range), BlockPlacement::Below(anchor)) => {
-                range.start().cmp(anchor, buffer).then(Ordering::Less)
-            }
-            (BlockPlacement::Replace(range_a), BlockPlacement::Replace(range_b)) => range_a
-                .start()
-                .cmp(range_b.start(), buffer)
-                .then_with(|| range_b.end().cmp(range_a.end(), buffer)),
-        }
+        self.start()
+            .cmp(other.start(), buffer)
+            .then_with(|| other.end().cmp(self.end(), buffer))
+            .then_with(|| self.sort_order().cmp(&other.sort_order()))
     }
 
     fn to_wrap_row(&self, wrap_snapshot: &WrapSnapshot) -> Option<BlockPlacement<WrapRow>> {
@@ -165,6 +155,12 @@ impl BlockPlacement<Anchor> {
                 let wrap_row = WrapRow(wrap_snapshot.make_wrap_point(position, Bias::Left).row());
                 Some(BlockPlacement::Above(wrap_row))
             }
+            BlockPlacement::Near(position) => {
+                let mut position = position.to_point(buffer_snapshot);
+                position.column = buffer_snapshot.line_len(MultiBufferRow(position.row));
+                let wrap_row = WrapRow(wrap_snapshot.make_wrap_point(position, Bias::Left).row());
+                Some(BlockPlacement::Near(wrap_row))
+            }
             BlockPlacement::Below(position) => {
                 let mut position = position.to_point(buffer_snapshot);
                 position.column = buffer_snapshot.line_len(MultiBufferRow(position.row));
@@ -193,7 +189,7 @@ impl BlockPlacement<Anchor> {
 pub struct CustomBlock {
     id: CustomBlockId,
     placement: BlockPlacement<Anchor>,
-    height: u32,
+    height: Option<u32>,
     style: BlockStyle,
     render: Arc<Mutex<RenderBlock>>,
     priority: usize,
@@ -201,7 +197,9 @@ pub struct CustomBlock {
 
 pub struct BlockProperties<P> {
     pub placement: BlockPlacement<P>,
-    pub height: u32,
+    // None if the block takes up no space
+    // (e.g. a horizontal line)
+    pub height: Option<u32>,
     pub style: BlockStyle,
     pub render: RenderBlock,
     pub priority: usize,
@@ -302,9 +300,16 @@ impl Block {
         }
     }
 
+    pub fn has_height(&self) -> bool {
+        match self {
+            Block::Custom(block) => block.height.is_some(),
+            Block::ExcerptBoundary { .. } | Block::FoldedBuffer { .. } => true,
+        }
+    }
+
     pub fn height(&self) -> u32 {
         match self {
-            Block::Custom(block) => block.height,
+            Block::Custom(block) => block.height.unwrap_or(0),
             Block::ExcerptBoundary { height, .. } | Block::FoldedBuffer { height, .. } => *height,
         }
     }
@@ -324,9 +329,20 @@ impl Block {
         }
     }
 
+    pub fn place_near(&self) -> bool {
+        match self {
+            Block::Custom(block) => matches!(block.placement, BlockPlacement::Near(_)),
+            Block::FoldedBuffer { .. } => false,
+            Block::ExcerptBoundary { .. } => false,
+        }
+    }
+
     fn place_below(&self) -> bool {
         match self {
-            Block::Custom(block) => matches!(block.placement, BlockPlacement::Below(_)),
+            Block::Custom(block) => matches!(
+                block.placement,
+                BlockPlacement::Below(_) | BlockPlacement::Near(_)
+            ),
             Block::FoldedBuffer { .. } => false,
             Block::ExcerptBoundary { .. } => false,
         }
@@ -669,7 +685,7 @@ impl BlockMap {
                     BlockPlacement::Above(position) => {
                         rows_before_block = position.0 - new_transforms.summary().input_rows;
                     }
-                    BlockPlacement::Below(position) => {
+                    BlockPlacement::Near(position) | BlockPlacement::Below(position) => {
                         rows_before_block = (position.0 + 1) - new_transforms.summary().input_rows;
                     }
                     BlockPlacement::Replace(range) => {
@@ -803,60 +819,39 @@ impl BlockMap {
 
     fn sort_blocks(blocks: &mut Vec<(BlockPlacement<WrapRow>, Block)>) {
         blocks.sort_unstable_by(|(placement_a, block_a), (placement_b, block_b)| {
-            let placement_comparison = match (placement_a, placement_b) {
-                (BlockPlacement::Above(row_a), BlockPlacement::Above(row_b))
-                | (BlockPlacement::Below(row_a), BlockPlacement::Below(row_b)) => row_a.cmp(row_b),
-                (BlockPlacement::Above(row_a), BlockPlacement::Below(row_b)) => {
-                    row_a.cmp(row_b).then(Ordering::Less)
-                }
-                (BlockPlacement::Below(row_a), BlockPlacement::Above(row_b)) => {
-                    row_a.cmp(row_b).then(Ordering::Greater)
-                }
-                (BlockPlacement::Above(row), BlockPlacement::Replace(range)) => {
-                    row.cmp(range.start()).then(Ordering::Greater)
-                }
-                (BlockPlacement::Replace(range), BlockPlacement::Above(row)) => {
-                    range.start().cmp(row).then(Ordering::Less)
-                }
-                (BlockPlacement::Below(row), BlockPlacement::Replace(range)) => {
-                    row.cmp(range.start()).then(Ordering::Greater)
-                }
-                (BlockPlacement::Replace(range), BlockPlacement::Below(row)) => {
-                    range.start().cmp(row).then(Ordering::Less)
-                }
-                (BlockPlacement::Replace(range_a), BlockPlacement::Replace(range_b)) => range_a
-                    .start()
-                    .cmp(range_b.start())
-                    .then_with(|| range_b.end().cmp(range_a.end()))
-                    .then_with(|| {
-                        if block_a.is_header() {
-                            Ordering::Less
-                        } else if block_b.is_header() {
-                            Ordering::Greater
-                        } else {
-                            Ordering::Equal
-                        }
-                    }),
-            };
-            placement_comparison.then_with(|| match (block_a, block_b) {
-                (
-                    Block::ExcerptBoundary {
-                        excerpt: excerpt_a, ..
-                    },
-                    Block::ExcerptBoundary {
-                        excerpt: excerpt_b, ..
-                    },
-                ) => Some(excerpt_a.id).cmp(&Some(excerpt_b.id)),
-                (Block::ExcerptBoundary { .. }, Block::Custom(_)) => Ordering::Less,
-                (Block::Custom(_), Block::ExcerptBoundary { .. }) => Ordering::Greater,
-                (Block::Custom(block_a), Block::Custom(block_b)) => block_a
-                    .priority
-                    .cmp(&block_b.priority)
-                    .then_with(|| block_a.id.cmp(&block_b.id)),
-                _ => {
-                    unreachable!()
-                }
-            })
+            placement_a
+                .start()
+                .cmp(placement_b.start())
+                .then_with(|| placement_b.end().cmp(placement_a.end()))
+                .then_with(|| {
+                    if block_a.is_header() {
+                        Ordering::Less
+                    } else if block_b.is_header() {
+                        Ordering::Greater
+                    } else {
+                        Ordering::Equal
+                    }
+                })
+                .then_with(|| placement_a.sort_order().cmp(&placement_b.sort_order()))
+                .then_with(|| match (block_a, block_b) {
+                    (
+                        Block::ExcerptBoundary {
+                            excerpt: excerpt_a, ..
+                        },
+                        Block::ExcerptBoundary {
+                            excerpt: excerpt_b, ..
+                        },
+                    ) => Some(excerpt_a.id).cmp(&Some(excerpt_b.id)),
+                    (Block::ExcerptBoundary { .. }, Block::Custom(_)) => Ordering::Less,
+                    (Block::Custom(_), Block::ExcerptBoundary { .. }) => Ordering::Greater,
+                    (Block::Custom(block_a), Block::Custom(block_b)) => block_a
+                        .priority
+                        .cmp(&block_b.priority)
+                        .then_with(|| block_a.id.cmp(&block_b.id)),
+                    _ => {
+                        unreachable!()
+                    }
+                })
         });
         blocks.dedup_by(|right, left| match (left.0.clone(), right.0.clone()) {
             (BlockPlacement::Replace(range), BlockPlacement::Above(row))
@@ -999,7 +994,7 @@ impl BlockMapWriter<'_> {
         let mut previous_wrap_row_range: Option<Range<u32>> = None;
         for block in blocks {
             if let BlockPlacement::Replace(_) = &block.placement {
-                debug_assert!(block.height > 0);
+                debug_assert!(block.height.unwrap() > 0);
             }
 
             let id = CustomBlockId(self.0.next_block_id.fetch_add(1, SeqCst));
@@ -1064,11 +1059,11 @@ impl BlockMapWriter<'_> {
                     debug_assert!(new_height > 0);
                 }
 
-                if block.height != new_height {
+                if block.height != Some(new_height) {
                     let new_block = CustomBlock {
                         id: block.id,
                         placement: block.placement.clone(),
-                        height: new_height,
+                        height: Some(new_height),
                         style: block.style,
                         render: block.render.clone(),
                         priority: block.priority,
@@ -1965,21 +1960,21 @@ mod tests {
             BlockProperties {
                 style: BlockStyle::Fixed,
                 placement: BlockPlacement::Above(buffer_snapshot.anchor_after(Point::new(1, 0))),
-                height: 1,
+                height: Some(1),
                 render: Arc::new(|_| div().into_any()),
                 priority: 0,
             },
             BlockProperties {
                 style: BlockStyle::Fixed,
                 placement: BlockPlacement::Above(buffer_snapshot.anchor_after(Point::new(1, 2))),
-                height: 2,
+                height: Some(2),
                 render: Arc::new(|_| div().into_any()),
                 priority: 0,
             },
             BlockProperties {
                 style: BlockStyle::Fixed,
                 placement: BlockPlacement::Below(buffer_snapshot.anchor_after(Point::new(3, 3))),
-                height: 3,
+                height: Some(3),
                 render: Arc::new(|_| div().into_any()),
                 priority: 0,
             },
@@ -1992,7 +1987,7 @@ mod tests {
             .blocks_in_range(0..8)
             .map(|(start_row, block)| {
                 let block = block.as_custom().unwrap();
-                (start_row..start_row + block.height, block.id)
+                (start_row..start_row + block.height.unwrap(), block.id)
             })
             .collect::<Vec<_>>();
 
@@ -2203,21 +2198,21 @@ mod tests {
             BlockProperties {
                 style: BlockStyle::Fixed,
                 placement: BlockPlacement::Above(buffer_snapshot.anchor_after(Point::new(1, 0))),
-                height: 1,
+                height: Some(1),
                 render: Arc::new(|_| div().into_any()),
                 priority: 0,
             },
             BlockProperties {
                 style: BlockStyle::Fixed,
                 placement: BlockPlacement::Above(buffer_snapshot.anchor_after(Point::new(1, 2))),
-                height: 2,
+                height: Some(2),
                 render: Arc::new(|_| div().into_any()),
                 priority: 0,
             },
             BlockProperties {
                 style: BlockStyle::Fixed,
                 placement: BlockPlacement::Below(buffer_snapshot.anchor_after(Point::new(3, 3))),
-                height: 3,
+                height: Some(3),
                 render: Arc::new(|_| div().into_any()),
                 priority: 0,
             },
@@ -2307,14 +2302,14 @@ mod tests {
                 style: BlockStyle::Fixed,
                 placement: BlockPlacement::Above(buffer_snapshot.anchor_after(Point::new(1, 12))),
                 render: Arc::new(|_| div().into_any()),
-                height: 1,
+                height: Some(1),
                 priority: 0,
             },
             BlockProperties {
                 style: BlockStyle::Fixed,
                 placement: BlockPlacement::Below(buffer_snapshot.anchor_after(Point::new(1, 1))),
                 render: Arc::new(|_| div().into_any()),
-                height: 1,
+                height: Some(1),
                 priority: 0,
             },
         ]);
@@ -2352,7 +2347,7 @@ mod tests {
                 buffer_snapshot.anchor_after(Point::new(1, 3))
                     ..=buffer_snapshot.anchor_before(Point::new(3, 1)),
             ),
-            height: 4,
+            height: Some(4),
             render: Arc::new(|_| div().into_any()),
             priority: 0,
         }])[0];
@@ -2405,21 +2400,21 @@ mod tests {
             BlockProperties {
                 style: BlockStyle::Fixed,
                 placement: BlockPlacement::Above(buffer_snapshot.anchor_after(Point::new(0, 3))),
-                height: 1,
+                height: Some(1),
                 render: Arc::new(|_| div().into_any()),
                 priority: 0,
             },
             BlockProperties {
                 style: BlockStyle::Fixed,
                 placement: BlockPlacement::Above(buffer_snapshot.anchor_after(Point::new(1, 3))),
-                height: 1,
+                height: Some(1),
                 render: Arc::new(|_| div().into_any()),
                 priority: 0,
             },
             BlockProperties {
                 style: BlockStyle::Fixed,
                 placement: BlockPlacement::Below(buffer_snapshot.anchor_after(Point::new(6, 2))),
-                height: 1,
+                height: Some(1),
                 render: Arc::new(|_| div().into_any()),
                 priority: 0,
             },
@@ -2433,21 +2428,21 @@ mod tests {
             BlockProperties {
                 style: BlockStyle::Fixed,
                 placement: BlockPlacement::Below(buffer_snapshot.anchor_after(Point::new(1, 3))),
-                height: 1,
+                height: Some(1),
                 render: Arc::new(|_| div().into_any()),
                 priority: 0,
             },
             BlockProperties {
                 style: BlockStyle::Fixed,
                 placement: BlockPlacement::Above(buffer_snapshot.anchor_after(Point::new(2, 1))),
-                height: 1,
+                height: Some(1),
                 render: Arc::new(|_| div().into_any()),
                 priority: 0,
             },
             BlockProperties {
                 style: BlockStyle::Fixed,
                 placement: BlockPlacement::Above(buffer_snapshot.anchor_after(Point::new(6, 1))),
-                height: 1,
+                height: Some(1),
                 render: Arc::new(|_| div().into_any()),
                 priority: 0,
             },
@@ -2546,21 +2541,21 @@ mod tests {
             BlockProperties {
                 style: BlockStyle::Fixed,
                 placement: BlockPlacement::Above(buffer_snapshot.anchor_after(Point::new(1, 0))),
-                height: 1,
+                height: Some(1),
                 render: Arc::new(|_| div().into_any()),
                 priority: 0,
             },
             BlockProperties {
                 style: BlockStyle::Fixed,
                 placement: BlockPlacement::Above(buffer_snapshot.anchor_after(Point::new(2, 0))),
-                height: 1,
+                height: Some(1),
                 render: Arc::new(|_| div().into_any()),
                 priority: 0,
             },
             BlockProperties {
                 style: BlockStyle::Fixed,
                 placement: BlockPlacement::Below(buffer_snapshot.anchor_after(Point::new(3, 0))),
-                height: 1,
+                height: Some(1),
                 render: Arc::new(|_| div().into_any()),
                 priority: 0,
             },
@@ -2569,14 +2564,14 @@ mod tests {
             BlockProperties {
                 style: BlockStyle::Fixed,
                 placement: BlockPlacement::Above(buffer_snapshot.anchor_after(Point::new(4, 0))),
-                height: 1,
+                height: Some(1),
                 render: Arc::new(|_| div().into_any()),
                 priority: 0,
             },
             BlockProperties {
                 style: BlockStyle::Fixed,
                 placement: BlockPlacement::Below(buffer_snapshot.anchor_after(Point::new(5, 0))),
-                height: 1,
+                height: Some(1),
                 render: Arc::new(|_| div().into_any()),
                 priority: 0,
             },
@@ -2623,7 +2618,7 @@ mod tests {
         let excerpt_blocks_1 = writer.insert(vec![BlockProperties {
             style: BlockStyle::Fixed,
             placement: BlockPlacement::Above(buffer_snapshot.anchor_after(Point::new(0, 0))),
-            height: 1,
+            height: Some(1),
             render: Arc::new(|_| div().into_any()),
             priority: 0,
         }]);
@@ -2980,7 +2975,7 @@ mod tests {
                             BlockProperties {
                                 style: BlockStyle::Fixed,
                                 placement,
-                                height,
+                                height: Some(height),
                                 render: Arc::new(|_| div().into_any()),
                                 priority: 0,
                             }
@@ -3007,7 +3002,7 @@ mod tests {
 
                     for (block_properties, block_id) in block_properties.iter().zip(block_ids) {
                         log::info!(
-                            "inserted block {:?} with height {} and id {:?}",
+                            "inserted block {:?} with height {:?} and id {:?}",
                             block_properties
                                 .placement
                                 .as_ref()
@@ -3524,7 +3519,7 @@ mod tests {
         let _block_id = writer.insert(vec![BlockProperties {
             style: BlockStyle::Fixed,
             placement: BlockPlacement::Above(buffer_snapshot.anchor_after(Point::new(1, 0))),
-            height: 1,
+            height: Some(1),
             render: Arc::new(|_| div().into_any()),
             priority: 0,
         }])[0];

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

@@ -14,6 +14,7 @@ use std::{
     fmt, iter,
     ops::{Add, AddAssign, Deref, DerefMut, Range, Sub},
     sync::Arc,
+    usize,
 };
 use sum_tree::{Bias, Cursor, FilterCursor, SumTree, Summary, TreeMap};
 use ui::IntoElement as _;

crates/editor/src/editor.rs 🔗

@@ -8717,7 +8717,7 @@ impl Editor {
         let blocks = vec![BlockProperties {
             style: BlockStyle::Sticky,
             placement: BlockPlacement::Above(anchor),
-            height,
+            height: Some(height),
             render: Arc::new(move |cx| {
                 *cloned_prompt.read(cx).gutter_dimensions.lock() = *cx.gutter_dimensions;
                 cloned_prompt.clone().into_any_element()
@@ -12801,7 +12801,6 @@ impl Editor {
     ) {
         let buffer = self.buffer.read(cx).snapshot(cx);
         let selection = self.selections.newest::<usize>(cx);
-
         // If there is an active Diagnostic Popover jump to its diagnostic instead.
         if direction == Direction::Next {
             if let Some(popover) = self.hover_state.diagnostic_popover.as_ref() {
@@ -13806,7 +13805,7 @@ impl Editor {
                         [BlockProperties {
                             style: BlockStyle::Flex,
                             placement: BlockPlacement::Below(range.start),
-                            height: 1,
+                            height: Some(1),
                             render: Arc::new({
                                 let rename_editor = rename_editor.clone();
                                 move |cx: &mut BlockContext| {
@@ -14273,7 +14272,7 @@ impl Editor {
                             placement: BlockPlacement::Below(
                                 buffer.anchor_after(entry.range.start),
                             ),
-                            height: message_height,
+                            height: Some(message_height),
                             render: diagnostic_block_renderer(diagnostic, None, true),
                             priority: 0,
                         }

crates/editor/src/editor_tests.rs 🔗

@@ -4234,7 +4234,7 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
             [BlockProperties {
                 style: BlockStyle::Fixed,
                 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
-                height: 1,
+                height: Some(1),
                 render: Arc::new(|_| div().into_any()),
                 priority: 0,
             }],
@@ -4275,7 +4275,7 @@ async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
         editor.insert_blocks(
             [BlockProperties {
                 placement,
-                height: 4,
+                height: Some(4),
                 style: BlockStyle::Sticky,
                 render: Arc::new(|_| gpui::div().into_any_element()),
                 priority: 0,

crates/editor/src/element.rs 🔗

@@ -27,7 +27,7 @@ use crate::{
 };
 use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
 use client::ParticipantIndex;
-use collections::{BTreeMap, HashMap, HashSet};
+use collections::{BTreeMap, HashMap};
 use feature_flags::{Debugger, FeatureFlagAppExt};
 use file_icons::FileIcons;
 use git::{Oid, blame::BlameEntry, status::FileStatus};
@@ -1219,7 +1219,7 @@ impl EditorElement {
         &self,
         snapshot: &EditorSnapshot,
         selections: &[(PlayerColor, Vec<SelectionLayout>)],
-        block_start_rows: &HashSet<DisplayRow>,
+        row_block_types: &HashMap<DisplayRow, bool>,
         visible_display_row_range: Range<DisplayRow>,
         line_layouts: &[LineWithInvisibles],
         text_hitbox: &Hitbox,
@@ -1246,7 +1246,7 @@ impl EditorElement {
                     let in_range = visible_display_row_range.contains(&cursor_position.row());
                     if (selection.is_local && !show_local_cursors)
                         || !in_range
-                        || block_start_rows.contains(&cursor_position.row())
+                        || row_block_types.get(&cursor_position.row()) == Some(&true)
                     {
                         continue;
                     }
@@ -1571,6 +1571,7 @@ impl EditorElement {
         &self,
         line_layouts: &[LineWithInvisibles],
         crease_trailers: &[Option<CreaseTrailerLayout>],
+        row_block_types: &HashMap<DisplayRow, bool>,
         content_origin: gpui::Point<Pixels>,
         scroll_pixel_position: gpui::Point<Pixels>,
         inline_completion_popover_origin: Option<gpui::Point<Pixels>>,
@@ -1616,6 +1617,7 @@ impl EditorElement {
                 .map(|(point, diag)| (point.to_display_point(&snapshot), diag.clone()))
                 .skip_while(|(point, _)| point.row() < start_row)
                 .take_while(|(point, _)| point.row() < end_row)
+                .filter(|(point, _)| !row_block_types.contains_key(&point.row()))
                 .fold(HashMap::default(), |mut acc, (point, diagnostic)| {
                     acc.entry(point.row())
                         .or_insert_with(Vec::new)
@@ -2614,34 +2616,41 @@ impl EditorElement {
         editor_width: Pixels,
         scroll_width: &mut Pixels,
         resized_blocks: &mut HashMap<CustomBlockId, u32>,
+        row_block_types: &mut HashMap<DisplayRow, bool>,
         selections: &[Selection<Point>],
         selected_buffer_ids: &Vec<BufferId>,
         is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
         sticky_header_excerpt_id: Option<ExcerptId>,
         window: &mut Window,
         cx: &mut App,
-    ) -> (AnyElement, Size<Pixels>) {
+    ) -> (AnyElement, Size<Pixels>, DisplayRow, Pixels) {
+        let mut x_position = None;
         let mut element = match block {
             Block::Custom(block) => {
                 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]
-                            .x_for_index(align_to.column() as usize)
-                    } else {
-                        layout_line(
-                            align_to.row(),
-                            snapshot,
-                            &self.style,
-                            editor_width,
-                            is_row_soft_wrapped,
-                            window,
-                            cx,
-                        )
-                        .x_for_index(align_to.column() as usize)
-                    };
+                let x_and_width = |layout: &LineWithInvisibles| {
+                    Some((
+                        text_x + layout.x_for_index(align_to.column() as usize),
+                        text_x + layout.width,
+                    ))
+                };
+                x_position = if rows.contains(&align_to.row()) {
+                    x_and_width(&line_layouts[align_to.row().minus(rows.start) as usize])
+                } else {
+                    x_and_width(&layout_line(
+                        align_to.row(),
+                        snapshot,
+                        &self.style,
+                        editor_width,
+                        is_row_soft_wrapped,
+                        window,
+                        cx,
+                    ))
+                };
+
+                let anchor_x = x_position.unwrap().0;
 
                 let selected = selections
                     .binary_search_by(|selection| {
@@ -2746,17 +2755,43 @@ impl EditorElement {
             element.layout_as_root(size(available_width, quantized_height.into()), window, cx)
         };
 
+        let mut row = block_row_start;
+        let mut x_offset = px(0.);
+        let mut is_block = true;
+
         if let BlockId::Custom(custom_block_id) = block_id {
-            if block.height() > 0 {
-                let element_height_in_lines =
+            if block.has_height() {
+                let mut element_height_in_lines =
                     ((final_size.height / line_height).ceil() as u32).max(1);
+
+                if block.place_near() && element_height_in_lines == 1 {
+                    if let Some((x_target, line_width)) = x_position {
+                        let margin = em_width * 2;
+                        if line_width + final_size.width + margin
+                            < editor_width + gutter_dimensions.full_width()
+                            && !row_block_types.contains_key(&(row - 1))
+                        {
+                            x_offset = line_width + margin;
+                            row = row - 1;
+                            is_block = false;
+                            element_height_in_lines = 0;
+                        } else {
+                            let max_offset =
+                                editor_width + gutter_dimensions.full_width() - final_size.width;
+                            let min_offset = (x_target + em_width - final_size.width)
+                                .max(gutter_dimensions.full_width());
+                            x_offset = x_target.min(max_offset).max(min_offset);
+                        }
+                    }
+                };
                 if element_height_in_lines != block.height() {
                     resized_blocks.insert(custom_block_id, element_height_in_lines);
                 }
             }
         }
+        row_block_types.insert(row, is_block);
 
-        (element, final_size)
+        (element, final_size, row, x_offset)
     }
 
     fn render_buffer_header(
@@ -2962,14 +2997,14 @@ impl EditorElement {
         em_width: Pixels,
         text_x: Pixels,
         line_height: Pixels,
-        line_layouts: &[LineWithInvisibles],
+        line_layouts: &mut [LineWithInvisibles],
         selections: &[Selection<Point>],
         selected_buffer_ids: &Vec<BufferId>,
         is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
         sticky_header_excerpt_id: Option<ExcerptId>,
         window: &mut Window,
         cx: &mut App,
-    ) -> Result<Vec<BlockLayout>, HashMap<CustomBlockId, u32>> {
+    ) -> Result<(Vec<BlockLayout>, HashMap<DisplayRow, bool>), HashMap<CustomBlockId, u32>> {
         let (fixed_blocks, non_fixed_blocks) = snapshot
             .blocks_in_range(rows.clone())
             .partition::<Vec<_>, _>(|(_, block)| block.style() == BlockStyle::Fixed);
@@ -2980,6 +3015,7 @@ impl EditorElement {
         let mut fixed_block_max_width = Pixels::ZERO;
         let mut blocks = Vec::new();
         let mut resized_blocks = HashMap::default();
+        let mut row_block_types = HashMap::default();
 
         for (row, block) in fixed_blocks {
             let block_id = block.id();
@@ -2988,7 +3024,7 @@ impl EditorElement {
                 focused_block = None;
             }
 
-            let (element, element_size) = self.render_block(
+            let (element, element_size, row, x_offset) = self.render_block(
                 block,
                 AvailableSpace::MinContent,
                 block_id,
@@ -3004,6 +3040,7 @@ impl EditorElement {
                 editor_width,
                 scroll_width,
                 &mut resized_blocks,
+                &mut row_block_types,
                 selections,
                 selected_buffer_ids,
                 is_row_soft_wrapped,
@@ -3011,27 +3048,32 @@ impl EditorElement {
                 window,
                 cx,
             );
+
             fixed_block_max_width = fixed_block_max_width.max(element_size.width + em_width);
             blocks.push(BlockLayout {
                 id: block_id,
+                x_offset,
                 row: Some(row),
                 element,
                 available_space: size(AvailableSpace::MinContent, element_size.height.into()),
                 style: BlockStyle::Fixed,
+                overlaps_gutter: true,
                 is_buffer_header: block.is_buffer_header(),
             });
         }
 
         for (row, block) in non_fixed_blocks {
             let style = block.style();
-            let width = match style {
-                BlockStyle::Sticky => hitbox.size.width,
-                BlockStyle::Flex => hitbox
+            let width = match (style, block.place_near()) {
+                (_, true) => AvailableSpace::MinContent,
+                (BlockStyle::Sticky, _) => hitbox.size.width.into(),
+                (BlockStyle::Flex, _) => hitbox
                     .size
                     .width
                     .max(fixed_block_max_width)
-                    .max(gutter_dimensions.width + *scroll_width),
-                BlockStyle::Fixed => unreachable!(),
+                    .max(gutter_dimensions.width + *scroll_width)
+                    .into(),
+                (BlockStyle::Fixed, _) => unreachable!(),
             };
             let block_id = block.id();
 
@@ -3039,9 +3081,9 @@ impl EditorElement {
                 focused_block = None;
             }
 
-            let (element, element_size) = self.render_block(
+            let (element, element_size, row, x_offset) = self.render_block(
                 block,
-                width.into(),
+                width,
                 block_id,
                 row,
                 snapshot,
@@ -3055,6 +3097,7 @@ impl EditorElement {
                 editor_width,
                 scroll_width,
                 &mut resized_blocks,
+                &mut row_block_types,
                 selections,
                 selected_buffer_ids,
                 is_row_soft_wrapped,
@@ -3065,10 +3108,12 @@ impl EditorElement {
 
             blocks.push(BlockLayout {
                 id: block_id,
+                x_offset,
                 row: Some(row),
                 element,
-                available_space: size(width.into(), element_size.height.into()),
+                available_space: size(width, element_size.height.into()),
                 style,
+                overlaps_gutter: !block.place_near(),
                 is_buffer_header: block.is_buffer_header(),
             });
         }
@@ -3090,7 +3135,7 @@ impl EditorElement {
                             BlockStyle::Sticky => AvailableSpace::Definite(hitbox.size.width),
                         };
 
-                        let (element, element_size) = self.render_block(
+                        let (element, element_size, _, x_offset) = self.render_block(
                             &block,
                             width,
                             focused_block.id,
@@ -3106,6 +3151,7 @@ impl EditorElement {
                             editor_width,
                             scroll_width,
                             &mut resized_blocks,
+                            &mut row_block_types,
                             selections,
                             selected_buffer_ids,
                             is_row_soft_wrapped,
@@ -3116,10 +3162,12 @@ impl EditorElement {
 
                         blocks.push(BlockLayout {
                             id: block.id(),
+                            x_offset,
                             row: None,
                             element,
                             available_space: size(width, element_size.height.into()),
                             style,
+                            overlaps_gutter: true,
                             is_buffer_header: block.is_buffer_header(),
                         });
                     }
@@ -3129,18 +3177,15 @@ impl EditorElement {
 
         if resized_blocks.is_empty() {
             *scroll_width = (*scroll_width).max(fixed_block_max_width - gutter_dimensions.width);
-            Ok(blocks)
+            Ok((blocks, row_block_types))
         } else {
             Err(resized_blocks)
         }
     }
 
-    /// Returns true if any of the blocks changed size since the previous frame. This will trigger
-    /// a restart of rendering for the editor based on the new sizes.
     fn layout_blocks(
         &self,
         blocks: &mut Vec<BlockLayout>,
-        block_starts: &mut HashSet<DisplayRow>,
         hitbox: &Hitbox,
         line_height: Pixels,
         scroll_pixel_position: gpui::Point<Pixels>,
@@ -3149,10 +3194,9 @@ impl EditorElement {
     ) {
         for block in blocks {
             let mut origin = if let Some(row) = block.row {
-                block_starts.insert(row);
                 hitbox.origin
                     + point(
-                        Pixels::ZERO,
+                        block.x_offset,
                         row.as_f32() * line_height - scroll_pixel_position.y,
                     )
             } else {
@@ -5322,7 +5366,15 @@ impl EditorElement {
 
     fn paint_blocks(&mut self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
         for mut block in layout.blocks.drain(..) {
-            block.element.paint(window, cx);
+            if block.overlaps_gutter {
+                block.element.paint(window, cx);
+            } else {
+                let mut bounds = layout.hitbox.bounds;
+                bounds.origin.x += layout.gutter_hitbox.bounds.size.width;
+                window.with_content_mask(Some(ContentMask { bounds }), |window| {
+                    block.element.paint(window, cx);
+                })
+            }
         }
     }
 
@@ -6955,7 +7007,7 @@ impl Element for EditorElement {
                             em_width,
                             gutter_dimensions.full_width(),
                             line_height,
-                            &line_layouts,
+                            &mut line_layouts,
                             &local_selections,
                             &selected_buffer_ids,
                             is_row_soft_wrapped,
@@ -6964,7 +7016,7 @@ impl Element for EditorElement {
                             cx,
                         )
                     });
-                    let mut blocks = match blocks {
+                    let (mut blocks, row_block_types) = match blocks {
                         Ok(blocks) => blocks,
                         Err(resized_blocks) => {
                             self.editor.update(cx, |editor, cx| {
@@ -7078,6 +7130,7 @@ impl Element for EditorElement {
                     let mut inline_diagnostics = self.layout_inline_diagnostics(
                         &line_layouts,
                         &crease_trailers,
+                        &row_block_types,
                         content_origin,
                         scroll_pixel_position,
                         inline_completion_popover_origin,
@@ -7093,7 +7146,9 @@ impl Element for EditorElement {
                     let mut inline_blame = None;
                     if let Some(newest_selection_head) = newest_selection_head {
                         let display_row = newest_selection_head.row();
-                        if (start_row..end_row).contains(&display_row) {
+                        if (start_row..end_row).contains(&display_row)
+                            && !row_block_types.contains_key(&display_row)
+                        {
                             let line_ix = display_row.minus(start_row) as usize;
                             let row_info = &row_infos[line_ix];
                             let line_layout = &line_layouts[line_ix];
@@ -7161,12 +7216,9 @@ impl Element for EditorElement {
                         cx,
                     );
 
-                    let mut block_start_rows = HashSet::default();
-
                     window.with_element_namespace("blocks", |window| {
                         self.layout_blocks(
                             &mut blocks,
-                            &mut block_start_rows,
                             &hitbox,
                             line_height,
                             scroll_pixel_position,
@@ -7184,7 +7236,7 @@ impl Element for EditorElement {
                     let visible_cursors = self.layout_visible_cursors(
                         &snapshot,
                         &selections,
-                        &block_start_rows,
+                        &row_block_types,
                         start_row..end_row,
                         &line_layouts,
                         &text_hitbox,
@@ -8028,10 +8080,12 @@ impl PositionMap {
 
 struct BlockLayout {
     id: BlockId,
+    x_offset: Pixels,
     row: Option<DisplayRow>,
     element: AnyElement,
     available_space: Size<AvailableSpace>,
     style: BlockStyle,
+    overlaps_gutter: bool,
     is_buffer_header: bool,
 }
 
@@ -8620,7 +8674,7 @@ mod tests {
                     [BlockProperties {
                         style: BlockStyle::Fixed,
                         placement: BlockPlacement::Above(Anchor::min()),
-                        height: 3,
+                        height: Some(3),
                         render: Arc::new(|cx| div().h(3. * cx.window.line_height()).into_any()),
                         priority: 0,
                     }],

crates/multi_buffer/src/anchor.rs 🔗

@@ -57,6 +57,10 @@ impl Anchor {
     }
 
     pub fn cmp(&self, other: &Anchor, snapshot: &MultiBufferSnapshot) -> Ordering {
+        if self == other {
+            return Ordering::Equal;
+        }
+
         let excerpt_id_cmp = self.excerpt_id.cmp(&other.excerpt_id, snapshot);
         if excerpt_id_cmp.is_ne() {
             return excerpt_id_cmp;

crates/repl/src/session.rs 🔗

@@ -89,7 +89,7 @@ impl EditorBlock {
             let block = BlockProperties {
                 placement: BlockPlacement::Below(code_range.end),
                 // Take up at least one height for status, allow the editor to determine the real height based on the content from render
-                height: 1,
+                height: Some(1),
                 style: BlockStyle::Sticky,
                 render: Self::create_output_area_renderer(execution_view.clone(), on_close.clone()),
                 priority: 0,