Combine excerpt footer and header into a single block (#19441)

Max Brunsfeld and Richard created

This simplifies rendering of excerpt headers and footers, and removes
the need to store a `BlockDisposition` on these boundary blocks. It's a
step toward implementing "replace blocks", which we want to use in the
assistant panel.

We've also cleaned up the way heights are specified for headers and
footers and fixed some visual asymmetries between the "expand upward"
and "expand downward" buttons.

Release Notes:

- N/A

---------

Co-authored-by: Richard <richard@zed.dev>

Change summary

crates/diagnostics/src/diagnostics_tests.rs |   4 
crates/editor/src/display_map/block_map.rs  | 301 ++++-------
crates/editor/src/editor.rs                 |  31 
crates/editor/src/element.rs                | 577 +++++++++-------------
crates/editor/src/movement.rs               |   2 
crates/multi_buffer/src/multi_buffer.rs     |   2 
crates/repl/src/session.rs                  |  29 
7 files changed, 397 insertions(+), 549 deletions(-)

Detailed changes

crates/diagnostics/src/diagnostics_tests.rs 🔗

@@ -962,7 +962,6 @@ fn random_diagnostic(
 
 const FILE_HEADER: &str = "file header";
 const EXCERPT_HEADER: &str = "excerpt header";
-const EXCERPT_FOOTER: &str = "excerpt footer";
 
 fn editor_blocks(
     editor: &View<Editor>,
@@ -998,7 +997,7 @@ fn editor_blocks(
                                     .ok()?
                             }
 
-                            Block::ExcerptHeader {
+                            Block::ExcerptBoundary {
                                 starts_new_buffer, ..
                             } => {
                                 if *starts_new_buffer {
@@ -1007,7 +1006,6 @@ fn editor_blocks(
                                     EXCERPT_HEADER.into()
                                 }
                             }
-                            Block::ExcerptFooter { .. } => EXCERPT_FOOTER.into(),
                         };
 
                         Some((row, name))

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

@@ -5,8 +5,8 @@ use super::{
 use crate::{EditorStyle, GutterDimensions};
 use collections::{Bound, HashMap, HashSet};
 use gpui::{AnyElement, EntityId, Pixels, WindowContext};
-use language::{BufferSnapshot, Chunk, Patch, Point};
-use multi_buffer::{Anchor, ExcerptId, ExcerptRange, MultiBufferRow, ToPoint as _};
+use language::{Chunk, Patch, Point};
+use multi_buffer::{Anchor, ExcerptId, ExcerptInfo, MultiBufferRow, ToPoint as _};
 use parking_lot::Mutex;
 use std::{
     cell::RefCell,
@@ -128,26 +128,17 @@ pub struct BlockContext<'a, 'b> {
 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
 pub enum BlockId {
     Custom(CustomBlockId),
-    ExcerptHeader(ExcerptId),
-    ExcerptFooter(ExcerptId),
-}
-
-impl From<BlockId> for EntityId {
-    fn from(value: BlockId) -> Self {
-        match value {
-            BlockId::Custom(CustomBlockId(id)) => EntityId::from(id as u64),
-            BlockId::ExcerptHeader(id) => id.into(),
-            BlockId::ExcerptFooter(id) => id.into(),
-        }
-    }
+    ExcerptBoundary(Option<ExcerptId>),
 }
 
 impl From<BlockId> for ElementId {
     fn from(value: BlockId) -> Self {
         match value {
             BlockId::Custom(CustomBlockId(id)) => ("Block", id).into(),
-            BlockId::ExcerptHeader(id) => ("ExcerptHeader", EntityId::from(id)).into(),
-            BlockId::ExcerptFooter(id) => ("ExcerptFooter", EntityId::from(id)).into(),
+            BlockId::ExcerptBoundary(next_excerpt) => match next_excerpt {
+                Some(id) => ("ExcerptBoundary", EntityId::from(id)).into(),
+                None => "LastExcerptBoundary".into(),
+            },
         }
     }
 }
@@ -156,8 +147,7 @@ impl std::fmt::Display for BlockId {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         match self {
             Self::Custom(id) => write!(f, "Block({id:?})"),
-            Self::ExcerptHeader(id) => write!(f, "ExcerptHeader({id:?})"),
-            Self::ExcerptFooter(id) => write!(f, "ExcerptFooter({id:?})"),
+            Self::ExcerptBoundary(id) => write!(f, "ExcerptHeader({id:?})"),
         }
     }
 }
@@ -177,8 +167,7 @@ struct Transform {
 
 pub(crate) enum BlockType {
     Custom(CustomBlockId),
-    Header,
-    Footer,
+    ExcerptBoundary,
 }
 
 pub(crate) trait BlockLike {
@@ -191,27 +180,20 @@ pub(crate) trait BlockLike {
 #[derive(Clone)]
 pub enum Block {
     Custom(Arc<CustomBlock>),
-    ExcerptHeader {
-        id: ExcerptId,
-        buffer: BufferSnapshot,
-        range: ExcerptRange<text::Anchor>,
+    ExcerptBoundary {
+        prev_excerpt: Option<ExcerptInfo>,
+        next_excerpt: Option<ExcerptInfo>,
         height: u32,
         starts_new_buffer: bool,
         show_excerpt_controls: bool,
     },
-    ExcerptFooter {
-        id: ExcerptId,
-        disposition: BlockDisposition,
-        height: u32,
-    },
 }
 
 impl BlockLike for Block {
     fn block_type(&self) -> BlockType {
         match self {
             Block::Custom(block) => BlockType::Custom(block.id),
-            Block::ExcerptHeader { .. } => BlockType::Header,
-            Block::ExcerptFooter { .. } => BlockType::Footer,
+            Block::ExcerptBoundary { .. } => BlockType::ExcerptBoundary,
         }
     }
 
@@ -222,8 +204,7 @@ impl BlockLike for Block {
     fn priority(&self) -> usize {
         match self {
             Block::Custom(block) => block.priority,
-            Block::ExcerptHeader { .. } => usize::MAX,
-            Block::ExcerptFooter { .. } => 0,
+            Block::ExcerptBoundary { .. } => usize::MAX,
         }
     }
 }
@@ -232,32 +213,36 @@ impl Block {
     pub fn id(&self) -> BlockId {
         match self {
             Block::Custom(block) => BlockId::Custom(block.id),
-            Block::ExcerptHeader { id, .. } => BlockId::ExcerptHeader(*id),
-            Block::ExcerptFooter { id, .. } => BlockId::ExcerptFooter(*id),
+            Block::ExcerptBoundary { next_excerpt, .. } => {
+                BlockId::ExcerptBoundary(next_excerpt.as_ref().map(|info| info.id))
+            }
         }
     }
 
     fn disposition(&self) -> BlockDisposition {
         match self {
             Block::Custom(block) => block.disposition,
-            Block::ExcerptHeader { .. } => BlockDisposition::Above,
-            Block::ExcerptFooter { disposition, .. } => *disposition,
+            Block::ExcerptBoundary { next_excerpt, .. } => {
+                if next_excerpt.is_some() {
+                    BlockDisposition::Above
+                } else {
+                    BlockDisposition::Below
+                }
+            }
         }
     }
 
     pub fn height(&self) -> u32 {
         match self {
             Block::Custom(block) => block.height,
-            Block::ExcerptHeader { height, .. } => *height,
-            Block::ExcerptFooter { height, .. } => *height,
+            Block::ExcerptBoundary { height, .. } => *height,
         }
     }
 
     pub fn style(&self) -> BlockStyle {
         match self {
             Block::Custom(block) => block.style,
-            Block::ExcerptHeader { .. } => BlockStyle::Sticky,
-            Block::ExcerptFooter { .. } => BlockStyle::Sticky,
+            Block::ExcerptBoundary { .. } => BlockStyle::Sticky,
         }
     }
 }
@@ -266,24 +251,17 @@ impl Debug for Block {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         match self {
             Self::Custom(block) => f.debug_struct("Custom").field("block", block).finish(),
-            Self::ExcerptHeader {
-                buffer,
+            Self::ExcerptBoundary {
                 starts_new_buffer,
-                id,
+                next_excerpt,
+                prev_excerpt,
                 ..
             } => f
-                .debug_struct("ExcerptHeader")
-                .field("id", &id)
-                .field("path", &buffer.file().map(|f| f.path()))
+                .debug_struct("ExcerptBoundary")
+                .field("prev_excerpt", &prev_excerpt)
+                .field("next_excerpt", &next_excerpt)
                 .field("starts_new_buffer", &starts_new_buffer)
                 .finish(),
-            Block::ExcerptFooter {
-                id, disposition, ..
-            } => f
-                .debug_struct("ExcerptFooter")
-                .field("id", &id)
-                .field("disposition", &disposition)
-                .finish(),
         }
     }
 }
@@ -595,66 +573,62 @@ impl BlockMap {
     {
         buffer
             .excerpt_boundaries_in_range(range)
-            .flat_map(move |excerpt_boundary| {
-                let mut wrap_row = wrap_snapshot
-                    .make_wrap_point(Point::new(excerpt_boundary.row.0, 0), Bias::Left)
-                    .row();
-
-                [
-                    show_excerpt_controls
-                        .then(|| {
-                            let disposition;
-                            if excerpt_boundary.next.is_some() {
-                                disposition = BlockDisposition::Above;
-                            } else {
-                                wrap_row = wrap_snapshot
-                                    .make_wrap_point(
-                                        Point::new(
-                                            excerpt_boundary.row.0,
-                                            buffer.line_len(excerpt_boundary.row),
-                                        ),
-                                        Bias::Left,
-                                    )
-                                    .row();
-                                disposition = BlockDisposition::Below;
-                            }
-
-                            excerpt_boundary.prev.as_ref().map(|prev| {
-                                (
-                                    wrap_row,
-                                    Block::ExcerptFooter {
-                                        id: prev.id,
-                                        height: excerpt_footer_height,
-                                        disposition,
-                                    },
-                                )
-                            })
-                        })
-                        .flatten(),
-                    excerpt_boundary.next.map(|next| {
-                        let starts_new_buffer = excerpt_boundary
-                            .prev
-                            .map_or(true, |prev| prev.buffer_id != next.buffer_id);
-
-                        (
-                            wrap_row,
-                            Block::ExcerptHeader {
-                                id: next.id,
-                                buffer: next.buffer,
-                                range: next.range,
-                                height: if starts_new_buffer {
-                                    buffer_header_height
-                                } else {
-                                    excerpt_header_height
-                                },
-                                starts_new_buffer,
-                                show_excerpt_controls,
-                            },
+            .filter_map(move |excerpt_boundary| {
+                let wrap_row;
+                if excerpt_boundary.next.is_some() {
+                    wrap_row = wrap_snapshot
+                        .make_wrap_point(Point::new(excerpt_boundary.row.0, 0), Bias::Left)
+                        .row();
+                } else {
+                    wrap_row = wrap_snapshot
+                        .make_wrap_point(
+                            Point::new(
+                                excerpt_boundary.row.0,
+                                buffer.line_len(excerpt_boundary.row),
+                            ),
+                            Bias::Left,
                         )
-                    }),
-                ]
+                        .row();
+                }
+
+                let starts_new_buffer = match (&excerpt_boundary.prev, &excerpt_boundary.next) {
+                    (_, None) => false,
+                    (None, Some(_)) => true,
+                    (Some(prev), Some(next)) => prev.buffer_id != next.buffer_id,
+                };
+
+                let mut height = 0;
+                if excerpt_boundary.prev.is_some() {
+                    if show_excerpt_controls {
+                        height += excerpt_footer_height;
+                    }
+                }
+                if excerpt_boundary.next.is_some() {
+                    if starts_new_buffer {
+                        height += buffer_header_height;
+                        if show_excerpt_controls {
+                            height += excerpt_header_height;
+                        }
+                    } else {
+                        height += excerpt_header_height;
+                    }
+                }
+
+                if height == 0 {
+                    return None;
+                }
+
+                Some((
+                    wrap_row,
+                    Block::ExcerptBoundary {
+                        prev_excerpt: excerpt_boundary.prev,
+                        next_excerpt: excerpt_boundary.next,
+                        height,
+                        starts_new_buffer,
+                        show_excerpt_controls,
+                    },
+                ))
             })
-            .flatten()
     }
 
     pub(crate) fn sort_blocks<B: BlockLike>(blocks: &mut [(u32, B)]) {
@@ -665,12 +639,9 @@ impl BlockMap {
                     .disposition()
                     .cmp(&block_b.disposition())
                     .then_with(|| match ((block_a.block_type()), (block_b.block_type())) {
-                        (BlockType::Footer, BlockType::Footer) => Ordering::Equal,
-                        (BlockType::Footer, _) => Ordering::Less,
-                        (_, BlockType::Footer) => Ordering::Greater,
-                        (BlockType::Header, BlockType::Header) => Ordering::Equal,
-                        (BlockType::Header, _) => Ordering::Less,
-                        (_, BlockType::Header) => Ordering::Greater,
+                        (BlockType::ExcerptBoundary, BlockType::ExcerptBoundary) => Ordering::Equal,
+                        (BlockType::ExcerptBoundary, _) => Ordering::Less,
+                        (_, BlockType::ExcerptBoundary) => Ordering::Greater,
                         (BlockType::Custom(a_id), BlockType::Custom(b_id)) => block_b
                             .priority()
                             .cmp(&block_a.priority())
@@ -1045,33 +1016,19 @@ impl BlockSnapshot {
                 let custom_block = self.custom_blocks_by_id.get(&custom_block_id)?;
                 Some(Block::Custom(custom_block.clone()))
             }
-            BlockId::ExcerptHeader(excerpt_id) => {
-                let excerpt_range = buffer.range_for_excerpt::<Point>(excerpt_id)?;
-                let wrap_point = self
-                    .wrap_snapshot
-                    .make_wrap_point(excerpt_range.start, Bias::Left);
-                let mut cursor = self.transforms.cursor::<(WrapRow, BlockRow)>(&());
-                cursor.seek(&WrapRow(wrap_point.row()), Bias::Left, &());
-                while let Some(transform) = cursor.item() {
-                    if let Some(block) = transform.block.as_ref() {
-                        if block.id() == block_id {
-                            return Some(block.clone());
-                        }
-                    } else if cursor.start().0 > WrapRow(wrap_point.row()) {
-                        break;
-                    }
-
-                    cursor.next(&());
+            BlockId::ExcerptBoundary(next_excerpt_id) => {
+                let wrap_point;
+                if let Some(next_excerpt_id) = next_excerpt_id {
+                    let excerpt_range = buffer.range_for_excerpt::<Point>(next_excerpt_id)?;
+                    wrap_point = self
+                        .wrap_snapshot
+                        .make_wrap_point(excerpt_range.start, Bias::Left);
+                } else {
+                    wrap_point = self
+                        .wrap_snapshot
+                        .make_wrap_point(buffer.max_point(), Bias::Left);
                 }
 
-                None
-            }
-            BlockId::ExcerptFooter(excerpt_id) => {
-                let excerpt_range = buffer.range_for_excerpt::<Point>(excerpt_id)?;
-                let wrap_point = self
-                    .wrap_snapshot
-                    .make_wrap_point(excerpt_range.end, Bias::Left);
-
                 let mut cursor = self.transforms.cursor::<(WrapRow, BlockRow)>(&());
                 cursor.seek(&WrapRow(wrap_point.row()), Bias::Left, &());
                 while let Some(transform) = cursor.item() {
@@ -1468,7 +1425,7 @@ mod tests {
     };
     use gpui::{div, font, px, AppContext, Context as _, Element};
     use language::{Buffer, Capability};
-    use multi_buffer::MultiBuffer;
+    use multi_buffer::{ExcerptRange, MultiBuffer};
     use rand::prelude::*;
     use settings::SettingsStore;
     use std::env;
@@ -1724,22 +1681,20 @@ mod tests {
         // Each excerpt has a header above and footer below. Excerpts are also *separated* by a newline.
         assert_eq!(
             snapshot.text(),
-            "\nBuff\ner 1\n\n\nBuff\ner 2\n\n\nBuff\ner 3\n"
+            "\n\nBuff\ner 1\n\n\n\nBuff\ner 2\n\n\n\nBuff\ner 3\n"
         );
 
         let blocks: Vec<_> = snapshot
             .blocks_in_range(0..u32::MAX)
-            .map(|(row, block)| (row, block.id()))
+            .map(|(row, block)| (row..row + block.height(), block.id()))
             .collect();
         assert_eq!(
             blocks,
             vec![
-                (0, BlockId::ExcerptHeader(excerpt_ids[0])),
-                (3, BlockId::ExcerptFooter(excerpt_ids[0])),
-                (4, BlockId::ExcerptHeader(excerpt_ids[1])),
-                (7, BlockId::ExcerptFooter(excerpt_ids[1])),
-                (8, BlockId::ExcerptHeader(excerpt_ids[2])),
-                (11, BlockId::ExcerptFooter(excerpt_ids[2]))
+                (0..2, BlockId::ExcerptBoundary(Some(excerpt_ids[0]))), // path, header
+                (4..7, BlockId::ExcerptBoundary(Some(excerpt_ids[1]))), // footer, path, header
+                (9..12, BlockId::ExcerptBoundary(Some(excerpt_ids[2]))), // footer, path, header
+                (14..15, BlockId::ExcerptBoundary(None)),               // footer
             ]
         );
     }
@@ -2283,13 +2238,10 @@ mod tests {
 
         #[derive(Debug, Eq, PartialEq)]
         enum ExpectedBlock {
-            ExcerptHeader {
+            ExcerptBoundary {
                 height: u32,
                 starts_new_buffer: bool,
-            },
-            ExcerptFooter {
-                height: u32,
-                disposition: BlockDisposition,
+                is_last: bool,
             },
             Custom {
                 disposition: BlockDisposition,
@@ -2303,8 +2255,7 @@ mod tests {
             fn block_type(&self) -> BlockType {
                 match self {
                     ExpectedBlock::Custom { id, .. } => BlockType::Custom(*id),
-                    ExpectedBlock::ExcerptHeader { .. } => BlockType::Header,
-                    ExpectedBlock::ExcerptFooter { .. } => BlockType::Footer,
+                    ExpectedBlock::ExcerptBoundary { .. } => BlockType::ExcerptBoundary,
                 }
             }
 
@@ -2315,8 +2266,7 @@ mod tests {
             fn priority(&self) -> usize {
                 match self {
                     ExpectedBlock::Custom { priority, .. } => *priority,
-                    ExpectedBlock::ExcerptHeader { .. } => usize::MAX,
-                    ExpectedBlock::ExcerptFooter { .. } => 0,
+                    ExpectedBlock::ExcerptBoundary { .. } => usize::MAX,
                 }
             }
         }
@@ -2324,17 +2274,21 @@ mod tests {
         impl ExpectedBlock {
             fn height(&self) -> u32 {
                 match self {
-                    ExpectedBlock::ExcerptHeader { height, .. } => *height,
+                    ExpectedBlock::ExcerptBoundary { height, .. } => *height,
                     ExpectedBlock::Custom { height, .. } => *height,
-                    ExpectedBlock::ExcerptFooter { height, .. } => *height,
                 }
             }
 
             fn disposition(&self) -> BlockDisposition {
                 match self {
-                    ExpectedBlock::ExcerptHeader { .. } => BlockDisposition::Above,
+                    ExpectedBlock::ExcerptBoundary { is_last, .. } => {
+                        if *is_last {
+                            BlockDisposition::Below
+                        } else {
+                            BlockDisposition::Above
+                        }
+                    }
                     ExpectedBlock::Custom { disposition, .. } => *disposition,
-                    ExpectedBlock::ExcerptFooter { disposition, .. } => *disposition,
                 }
             }
         }
@@ -2348,21 +2302,15 @@ mod tests {
                         height: block.height,
                         priority: block.priority,
                     },
-                    Block::ExcerptHeader {
+                    Block::ExcerptBoundary {
                         height,
                         starts_new_buffer,
+                        next_excerpt,
                         ..
-                    } => ExpectedBlock::ExcerptHeader {
+                    } => ExpectedBlock::ExcerptBoundary {
                         height,
                         starts_new_buffer,
-                    },
-                    Block::ExcerptFooter {
-                        height,
-                        disposition,
-                        ..
-                    } => ExpectedBlock::ExcerptFooter {
-                        height,
-                        disposition,
+                        is_last: next_excerpt.is_none(),
                     },
                 }
             }
@@ -2380,8 +2328,7 @@ mod tests {
         fn as_custom(&self) -> Option<&CustomBlock> {
             match self {
                 Block::Custom(block) => Some(block),
-                Block::ExcerptHeader { .. } => None,
-                Block::ExcerptFooter { .. } => None,
+                Block::ExcerptBoundary { .. } => None,
             }
         }
     }

crates/editor/src/editor.rs 🔗

@@ -73,12 +73,12 @@ use git::blame::GitBlame;
 use gpui::{
     div, impl_actions, point, prelude::*, px, relative, size, uniform_list, Action, AnyElement,
     AppContext, AsyncWindowContext, AvailableSpace, BackgroundExecutor, Bounds, ClipboardEntry,
-    ClipboardItem, Context, DispatchPhase, ElementId, EntityId, EventEmitter, FocusHandle,
-    FocusOutEvent, FocusableView, FontId, FontWeight, HighlightStyle, Hsla, InteractiveText,
-    KeyContext, ListSizingBehavior, Model, MouseButton, PaintQuad, ParentElement, Pixels, Render,
-    SharedString, Size, StrikethroughStyle, Styled, StyledText, Subscription, Task, TextStyle,
-    UTF16Selection, UnderlineStyle, UniformListScrollHandle, View, ViewContext, ViewInputHandler,
-    VisualContext, WeakFocusHandle, WeakView, WindowContext,
+    ClipboardItem, Context, DispatchPhase, ElementId, EventEmitter, FocusHandle, FocusOutEvent,
+    FocusableView, FontId, FontWeight, HighlightStyle, Hsla, InteractiveText, KeyContext,
+    ListSizingBehavior, Model, MouseButton, PaintQuad, ParentElement, Pixels, Render, SharedString,
+    Size, StrikethroughStyle, Styled, StyledText, Subscription, Task, TextStyle, UTF16Selection,
+    UnderlineStyle, UniformListScrollHandle, View, ViewContext, ViewInputHandler, VisualContext,
+    WeakFocusHandle, WeakView, WindowContext,
 };
 use highlight_matching_bracket::refresh_matching_bracket_highlights;
 use hover_popover::{hide_hover, HoverState};
@@ -171,7 +171,7 @@ use workspace::{Item as WorkspaceItem, OpenInTerminal, OpenTerminal, TabBarSetti
 use crate::hover_links::find_url;
 use crate::signature_help::{SignatureHelpHiddenBy, SignatureHelpState};
 
-pub const FILE_HEADER_HEIGHT: u32 = 1;
+pub const FILE_HEADER_HEIGHT: u32 = 2;
 pub const MULTI_BUFFER_EXCERPT_HEADER_HEIGHT: u32 = 1;
 pub const MULTI_BUFFER_EXCERPT_FOOTER_HEIGHT: u32 = 1;
 pub const DEFAULT_MULTIBUFFER_CONTEXT: u32 = 2;
@@ -640,7 +640,6 @@ pub struct Editor {
     tasks: BTreeMap<(BufferId, BufferRow), RunnableTasks>,
     tasks_update_task: Option<Task<()>>,
     previous_search_ranges: Option<Arc<[Range<Anchor>]>>,
-    file_header_size: u32,
     breadcrumb_header: Option<String>,
     focused_block: Option<FocusedBlock>,
     next_scroll_position: NextScrollCursorCenterTopBottom,
@@ -1846,7 +1845,6 @@ impl Editor {
             }),
             merge_adjacent: true,
         };
-        let file_header_size = if show_excerpt_controls { 3 } else { 2 };
         let display_map = cx.new_model(|cx| {
             DisplayMap::new(
                 buffer.clone(),
@@ -1854,7 +1852,7 @@ impl Editor {
                 font_size,
                 None,
                 show_excerpt_controls,
-                file_header_size,
+                FILE_HEADER_HEIGHT,
                 MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
                 MULTI_BUFFER_EXCERPT_FOOTER_HEIGHT,
                 fold_placeholder,
@@ -2038,7 +2036,6 @@ impl Editor {
                 .restore_unsaved_buffers,
             blame: None,
             blame_subscription: None,
-            file_header_size,
             tasks: Default::default(),
             _subscriptions: vec![
                 cx.observe(&buffer, Self::on_buffer_changed),
@@ -12808,7 +12805,7 @@ impl Editor {
     }
 
     pub fn file_header_size(&self) -> u32 {
-        self.file_header_size
+        FILE_HEADER_HEIGHT
     }
 
     pub fn revert(
@@ -14120,7 +14117,7 @@ pub fn diagnostic_block_renderer(
 
         let multi_line_diagnostic = diagnostic.message.contains('\n');
 
-        let buttons = |diagnostic: &Diagnostic, block_id: BlockId| {
+        let buttons = |diagnostic: &Diagnostic| {
             if multi_line_diagnostic {
                 v_flex()
             } else {
@@ -14128,7 +14125,7 @@ pub fn diagnostic_block_renderer(
             }
             .when(allow_closing, |div| {
                 div.children(diagnostic.is_primary.then(|| {
-                    IconButton::new(("close-block", EntityId::from(block_id)), IconName::XCircle)
+                    IconButton::new("close-block", IconName::XCircle)
                         .icon_color(Color::Muted)
                         .size(ButtonSize::Compact)
                         .style(ButtonStyle::Transparent)
@@ -14138,7 +14135,7 @@ pub fn diagnostic_block_renderer(
                 }))
             })
             .child(
-                IconButton::new(("copy-block", EntityId::from(block_id)), IconName::Copy)
+                IconButton::new("copy-block", IconName::Copy)
                     .icon_color(Color::Muted)
                     .size(ButtonSize::Compact)
                     .style(ButtonStyle::Transparent)
@@ -14153,7 +14150,7 @@ pub fn diagnostic_block_renderer(
             )
         };
 
-        let icon_size = buttons(&diagnostic, cx.block_id)
+        let icon_size = buttons(&diagnostic)
             .into_any_element()
             .layout_as_root(AvailableSpace::min_size(), cx);
 
@@ -14170,7 +14167,7 @@ pub fn diagnostic_block_renderer(
                     .w(cx.anchor_x - cx.gutter_dimensions.width - icon_size.width)
                     .flex_shrink(),
             )
-            .child(buttons(&diagnostic, cx.block_id))
+            .child(buttons(&diagnostic))
             .child(div().flex().flex_shrink_0().child(
                 StyledText::new(text_without_backticks.clone()).with_highlights(
                     &text_style,

crates/editor/src/element.rs 🔗

@@ -21,7 +21,8 @@ use crate::{
     EditorSnapshot, EditorStyle, ExpandExcerpts, FocusedBlock, GutterDimensions, HalfPageDown,
     HalfPageUp, HandleInput, HoveredCursor, HoveredHunk, LineDown, LineUp, OpenExcerpts, PageDown,
     PageUp, Point, RowExt, RowRangeExt, SelectPhase, Selection, SoftWrap, ToPoint,
-    CURSORS_VISIBLE_FOR, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN,
+    CURSORS_VISIBLE_FOR, FILE_HEADER_HEIGHT, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN,
+    MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
 };
 use client::ParticipantIndex;
 use collections::{BTreeMap, HashMap};
@@ -31,7 +32,7 @@ use gpui::{
     anchored, deferred, div, fill, outline, point, px, quad, relative, size, svg,
     transparent_black, Action, AnchorCorner, AnyElement, AvailableSpace, Bounds, ClipboardItem,
     ContentMask, Corners, CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Entity,
-    EntityId, FontId, GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement, Length,
+    FontId, GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement, Length,
     ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad,
     ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString, Size,
     StatefulInteractiveElement, Style, Styled, TextRun, TextStyle, TextStyleRefinement, View,
@@ -46,7 +47,7 @@ use language::{
     ChunkRendererContext,
 };
 use lsp::DiagnosticSeverity;
-use multi_buffer::{Anchor, MultiBufferPoint, MultiBufferRow};
+use multi_buffer::{Anchor, ExcerptId, ExpandExcerptDirection, MultiBufferPoint, MultiBufferRow};
 use project::{
     project_settings::{GitGutterSetting, ProjectSettings},
     ProjectPath,
@@ -1632,7 +1633,7 @@ impl EditorElement {
         let mut block_offset = 0;
         let mut found_excerpt_header = false;
         for (_, block) in snapshot.blocks_in_range(prev_line..row_range.start) {
-            if matches!(block, Block::ExcerptHeader { .. }) {
+            if matches!(block, Block::ExcerptBoundary { .. }) {
                 found_excerpt_header = true;
                 break;
             }
@@ -1649,7 +1650,7 @@ impl EditorElement {
         let mut block_height = 0;
         let mut found_excerpt_header = false;
         for (_, block) in snapshot.blocks_in_range(row_range.end..cons_line) {
-            if matches!(block, Block::ExcerptHeader { .. }) {
+            if matches!(block, Block::ExcerptBoundary { .. }) {
                 found_excerpt_header = true;
             }
             block_height += block.height();
@@ -2100,23 +2101,14 @@ impl EditorElement {
                     .into_any_element()
             }
 
-            Block::ExcerptHeader {
-                buffer,
-                range,
+            Block::ExcerptBoundary {
+                prev_excerpt,
+                next_excerpt,
+                show_excerpt_controls,
                 starts_new_buffer,
                 height,
-                id,
-                show_excerpt_controls,
                 ..
             } => {
-                let include_root = self
-                    .editor
-                    .read(cx)
-                    .project
-                    .as_ref()
-                    .map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
-                    .unwrap_or_default();
-
                 #[derive(Clone)]
                 struct JumpData {
                     position: Point,
@@ -2125,233 +2117,227 @@ impl EditorElement {
                     line_offset_from_top: u32,
                 }
 
-                let jump_data = project::File::from_dyn(buffer.file()).map(|file| {
-                    let jump_path = ProjectPath {
-                        worktree_id: file.worktree_id(cx),
-                        path: file.path.clone(),
-                    };
-                    let jump_anchor = range
-                        .primary
-                        .as_ref()
-                        .map_or(range.context.start, |primary| primary.start);
-
-                    let excerpt_start = range.context.start;
-                    let jump_position = language::ToPoint::to_point(&jump_anchor, buffer);
-                    let offset_from_excerpt_start = if jump_anchor == excerpt_start {
-                        0
-                    } else {
-                        let excerpt_start_row =
-                            language::ToPoint::to_point(&jump_anchor, buffer).row;
-                        jump_position.row - excerpt_start_row
-                    };
-
-                    let line_offset_from_top =
-                        block_row_start.0 + *height + offset_from_excerpt_start
-                            - snapshot
-                                .scroll_anchor
-                                .scroll_position(&snapshot.display_snapshot)
-                                .y as u32;
-
-                    JumpData {
-                        position: jump_position,
-                        anchor: jump_anchor,
-                        path: jump_path,
-                        line_offset_from_top,
-                    }
-                });
-
                 let icon_offset = gutter_dimensions.width
                     - (gutter_dimensions.left_padding + gutter_dimensions.margin);
 
-                let element = if *starts_new_buffer {
-                    let path = buffer.resolve_file_path(cx, include_root);
-                    let mut filename = None;
-                    let mut parent_path = None;
-                    // Can't use .and_then() because `.file_name()` and `.parent()` return references :(
-                    if let Some(path) = path {
-                        filename = path.file_name().map(|f| f.to_string_lossy().to_string());
-                        parent_path = path
-                            .parent()
-                            .map(|p| SharedString::from(p.to_string_lossy().to_string() + "/"));
-                    }
+                let header_padding = px(6.0);
 
-                    let header_padding = px(6.0);
+                let mut result = v_flex().id(block_id).w_full();
 
-                    v_flex()
-                        .id(("path excerpt header", EntityId::from(block_id)))
-                        .w_full()
-                        .px(header_padding)
-                        .pt(header_padding)
-                        .child(
-                            h_flex()
-                                .flex_basis(Length::Definite(DefiniteLength::Fraction(0.667)))
-                                .id("path header block")
-                                .h(2. * cx.line_height())
-                                .px(gpui::px(12.))
-                                .rounded_md()
-                                .shadow_md()
-                                .border_1()
-                                .border_color(cx.theme().colors().border)
-                                .bg(cx.theme().colors().editor_subheader_background)
-                                .justify_between()
-                                .hover(|style| style.bg(cx.theme().colors().element_hover))
-                                .child(
-                                    h_flex().gap_3().child(
-                                        h_flex()
-                                            .gap_2()
-                                            .child(
-                                                filename
-                                                    .map(SharedString::from)
-                                                    .unwrap_or_else(|| "untitled".into()),
-                                            )
-                                            .when_some(parent_path, |then, path| {
-                                                then.child(
-                                                    div()
-                                                        .child(path)
-                                                        .text_color(cx.theme().colors().text_muted),
-                                                )
-                                            }),
-                                    ),
-                                )
-                                .when_some(jump_data.clone(), |el, jump_data| {
-                                    el.child(Icon::new(IconName::ArrowUpRight))
-                                        .cursor_pointer()
-                                        .tooltip(|cx| {
-                                            Tooltip::for_action("Jump to File", &OpenExcerpts, cx)
-                                        })
-                                        .on_mouse_down(MouseButton::Left, |_, cx| {
-                                            cx.stop_propagation()
-                                        })
-                                        .on_click(cx.listener_for(&self.editor, {
-                                            move |editor, _, cx| {
-                                                editor.jump(
-                                                    jump_data.path.clone(),
-                                                    jump_data.position,
-                                                    jump_data.anchor,
-                                                    jump_data.line_offset_from_top,
-                                                    cx,
-                                                );
-                                            }
-                                        }))
-                                }),
-                        )
-                        .children(show_excerpt_controls.then(|| {
+                if let Some(prev_excerpt) = prev_excerpt {
+                    if *show_excerpt_controls {
+                        result = result.child(
                             h_flex()
-                                .flex_basis(Length::Definite(DefiniteLength::Fraction(0.333)))
-                                .h(1. * cx.line_height())
-                                .pt_1()
-                                .justify_end()
+                                .w(icon_offset)
+                                .h(MULTI_BUFFER_EXCERPT_HEADER_HEIGHT as f32 * cx.line_height())
                                 .flex_none()
-                                .w(icon_offset - header_padding)
+                                .justify_end()
+                                .child(self.render_expand_excerpt_button(
+                                    prev_excerpt.id,
+                                    ExpandExcerptDirection::Down,
+                                    IconName::ArrowDownFromLine,
+                                    cx,
+                                )),
+                        );
+                    }
+                }
+
+                if let Some(next_excerpt) = next_excerpt {
+                    let buffer = &next_excerpt.buffer;
+                    let range = &next_excerpt.range;
+                    let jump_data = project::File::from_dyn(buffer.file()).map(|file| {
+                        let jump_path = ProjectPath {
+                            worktree_id: file.worktree_id(cx),
+                            path: file.path.clone(),
+                        };
+                        let jump_anchor = range
+                            .primary
+                            .as_ref()
+                            .map_or(range.context.start, |primary| primary.start);
+
+                        let excerpt_start = range.context.start;
+                        let jump_position = language::ToPoint::to_point(&jump_anchor, buffer);
+                        let offset_from_excerpt_start = if jump_anchor == excerpt_start {
+                            0
+                        } else {
+                            let excerpt_start_row =
+                                language::ToPoint::to_point(&jump_anchor, buffer).row;
+                            jump_position.row - excerpt_start_row
+                        };
+
+                        let line_offset_from_top =
+                            block_row_start.0 + *height + offset_from_excerpt_start
+                                - snapshot
+                                    .scroll_anchor
+                                    .scroll_position(&snapshot.display_snapshot)
+                                    .y as u32;
+
+                        JumpData {
+                            position: jump_position,
+                            anchor: jump_anchor,
+                            path: jump_path,
+                            line_offset_from_top,
+                        }
+                    });
+
+                    if *starts_new_buffer {
+                        let include_root = self
+                            .editor
+                            .read(cx)
+                            .project
+                            .as_ref()
+                            .map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
+                            .unwrap_or_default();
+                        let path = buffer.resolve_file_path(cx, include_root);
+                        let filename = path
+                            .as_ref()
+                            .and_then(|path| Some(path.file_name()?.to_string_lossy().to_string()));
+                        let parent_path = path.as_ref().and_then(|path| {
+                            Some(path.parent()?.to_string_lossy().to_string() + "/")
+                        });
+
+                        result = result.child(
+                            div()
+                                .px(header_padding)
+                                .pt(header_padding)
+                                .w_full()
+                                .h(FILE_HEADER_HEIGHT as f32 * cx.line_height())
                                 .child(
-                                    ButtonLike::new("expand-icon")
-                                        .style(ButtonStyle::Transparent)
+                                    h_flex()
+                                        .id("path header block")
+                                        .size_full()
+                                        .flex_basis(Length::Definite(DefiniteLength::Fraction(
+                                            0.667,
+                                        )))
+                                        .px(gpui::px(12.))
+                                        .rounded_md()
+                                        .shadow_md()
+                                        .border_1()
+                                        .border_color(cx.theme().colors().border)
+                                        .bg(cx.theme().colors().editor_subheader_background)
+                                        .justify_between()
+                                        .hover(|style| style.bg(cx.theme().colors().element_hover))
                                         .child(
-                                            svg()
-                                                .path(IconName::ArrowUpFromLine.path())
-                                                .size(IconSize::XSmall.rems())
-                                                .text_color(cx.theme().colors().editor_line_number)
-                                                .group("")
-                                                .hover(|style| {
-                                                    style.text_color(
-                                                        cx.theme()
-                                                            .colors()
-                                                            .editor_active_line_number,
+                                            h_flex().gap_3().child(
+                                                h_flex()
+                                                    .gap_2()
+                                                    .child(
+                                                        filename
+                                                            .map(SharedString::from)
+                                                            .unwrap_or_else(|| "untitled".into()),
                                                     )
-                                                }),
+                                                    .when_some(parent_path, |then, path| {
+                                                        then.child(div().child(path).text_color(
+                                                            cx.theme().colors().text_muted,
+                                                        ))
+                                                    }),
+                                            ),
                                         )
-                                        .on_click(cx.listener_for(&self.editor, {
-                                            let id = *id;
-                                            move |editor, _, cx| {
-                                                editor.expand_excerpt(
-                                                    id,
-                                                    multi_buffer::ExpandExcerptDirection::Up,
-                                                    cx,
-                                                );
-                                            }
-                                        }))
-                                        .tooltip({
-                                            move |cx| {
-                                                Tooltip::for_action(
-                                                    "Expand Excerpt",
-                                                    &ExpandExcerpts { lines: 0 },
-                                                    cx,
-                                                )
-                                            }
+                                        .when_some(jump_data, |el, jump_data| {
+                                            el.child(Icon::new(IconName::ArrowUpRight))
+                                                .cursor_pointer()
+                                                .tooltip(|cx| {
+                                                    Tooltip::for_action(
+                                                        "Jump to File",
+                                                        &OpenExcerpts,
+                                                        cx,
+                                                    )
+                                                })
+                                                .on_mouse_down(MouseButton::Left, |_, cx| {
+                                                    cx.stop_propagation()
+                                                })
+                                                .on_click(cx.listener_for(&self.editor, {
+                                                    move |editor, _, cx| {
+                                                        editor.jump(
+                                                            jump_data.path.clone(),
+                                                            jump_data.position,
+                                                            jump_data.anchor,
+                                                            jump_data.line_offset_from_top,
+                                                            cx,
+                                                        );
+                                                    }
+                                                }))
                                         }),
-                                )
-                        }))
-                } else {
-                    v_flex()
-                        .id(("excerpt header", EntityId::from(block_id)))
-                        .w_full()
-                        .h(snapshot.excerpt_header_height() as f32 * cx.line_height())
-                        .child(
-                            div()
-                                .flex()
-                                .v_flex()
+                                ),
+                        );
+                        if *show_excerpt_controls {
+                            result = result.child(
+                                h_flex()
+                                    .w(icon_offset)
+                                    .h(MULTI_BUFFER_EXCERPT_HEADER_HEIGHT as f32 * cx.line_height())
+                                    .flex_none()
+                                    .justify_end()
+                                    .child(self.render_expand_excerpt_button(
+                                        next_excerpt.id,
+                                        ExpandExcerptDirection::Up,
+                                        IconName::ArrowUpFromLine,
+                                        cx,
+                                    )),
+                            );
+                        }
+                    } else {
+                        result = result.child(
+                            h_flex()
+                                .id("excerpt header block")
+                                .group("excerpt-jump-action")
                                 .justify_start()
-                                .id("jump to collapsed context")
-                                .w(relative(1.0))
-                                .h_full()
+                                .w_full()
+                                .h(MULTI_BUFFER_EXCERPT_HEADER_HEIGHT as f32 * cx.line_height())
+                                .relative()
                                 .child(
                                     div()
-                                        .h_px()
+                                        .top(px(0.))
+                                        .absolute()
                                         .w_full()
+                                        .h_px()
                                         .bg(cx.theme().colors().border_variant)
                                         .group_hover("excerpt-jump-action", |style| {
                                             style.bg(cx.theme().colors().border)
                                         }),
-                                ),
-                        )
-                        .child(
-                            h_flex()
-                                .justify_end()
-                                .flex_none()
-                                .w(icon_offset)
-                                .h_full()
+                                )
+                                .cursor_pointer()
+                                .when_some(jump_data.clone(), |this, jump_data| {
+                                    this.on_click(cx.listener_for(&self.editor, {
+                                        let path = jump_data.path.clone();
+                                        move |editor, _, cx| {
+                                            cx.stop_propagation();
+
+                                            editor.jump(
+                                                path.clone(),
+                                                jump_data.position,
+                                                jump_data.anchor,
+                                                jump_data.line_offset_from_top,
+                                                cx,
+                                            );
+                                        }
+                                    }))
+                                    .tooltip(move |cx| {
+                                        Tooltip::for_action(
+                                            format!(
+                                                "Jump to {}:L{}",
+                                                jump_data.path.path.display(),
+                                                jump_data.position.row + 1
+                                            ),
+                                            &OpenExcerpts,
+                                            cx,
+                                        )
+                                    })
+                                })
                                 .child(
-                                    show_excerpt_controls
-                                        .then(|| {
-                                            ButtonLike::new("expand-icon")
-                                                .style(ButtonStyle::Transparent)
-                                                .child(
-                                                    svg()
-                                                        .path(IconName::ArrowUpFromLine.path())
-                                                        .size(IconSize::XSmall.rems())
-                                                        .text_color(
-                                                            cx.theme().colors().editor_line_number,
-                                                        )
-                                                        .group("")
-                                                        .hover(|style| {
-                                                            style.text_color(
-                                                                cx.theme()
-                                                                    .colors()
-                                                                    .editor_active_line_number,
-                                                            )
-                                                        }),
-                                                )
-                                                .on_click(cx.listener_for(&self.editor, {
-                                                    let id = *id;
-                                                    move |editor, _, cx| {
-                                                        editor.expand_excerpt(
-                                                        id,
-                                                        multi_buffer::ExpandExcerptDirection::Up,
-                                                        cx,
-                                                    );
-                                                    }
-                                                }))
-                                                .tooltip({
-                                                    move |cx| {
-                                                        Tooltip::for_action(
-                                                            "Expand Excerpt",
-                                                            &ExpandExcerpts { lines: 0 },
-                                                            cx,
-                                                        )
-                                                    }
-                                                })
-                                        })
-                                        .unwrap_or_else(|| {
+                                    h_flex()
+                                        .w(icon_offset)
+                                        .h(MULTI_BUFFER_EXCERPT_HEADER_HEIGHT as f32
+                                            * cx.line_height())
+                                        .flex_none()
+                                        .justify_end()
+                                        .child(if *show_excerpt_controls {
+                                            self.render_expand_excerpt_button(
+                                                next_excerpt.id,
+                                                ExpandExcerptDirection::Up,
+                                                IconName::ArrowUpFromLine,
+                                                cx,
+                                            )
+                                        } else {
                                             ButtonLike::new("jump-icon")
                                                 .style(ButtonStyle::Transparent)
                                                 .child(
@@ -2361,7 +2347,6 @@ impl EditorElement {
                                                         .text_color(
                                                             cx.theme().colors().border_variant,
                                                         )
-                                                        .group("excerpt-jump-action")
                                                         .group_hover(
                                                             "excerpt-jump-action",
                                                             |style| {
@@ -2371,118 +2356,13 @@ impl EditorElement {
                                                             },
                                                         ),
                                                 )
-                                                .when_some(jump_data.clone(), |this, jump_data| {
-                                                    this.on_click(cx.listener_for(&self.editor, {
-                                                        let path = jump_data.path.clone();
-                                                        move |editor, _, cx| {
-                                                            cx.stop_propagation();
-
-                                                            editor.jump(
-                                                                path.clone(),
-                                                                jump_data.position,
-                                                                jump_data.anchor,
-                                                                jump_data.line_offset_from_top,
-                                                                cx,
-                                                            );
-                                                        }
-                                                    }))
-                                                    .tooltip(move |cx| {
-                                                        Tooltip::for_action(
-                                                            format!(
-                                                                "Jump to {}:L{}",
-                                                                jump_data.path.path.display(),
-                                                                jump_data.position.row + 1
-                                                            ),
-                                                            &OpenExcerpts,
-                                                            cx,
-                                                        )
-                                                    })
-                                                })
                                         }),
                                 ),
-                        )
-                        .group("excerpt-jump-action")
-                        .cursor_pointer()
-                        .when_some(jump_data.clone(), |this, jump_data| {
-                            this.on_click(cx.listener_for(&self.editor, {
-                                let path = jump_data.path.clone();
-                                move |editor, _, cx| {
-                                    cx.stop_propagation();
-
-                                    editor.jump(
-                                        path.clone(),
-                                        jump_data.position,
-                                        jump_data.anchor,
-                                        jump_data.line_offset_from_top,
-                                        cx,
-                                    );
-                                }
-                            }))
-                            .tooltip(move |cx| {
-                                Tooltip::for_action(
-                                    format!(
-                                        "Jump to {}:L{}",
-                                        jump_data.path.path.display(),
-                                        jump_data.position.row + 1
-                                    ),
-                                    &OpenExcerpts,
-                                    cx,
-                                )
-                            })
-                        })
-                };
-                element.into_any()
-            }
+                        );
+                    }
+                }
 
-            Block::ExcerptFooter { id, .. } => {
-                let element = v_flex()
-                    .id(("excerpt footer", EntityId::from(block_id)))
-                    .w_full()
-                    .h(snapshot.excerpt_footer_height() as f32 * cx.line_height())
-                    .child(
-                        h_flex()
-                            .justify_end()
-                            .flex_none()
-                            .w(gutter_dimensions.width
-                                - (gutter_dimensions.left_padding + gutter_dimensions.margin))
-                            .h_full()
-                            .child(
-                                ButtonLike::new("expand-icon")
-                                    .style(ButtonStyle::Transparent)
-                                    .child(
-                                        svg()
-                                            .path(IconName::ArrowDownFromLine.path())
-                                            .size(IconSize::XSmall.rems())
-                                            .text_color(cx.theme().colors().editor_line_number)
-                                            .group("")
-                                            .hover(|style| {
-                                                style.text_color(
-                                                    cx.theme().colors().editor_active_line_number,
-                                                )
-                                            }),
-                                    )
-                                    .on_click(cx.listener_for(&self.editor, {
-                                        let id = *id;
-                                        move |editor, _, cx| {
-                                            editor.expand_excerpt(
-                                                id,
-                                                multi_buffer::ExpandExcerptDirection::Down,
-                                                cx,
-                                            );
-                                        }
-                                    }))
-                                    .tooltip({
-                                        move |cx| {
-                                            Tooltip::for_action(
-                                                "Expand Excerpt",
-                                                &ExpandExcerpts { lines: 0 },
-                                                cx,
-                                            )
-                                        }
-                                    }),
-                            ),
-                    );
-                element.into_any()
+                result.into_any()
             }
         };
 
@@ -2509,6 +2389,33 @@ impl EditorElement {
         (element, final_size)
     }
 
+    fn render_expand_excerpt_button(
+        &self,
+        excerpt_id: ExcerptId,
+        direction: ExpandExcerptDirection,
+        icon: IconName,
+        cx: &mut WindowContext,
+    ) -> ButtonLike {
+        ButtonLike::new("expand-icon")
+            .style(ButtonStyle::Transparent)
+            .child(
+                svg()
+                    .path(icon.path())
+                    .size(IconSize::XSmall.rems())
+                    .text_color(cx.theme().colors().editor_line_number)
+                    .group("")
+                    .hover(|style| style.text_color(cx.theme().colors().editor_active_line_number)),
+            )
+            .on_click(cx.listener_for(&self.editor, {
+                move |editor, _, cx| {
+                    editor.expand_excerpt(excerpt_id, direction, cx);
+                }
+            }))
+            .tooltip({
+                move |cx| Tooltip::for_action("Expand Excerpt", &ExpandExcerpts { lines: 0 }, cx)
+            })
+    }
+
     #[allow(clippy::too_many_arguments)]
     fn render_blocks(
         &self,
@@ -3367,7 +3274,7 @@ impl EditorElement {
                     let end_row_in_current_excerpt = snapshot
                         .blocks_in_range(start_row..end_row)
                         .find_map(|(start_row, block)| {
-                            if matches!(block, Block::ExcerptHeader { .. }) {
+                            if matches!(block, Block::ExcerptBoundary { .. }) {
                                 Some(start_row)
                             } else {
                                 None

crates/multi_buffer/src/multi_buffer.rs 🔗

@@ -189,6 +189,7 @@ pub struct MultiBufferSnapshot {
     show_headers: bool,
 }
 
+#[derive(Clone)]
 pub struct ExcerptInfo {
     pub id: ExcerptId,
     pub buffer: BufferSnapshot,
@@ -201,6 +202,7 @@ impl std::fmt::Debug for ExcerptInfo {
         f.debug_struct(type_name::<Self>())
             .field("id", &self.id)
             .field("buffer_id", &self.buffer_id)
+            .field("path", &self.buffer.file().map(|f| f.path()))
             .field("range", &self.range)
             .finish()
     }

crates/repl/src/session.rs 🔗

@@ -17,8 +17,7 @@ use editor::{
 use futures::io::BufReader;
 use futures::{AsyncBufReadExt as _, FutureExt as _, StreamExt as _};
 use gpui::{
-    div, prelude::*, EntityId, EventEmitter, Model, Render, Subscription, Task, View, ViewContext,
-    WeakView,
+    div, prelude::*, EventEmitter, Model, Render, Subscription, Task, View, ViewContext, WeakView,
 };
 use language::Point;
 use project::Fs;
@@ -149,23 +148,21 @@ impl EditorBlock {
                 .w(text_line_height)
                 .h(text_line_height)
                 .child(
-                    IconButton::new(
-                        ("close_output_area", EntityId::from(cx.block_id)),
-                        IconName::Close,
-                    )
-                    .icon_size(IconSize::Small)
-                    .icon_color(Color::Muted)
-                    .size(ButtonSize::Compact)
-                    .shape(IconButtonShape::Square)
-                    .tooltip(|cx| Tooltip::text("Close output area", cx))
-                    .on_click(move |_, cx| {
-                        if let BlockId::Custom(block_id) = block_id {
-                            (on_close)(block_id, cx)
-                        }
-                    }),
+                    IconButton::new("close_output_area", IconName::Close)
+                        .icon_size(IconSize::Small)
+                        .icon_color(Color::Muted)
+                        .size(ButtonSize::Compact)
+                        .shape(IconButtonShape::Square)
+                        .tooltip(|cx| Tooltip::text("Close output area", cx))
+                        .on_click(move |_, cx| {
+                            if let BlockId::Custom(block_id) = block_id {
+                                (on_close)(block_id, cx)
+                            }
+                        }),
                 );
 
             div()
+                .id(cx.block_id)
                 .flex()
                 .items_start()
                 .min_h(text_line_height)