Allow folding buffers inside multi buffers (#22046)

Kirill Bulatov , Antonio Scandurra , Max Brunsfeld , and Cole Miller created

Closes https://github.com/zed-industries/zed/issues/4925


https://github.com/user-attachments/assets/e7b87375-893f-41ae-a2d9-d501499e40d1


Allows to fold any buffer inside multi buffers, either by clicking the
chevron icon on the header, or by using
`editor::Fold`/`editor::UnfoldLines`/`editor::ToggleFold`/`editor::FoldAll`
and `editor::UnfoldAll` actions inside the multi buffer (those were noop
there before).

Every fold has a fake line inside it, so it's possible to navigate into
that via the keyboard and unfold it with the corresponding editor
action.

The state is synchronized with the outline panel state: any fold inside
multi buffer folds the corresponding file entry; any file entry fold
inside the outline panel folds the corresponding buffer inside the multi
buffer, any directory fold inside the outline panel folds the
corresponding buffers inside the multi buffer for each nested file entry
in the panel.


Release Notes:

- Added a possibility to fold buffers inside multi buffers

---------

Co-authored-by: Antonio Scandurra <antonio@zed.dev>
Co-authored-by: Max Brunsfeld <max@zed.dev>
Co-authored-by: Cole Miller <cole@zed.dev>

Change summary

crates/diagnostics/src/diagnostics_tests.rs |    1 
crates/editor/src/display_map.rs            |   48 
crates/editor/src/display_map/block_map.rs  | 1031 ++++++++++++++++++----
crates/editor/src/editor.rs                 |  285 ++++-
crates/editor/src/editor_tests.rs           |  408 +++++++++
crates/editor/src/element.rs                |  380 +++++---
crates/editor/src/indent_guides.rs          |    8 
crates/multi_buffer/src/multi_buffer.rs     |   30 
crates/outline_panel/src/outline_panel.rs   |  643 +++++++++++--
9 files changed, 2,272 insertions(+), 562 deletions(-)

Detailed changes

crates/editor/src/display_map.rs 🔗

@@ -269,7 +269,7 @@ impl DisplayMap {
                     let start = buffer_snapshot.anchor_before(range.start);
                     let end = buffer_snapshot.anchor_after(range.end);
                     BlockProperties {
-                        placement: BlockPlacement::Replace(start..end),
+                        placement: BlockPlacement::Replace(start..=end),
                         render,
                         height,
                         style,
@@ -336,6 +336,38 @@ impl DisplayMap {
         block_map.remove_intersecting_replace_blocks(offset_ranges, inclusive);
     }
 
+    pub fn fold_buffer(&mut self, buffer_id: language::BufferId, cx: &mut ModelContext<Self>) {
+        let snapshot = self.buffer.read(cx).snapshot(cx);
+        let edits = self.buffer_subscription.consume().into_inner();
+        let tab_size = Self::tab_size(&self.buffer, cx);
+        let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
+        let (snapshot, edits) = self.fold_map.read(snapshot, edits);
+        let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
+        let (snapshot, edits) = self
+            .wrap_map
+            .update(cx, |map, cx| map.sync(snapshot, edits, cx));
+        let mut block_map = self.block_map.write(snapshot, edits);
+        block_map.fold_buffer(buffer_id, self.buffer.read(cx), cx)
+    }
+
+    pub fn unfold_buffer(&mut self, buffer_id: language::BufferId, cx: &mut ModelContext<Self>) {
+        let snapshot = self.buffer.read(cx).snapshot(cx);
+        let edits = self.buffer_subscription.consume().into_inner();
+        let tab_size = Self::tab_size(&self.buffer, cx);
+        let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
+        let (snapshot, edits) = self.fold_map.read(snapshot, edits);
+        let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
+        let (snapshot, edits) = self
+            .wrap_map
+            .update(cx, |map, cx| map.sync(snapshot, edits, cx));
+        let mut block_map = self.block_map.write(snapshot, edits);
+        block_map.unfold_buffer(buffer_id, self.buffer.read(cx), cx)
+    }
+
+    pub(crate) fn buffer_folded(&self, buffer_id: language::BufferId) -> bool {
+        self.block_map.folded_buffers.contains(&buffer_id)
+    }
+
     pub fn insert_creases(
         &mut self,
         creases: impl IntoIterator<Item = Crease<Anchor>>,
@@ -712,7 +744,11 @@ impl DisplaySnapshot {
         }
     }
 
-    pub fn next_line_boundary(&self, mut point: MultiBufferPoint) -> (Point, DisplayPoint) {
+    pub fn next_line_boundary(
+        &self,
+        mut point: MultiBufferPoint,
+    ) -> (MultiBufferPoint, DisplayPoint) {
+        let original_point = point;
         loop {
             let mut inlay_point = self.inlay_snapshot.to_inlay_point(point);
             let mut fold_point = self.fold_snapshot.to_fold_point(inlay_point, Bias::Right);
@@ -723,7 +759,7 @@ impl DisplaySnapshot {
             let mut display_point = self.point_to_display_point(point, Bias::Right);
             *display_point.column_mut() = self.line_len(display_point.row());
             let next_point = self.display_point_to_point(display_point, Bias::Right);
-            if next_point == point {
+            if next_point == point || original_point == point || original_point == next_point {
                 return (point, display_point);
             }
             point = next_point;
@@ -1081,10 +1117,6 @@ impl DisplaySnapshot {
             || self.fold_snapshot.is_line_folded(buffer_row)
     }
 
-    pub fn is_line_replaced(&self, buffer_row: MultiBufferRow) -> bool {
-        self.block_snapshot.is_line_replaced(buffer_row)
-    }
-
     pub fn is_block_line(&self, display_row: DisplayRow) -> bool {
         self.block_snapshot.is_block_line(BlockRow(display_row.0))
     }
@@ -2231,7 +2263,7 @@ pub mod tests {
                 [BlockProperties {
                     placement: BlockPlacement::Replace(
                         buffer_snapshot.anchor_before(Point::new(1, 2))
-                            ..buffer_snapshot.anchor_after(Point::new(2, 3)),
+                            ..=buffer_snapshot.anchor_after(Point::new(2, 3)),
                     ),
                     height: 4,
                     style: BlockStyle::Fixed,

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

@@ -4,24 +4,25 @@ use super::{
 };
 use crate::{EditorStyle, GutterDimensions};
 use collections::{Bound, HashMap, HashSet};
-use gpui::{AnyElement, EntityId, Pixels, WindowContext};
+use gpui::{AnyElement, AppContext, EntityId, Pixels, WindowContext};
 use language::{Chunk, Patch, Point};
 use multi_buffer::{
-    Anchor, ExcerptId, ExcerptInfo, MultiBufferRow, MultiBufferSnapshot, ToOffset, ToPoint as _,
+    Anchor, ExcerptId, ExcerptInfo, MultiBuffer, MultiBufferRow, MultiBufferSnapshot, ToOffset,
+    ToPoint as _,
 };
 use parking_lot::Mutex;
 use std::{
     cell::RefCell,
     cmp::{self, Ordering},
     fmt::Debug,
-    ops::{Deref, DerefMut, Range, RangeBounds},
+    ops::{Deref, DerefMut, Range, RangeBounds, RangeInclusive},
     sync::{
         atomic::{AtomicUsize, Ordering::SeqCst},
         Arc,
     },
 };
 use sum_tree::{Bias, SumTree, Summary, TreeMap};
-use text::Edit;
+use text::{BufferId, Edit};
 use ui::ElementId;
 
 const NEWLINES: &[u8] = &[b'\n'; u8::MAX as usize];
@@ -40,6 +41,7 @@ pub struct BlockMap {
     buffer_header_height: u32,
     excerpt_header_height: u32,
     excerpt_footer_height: u32,
+    pub(super) folded_buffers: HashSet<BufferId>,
 }
 
 pub struct BlockMapReader<'a> {
@@ -83,7 +85,7 @@ pub type RenderBlock = Arc<dyn Send + Sync + Fn(&mut BlockContext) -> AnyElement
 pub enum BlockPlacement<T> {
     Above(T),
     Below(T),
-    Replace(Range<T>),
+    Replace(RangeInclusive<T>),
 }
 
 impl<T> BlockPlacement<T> {
@@ -91,7 +93,7 @@ impl<T> BlockPlacement<T> {
         match self {
             BlockPlacement::Above(position) => position,
             BlockPlacement::Below(position) => position,
-            BlockPlacement::Replace(range) => &range.start,
+            BlockPlacement::Replace(range) => range.start(),
         }
     }
 
@@ -99,7 +101,7 @@ impl<T> BlockPlacement<T> {
         match self {
             BlockPlacement::Above(position) => position,
             BlockPlacement::Below(position) => position,
-            BlockPlacement::Replace(range) => &range.end,
+            BlockPlacement::Replace(range) => range.end(),
         }
     }
 
@@ -107,7 +109,7 @@ impl<T> BlockPlacement<T> {
         match self {
             BlockPlacement::Above(position) => BlockPlacement::Above(position),
             BlockPlacement::Below(position) => BlockPlacement::Below(position),
-            BlockPlacement::Replace(range) => BlockPlacement::Replace(&range.start..&range.end),
+            BlockPlacement::Replace(range) => BlockPlacement::Replace(range.start()..=range.end()),
         }
     }
 
@@ -115,7 +117,10 @@ impl<T> BlockPlacement<T> {
         match self {
             BlockPlacement::Above(position) => BlockPlacement::Above(f(position)),
             BlockPlacement::Below(position) => BlockPlacement::Below(f(position)),
-            BlockPlacement::Replace(range) => BlockPlacement::Replace(f(range.start)..f(range.end)),
+            BlockPlacement::Replace(range) => {
+                let (start, end) = range.into_inner();
+                BlockPlacement::Replace(f(start)..=f(end))
+            }
         }
     }
 }
@@ -134,21 +139,21 @@ impl BlockPlacement<Anchor> {
                 anchor_a.cmp(anchor_b, buffer).then(Ordering::Greater)
             }
             (BlockPlacement::Above(anchor), BlockPlacement::Replace(range)) => {
-                anchor.cmp(&range.start, buffer).then(Ordering::Less)
+                anchor.cmp(range.start(), buffer).then(Ordering::Less)
             }
             (BlockPlacement::Replace(range), BlockPlacement::Above(anchor)) => {
-                range.start.cmp(anchor, buffer).then(Ordering::Greater)
+                range.start().cmp(anchor, buffer).then(Ordering::Greater)
             }
             (BlockPlacement::Below(anchor), BlockPlacement::Replace(range)) => {
-                anchor.cmp(&range.start, buffer).then(Ordering::Greater)
+                anchor.cmp(range.start(), buffer).then(Ordering::Greater)
             }
             (BlockPlacement::Replace(range), BlockPlacement::Below(anchor)) => {
-                range.start.cmp(anchor, buffer).then(Ordering::Less)
+                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)),
+                .start()
+                .cmp(range_b.start(), buffer)
+                .then_with(|| range_b.end().cmp(range_a.end(), buffer)),
         }
     }
 
@@ -168,8 +173,8 @@ impl BlockPlacement<Anchor> {
                 Some(BlockPlacement::Below(wrap_row))
             }
             BlockPlacement::Replace(range) => {
-                let mut start = range.start.to_point(buffer_snapshot);
-                let mut end = range.end.to_point(buffer_snapshot);
+                let mut start = range.start().to_point(buffer_snapshot);
+                let mut end = range.end().to_point(buffer_snapshot);
                 if start == end {
                     None
                 } else {
@@ -179,50 +184,13 @@ impl BlockPlacement<Anchor> {
                     end.column = buffer_snapshot.line_len(MultiBufferRow(end.row));
                     let end_wrap_row =
                         WrapRow(wrap_snapshot.make_wrap_point(end, Bias::Left).row());
-                    Some(BlockPlacement::Replace(start_wrap_row..end_wrap_row))
+                    Some(BlockPlacement::Replace(start_wrap_row..=end_wrap_row))
                 }
             }
         }
     }
 }
 
-impl Ord for BlockPlacement<WrapRow> {
-    fn cmp(&self, other: &Self) -> Ordering {
-        match (self, other) {
-            (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::Less)
-            }
-            (BlockPlacement::Replace(range), BlockPlacement::Above(row)) => {
-                range.start.cmp(row).then(Ordering::Greater)
-            }
-            (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)),
-        }
-    }
-}
-
-impl PartialOrd for BlockPlacement<WrapRow> {
-    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
-        Some(self.cmp(other))
-    }
-}
-
 pub struct CustomBlock {
     id: CustomBlockId,
     placement: BlockPlacement<Anchor>,
@@ -272,6 +240,7 @@ pub struct BlockContext<'a, 'b> {
 #[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash)]
 pub enum BlockId {
     ExcerptBoundary(Option<ExcerptId>),
+    FoldedBuffer(ExcerptId),
     Custom(CustomBlockId),
 }
 
@@ -283,6 +252,7 @@ impl From<BlockId> for ElementId {
                 Some(id) => ("ExcerptBoundary", EntityId::from(id)).into(),
                 None => "LastExcerptBoundary".into(),
             },
+            BlockId::FoldedBuffer(id) => ("FoldedBuffer", EntityId::from(id)).into(),
         }
     }
 }
@@ -292,6 +262,7 @@ impl std::fmt::Display for BlockId {
         match self {
             Self::Custom(id) => write!(f, "Block({id:?})"),
             Self::ExcerptBoundary(id) => write!(f, "ExcerptHeader({id:?})"),
+            Self::FoldedBuffer(id) => write!(f, "FoldedBuffer({id:?})"),
         }
     }
 }
@@ -306,6 +277,12 @@ struct Transform {
 #[derive(Clone)]
 pub enum Block {
     Custom(Arc<CustomBlock>),
+    FoldedBuffer {
+        first_excerpt: ExcerptInfo,
+        prev_excerpt: Option<ExcerptInfo>,
+        height: u32,
+        show_excerpt_controls: bool,
+    },
     ExcerptBoundary {
         prev_excerpt: Option<ExcerptInfo>,
         next_excerpt: Option<ExcerptInfo>,
@@ -322,26 +299,28 @@ impl Block {
             Block::ExcerptBoundary { next_excerpt, .. } => {
                 BlockId::ExcerptBoundary(next_excerpt.as_ref().map(|info| info.id))
             }
+            Block::FoldedBuffer { first_excerpt, .. } => BlockId::FoldedBuffer(first_excerpt.id),
         }
     }
 
     pub fn height(&self) -> u32 {
         match self {
             Block::Custom(block) => block.height,
-            Block::ExcerptBoundary { height, .. } => *height,
+            Block::ExcerptBoundary { height, .. } | Block::FoldedBuffer { height, .. } => *height,
         }
     }
 
     pub fn style(&self) -> BlockStyle {
         match self {
             Block::Custom(block) => block.style,
-            Block::ExcerptBoundary { .. } => BlockStyle::Sticky,
+            Block::ExcerptBoundary { .. } | Block::FoldedBuffer { .. } => BlockStyle::Sticky,
         }
     }
 
     fn place_above(&self) -> bool {
         match self {
             Block::Custom(block) => matches!(block.placement, BlockPlacement::Above(_)),
+            Block::FoldedBuffer { .. } => false,
             Block::ExcerptBoundary { next_excerpt, .. } => next_excerpt.is_some(),
         }
     }
@@ -349,6 +328,7 @@ impl Block {
     fn place_below(&self) -> bool {
         match self {
             Block::Custom(block) => matches!(block.placement, BlockPlacement::Below(_)),
+            Block::FoldedBuffer { .. } => false,
             Block::ExcerptBoundary { next_excerpt, .. } => next_excerpt.is_none(),
         }
     }
@@ -356,15 +336,36 @@ impl Block {
     fn is_replacement(&self) -> bool {
         match self {
             Block::Custom(block) => matches!(block.placement, BlockPlacement::Replace(_)),
+            Block::FoldedBuffer { .. } => true,
             Block::ExcerptBoundary { .. } => false,
         }
     }
+
+    fn is_header(&self) -> bool {
+        match self {
+            Block::Custom(_) => false,
+            Block::FoldedBuffer { .. } => true,
+            Block::ExcerptBoundary { .. } => true,
+        }
+    }
 }
 
 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::FoldedBuffer {
+                first_excerpt,
+                prev_excerpt,
+                height,
+                show_excerpt_controls,
+            } => f
+                .debug_struct("FoldedBuffer")
+                .field("first_excerpt", &first_excerpt)
+                .field("prev_excerpt", prev_excerpt)
+                .field("height", height)
+                .field("show_excerpt_controls", show_excerpt_controls)
+                .finish(),
             Self::ExcerptBoundary {
                 starts_new_buffer,
                 next_excerpt,
@@ -372,9 +373,9 @@ impl Debug for Block {
                 ..
             } => f
                 .debug_struct("ExcerptBoundary")
-                .field("prev_excerpt", &prev_excerpt)
-                .field("next_excerpt", &next_excerpt)
-                .field("starts_new_buffer", &starts_new_buffer)
+                .field("prev_excerpt", prev_excerpt)
+                .field("next_excerpt", next_excerpt)
+                .field("starts_new_buffer", starts_new_buffer)
                 .finish(),
         }
     }
@@ -420,6 +421,7 @@ impl BlockMap {
             next_block_id: AtomicUsize::new(0),
             custom_blocks: Vec::new(),
             custom_blocks_by_id: TreeMap::default(),
+            folded_buffers: HashSet::default(),
             transforms: RefCell::new(transforms),
             wrap_snapshot: RefCell::new(wrap_snapshot.clone()),
             show_excerpt_controls,
@@ -495,13 +497,20 @@ impl BlockMap {
             let mut old_start = WrapRow(edit.old.start);
             let mut new_start = WrapRow(edit.new.start);
 
-            // Preserve transforms that:
-            // * strictly precedes this edit
-            // * isomorphic or replace transforms that end *at* the start of the edit
-            // * below blocks that end at the start of the edit
+            // Only preserve transforms that:
+            // * Strictly precedes this edit
+            // * Isomorphic transforms that end *at* the start of the edit
+            // * Below blocks that end at the start of the edit
+            // However, if we hit a replace block that ends at the start of the edit we want to reconstruct it.
             new_transforms.append(cursor.slice(&old_start, Bias::Left, &()), &());
             if let Some(transform) = cursor.item() {
-                if transform.summary.input_rows > 0 && cursor.end(&()) == old_start {
+                if transform.summary.input_rows > 0
+                    && cursor.end(&()) == old_start
+                    && transform
+                        .block
+                        .as_ref()
+                        .map_or(true, |b| !b.is_replacement())
+                {
                     // Preserve the transform (push and next)
                     new_transforms.push(transform.clone(), &());
                     cursor.next(&());
@@ -521,7 +530,6 @@ impl BlockMap {
             // Ensure the edit starts at a transform boundary.
             // If the edit starts within an isomorphic transform, preserve its prefix
             // If the edit lands within a replacement block, expand the edit to include the start of the replaced input range
-            let mut preserved_blocks_above_edit = false;
             let transform = cursor.item().unwrap();
             let transform_rows_before_edit = old_start.0 - cursor.start().0;
             if transform_rows_before_edit > 0 {
@@ -539,9 +547,6 @@ impl BlockMap {
                     debug_assert!(transform.summary.input_rows > 0);
                     old_start.0 -= transform_rows_before_edit;
                     new_start.0 -= transform_rows_before_edit;
-                    // The blocks *above* it are already in the new transforms, so
-                    // we don't need to re-insert them when querying blocks.
-                    preserved_blocks_above_edit = true;
                 }
             }
 
@@ -643,6 +648,7 @@ impl BlockMap {
                     self.buffer_header_height,
                     self.excerpt_header_height,
                     buffer,
+                    &self.folded_buffers,
                     (start_bound, end_bound),
                     wrap_snapshot,
                 ));
@@ -653,12 +659,6 @@ impl BlockMap {
             // For each of these blocks, insert a new isomorphic transform preceding the block,
             // and then insert the block itself.
             for (block_placement, block) in blocks_in_edit.drain(..) {
-                if preserved_blocks_above_edit
-                    && block_placement == BlockPlacement::Above(new_start)
-                {
-                    continue;
-                }
-
                 let mut summary = TransformSummary {
                     input_rows: 0,
                     output_rows: block.height(),
@@ -675,8 +675,8 @@ impl BlockMap {
                         rows_before_block = (position.0 + 1) - new_transforms.summary().input_rows;
                     }
                     BlockPlacement::Replace(range) => {
-                        rows_before_block = range.start.0 - new_transforms.summary().input_rows;
-                        summary.input_rows = range.end.0 - range.start.0 + 1;
+                        rows_before_block = range.start().0 - new_transforms.summary().input_rows;
+                        summary.input_rows = range.end().0 - range.start().0 + 1;
                     }
                 }
 
@@ -719,131 +719,208 @@ impl BlockMap {
         self.show_excerpt_controls
     }
 
-    fn header_and_footer_blocks<'a, 'b: 'a, 'c: 'a + 'b, R, T>(
+    #[allow(clippy::too_many_arguments)]
+    fn header_and_footer_blocks<'a, R, T>(
         show_excerpt_controls: bool,
         excerpt_footer_height: u32,
         buffer_header_height: u32,
         excerpt_header_height: u32,
-        buffer: &'b multi_buffer::MultiBufferSnapshot,
+        buffer: &'a multi_buffer::MultiBufferSnapshot,
+        folded_buffers: &'a HashSet<BufferId>,
         range: R,
-        wrap_snapshot: &'c WrapSnapshot,
-    ) -> impl Iterator<Item = (BlockPlacement<WrapRow>, Block)> + 'b
+        wrap_snapshot: &'a WrapSnapshot,
+    ) -> impl Iterator<Item = (BlockPlacement<WrapRow>, Block)> + 'a
     where
         R: RangeBounds<T>,
         T: multi_buffer::ToOffset,
     {
-        buffer
-            .excerpt_boundaries_in_range(range)
-            .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 mut boundaries = buffer.excerpt_boundaries_in_range(range).peekable();
 
-                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,
-                };
+        std::iter::from_fn(move || {
+            let excerpt_boundary = boundaries.next()?;
+            let wrap_row = if excerpt_boundary.next.is_some() {
+                wrap_snapshot.make_wrap_point(Point::new(excerpt_boundary.row.0, 0), Bias::Left)
+            } else {
+                wrap_snapshot.make_wrap_point(
+                    Point::new(
+                        excerpt_boundary.row.0,
+                        buffer.line_len(excerpt_boundary.row),
+                    ),
+                    Bias::Left,
+                )
+            }
+            .row();
 
-                let mut height = 0;
-                if excerpt_boundary.prev.is_some() {
-                    if show_excerpt_controls {
-                        height += excerpt_footer_height;
+            let new_buffer_id = match (&excerpt_boundary.prev, &excerpt_boundary.next) {
+                (_, None) => None,
+                (None, Some(next)) => Some(next.buffer_id),
+                (Some(prev), Some(next)) => {
+                    if prev.buffer_id != next.buffer_id {
+                        Some(next.buffer_id)
+                    } else {
+                        None
                     }
                 }
-                if excerpt_boundary.next.is_some() {
-                    if starts_new_buffer {
-                        height += buffer_header_height;
-                        if show_excerpt_controls {
-                            height += excerpt_header_height;
+            };
+
+            let prev_excerpt = excerpt_boundary
+                .prev
+                .filter(|prev| !folded_buffers.contains(&prev.buffer_id));
+
+            let mut height = 0;
+            if prev_excerpt.is_some() {
+                if show_excerpt_controls {
+                    height += excerpt_footer_height;
+                }
+            }
+
+            if let Some(new_buffer_id) = new_buffer_id {
+                let first_excerpt = excerpt_boundary.next.clone().unwrap();
+                if folded_buffers.contains(&new_buffer_id) {
+                    let mut buffer_end = Point::new(excerpt_boundary.row.0, 0)
+                        + excerpt_boundary.next.as_ref().unwrap().text_summary.lines;
+
+                    while let Some(next_boundary) = boundaries.peek() {
+                        if let Some(next_excerpt_boundary) = &next_boundary.next {
+                            if next_excerpt_boundary.buffer_id == new_buffer_id {
+                                buffer_end = Point::new(next_boundary.row.0, 0)
+                                    + next_excerpt_boundary.text_summary.lines;
+                            } else {
+                                break;
+                            }
                         }
-                    } else {
-                        height += excerpt_header_height;
+
+                        boundaries.next();
                     }
+
+                    let wrap_end_row = wrap_snapshot.make_wrap_point(buffer_end, Bias::Right).row();
+
+                    return Some((
+                        BlockPlacement::Replace(WrapRow(wrap_row)..=WrapRow(wrap_end_row)),
+                        Block::FoldedBuffer {
+                            prev_excerpt,
+                            height: height + buffer_header_height,
+                            show_excerpt_controls,
+                            first_excerpt,
+                        },
+                    ));
                 }
+            }
 
-                if height == 0 {
-                    return None;
+            if excerpt_boundary.next.is_some() {
+                if new_buffer_id.is_some() {
+                    height += buffer_header_height;
+                    if show_excerpt_controls {
+                        height += excerpt_header_height;
+                    }
+                } else {
+                    height += excerpt_header_height;
                 }
+            }
 
-                Some((
-                    if excerpt_boundary.next.is_some() {
-                        BlockPlacement::Above(WrapRow(wrap_row))
-                    } else {
-                        BlockPlacement::Below(WrapRow(wrap_row))
-                    },
-                    Block::ExcerptBoundary {
-                        prev_excerpt: excerpt_boundary.prev,
-                        next_excerpt: excerpt_boundary.next,
-                        height,
-                        starts_new_buffer,
-                        show_excerpt_controls,
-                    },
-                ))
-            })
+            if height == 0 {
+                return None;
+            }
+
+            Some((
+                if excerpt_boundary.next.is_some() {
+                    BlockPlacement::Above(WrapRow(wrap_row))
+                } else {
+                    BlockPlacement::Below(WrapRow(wrap_row))
+                },
+                Block::ExcerptBoundary {
+                    prev_excerpt,
+                    next_excerpt: excerpt_boundary.next,
+                    height,
+                    starts_new_buffer: new_buffer_id.is_some(),
+                    show_excerpt_controls,
+                },
+            ))
+        })
     }
 
     fn sort_blocks(blocks: &mut Vec<(BlockPlacement<WrapRow>, Block)>) {
         blocks.sort_unstable_by(|(placement_a, block_a), (placement_b, block_b)| {
-            placement_a
-                .cmp(&placement_b)
-                .then_with(|| match (block_a, block_b) {
-                    (
-                        Block::ExcerptBoundary {
-                            next_excerpt: next_excerpt_a,
-                            ..
-                        },
-                        Block::ExcerptBoundary {
-                            next_excerpt: next_excerpt_b,
-                            ..
-                        },
-                    ) => next_excerpt_a
-                        .as_ref()
-                        .map(|excerpt| excerpt.id)
-                        .cmp(&next_excerpt_b.as_ref().map(|excerpt| excerpt.id)),
-                    (Block::ExcerptBoundary { next_excerpt, .. }, Block::Custom(_)) => {
-                        if next_excerpt.is_some() {
+            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 {
-                            Ordering::Greater
-                        }
-                    }
-                    (Block::Custom(_), Block::ExcerptBoundary { next_excerpt, .. }) => {
-                        if next_excerpt.is_some() {
+                        } else if block_b.is_header() {
                             Ordering::Greater
                         } else {
-                            Ordering::Less
+                            Ordering::Equal
                         }
+                    }),
+            };
+            placement_comparison.then_with(|| match (block_a, block_b) {
+                (
+                    Block::ExcerptBoundary {
+                        next_excerpt: next_excerpt_a,
+                        ..
+                    },
+                    Block::ExcerptBoundary {
+                        next_excerpt: next_excerpt_b,
+                        ..
+                    },
+                ) => next_excerpt_a
+                    .as_ref()
+                    .map(|excerpt| excerpt.id)
+                    .cmp(&next_excerpt_b.as_ref().map(|excerpt| excerpt.id)),
+                (Block::ExcerptBoundary { next_excerpt, .. }, Block::Custom(_)) => {
+                    if next_excerpt.is_some() {
+                        Ordering::Less
+                    } else {
+                        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)),
-                })
+                }
+                (Block::Custom(_), Block::ExcerptBoundary { next_excerpt, .. }) => {
+                    if next_excerpt.is_some() {
+                        Ordering::Greater
+                    } else {
+                        Ordering::Less
+                    }
+                }
+                (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, right) {
-            (BlockPlacement::Replace(range), BlockPlacement::Above(row)) => {
-                range.start < *row && range.end >= *row
-            }
-            (BlockPlacement::Replace(range), BlockPlacement::Below(row)) => {
-                range.start <= *row && range.end > *row
-            }
+        blocks.dedup_by(|right, left| match (left.0.clone(), right.0.clone()) {
+            (BlockPlacement::Replace(range), BlockPlacement::Above(row))
+            | (BlockPlacement::Replace(range), BlockPlacement::Below(row)) => range.contains(&row),
             (BlockPlacement::Replace(range_a), BlockPlacement::Replace(range_b)) => {
-                if range_a.end >= range_b.start && range_a.start <= range_b.end {
-                    range_a.end = range_a.end.max(range_b.end);
+                if range_a.end() >= range_b.start() && range_a.start() <= range_b.end() {
+                    left.0 = BlockPlacement::Replace(
+                        *range_a.start()..=*range_a.end().max(range_b.end()),
+                    );
                     true
                 } else {
                     false
@@ -1149,6 +1226,50 @@ impl<'a> BlockMapWriter<'a> {
         self.remove(blocks_to_remove);
     }
 
+    pub fn fold_buffer(
+        &mut self,
+        buffer_id: BufferId,
+        multi_buffer: &MultiBuffer,
+        cx: &AppContext,
+    ) {
+        self.0.folded_buffers.insert(buffer_id);
+        self.recompute_blocks_for_buffer(buffer_id, multi_buffer, cx);
+    }
+
+    pub fn unfold_buffer(
+        &mut self,
+        buffer_id: BufferId,
+        multi_buffer: &MultiBuffer,
+        cx: &AppContext,
+    ) {
+        self.0.folded_buffers.remove(&buffer_id);
+        self.recompute_blocks_for_buffer(buffer_id, multi_buffer, cx);
+    }
+
+    fn recompute_blocks_for_buffer(
+        &mut self,
+        buffer_id: BufferId,
+        multi_buffer: &MultiBuffer,
+        cx: &AppContext,
+    ) {
+        let wrap_snapshot = self.0.wrap_snapshot.borrow().clone();
+
+        let mut edits = Patch::default();
+        for range in multi_buffer.excerpt_ranges_for_buffer(buffer_id, cx) {
+            let last_edit_row = cmp::min(
+                wrap_snapshot.make_wrap_point(range.end, Bias::Right).row() + 1,
+                wrap_snapshot.max_point().row(),
+            ) + 1;
+            let range = wrap_snapshot.make_wrap_point(range.start, Bias::Left).row()..last_edit_row;
+            edits.push(Edit {
+                old: range.clone(),
+                new: range,
+            });
+        }
+
+        self.0.sync(&wrap_snapshot, edits);
+    }
+
     fn blocks_intersecting_buffer_range(
         &self,
         range: Range<usize>,
@@ -1292,42 +1413,44 @@ impl BlockSnapshot {
 
     pub fn block_for_id(&self, block_id: BlockId) -> Option<Block> {
         let buffer = self.wrap_snapshot.buffer_snapshot();
-
-        match block_id {
+        let wrap_point = match block_id {
             BlockId::Custom(custom_block_id) => {
                 let custom_block = self.custom_blocks_by_id.get(&custom_block_id)?;
-                Some(Block::Custom(custom_block.clone()))
+                return Some(Block::Custom(custom_block.clone()));
             }
             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);
+                    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);
+                    self.wrap_snapshot
+                        .make_wrap_point(buffer.max_point(), Bias::Left)
                 }
+            }
+            BlockId::FoldedBuffer(excerpt_id) => self.wrap_snapshot.make_wrap_point(
+                buffer.range_for_excerpt::<Point>(excerpt_id)?.start,
+                Bias::Left,
+            ),
+        };
+        let wrap_row = WrapRow(wrap_point.row());
 
-                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;
-                    }
+        let mut cursor = self.transforms.cursor::<WrapRow>(&());
+        cursor.seek(&wrap_row, Bias::Left, &());
 
-                    cursor.next(&());
+        while let Some(transform) = cursor.item() {
+            if let Some(block) = transform.block.as_ref() {
+                if block.id() == block_id {
+                    return Some(block.clone());
                 }
-
-                None
+            } else if *cursor.start() > wrap_row {
+                break;
             }
+
+            cursor.next(&());
         }
+
+        None
     }
 
     pub fn max_point(&self) -> BlockPoint {
@@ -1421,11 +1544,10 @@ impl BlockSnapshot {
         let mut cursor = self.transforms.cursor::<(WrapRow, BlockRow)>(&());
         cursor.seek(&WrapRow(wrap_point.row()), Bias::Right, &());
         cursor.item().map_or(false, |transform| {
-            if let Some(Block::Custom(block)) = transform.block.as_ref() {
-                matches!(block.placement, BlockPlacement::Replace(_))
-            } else {
-                false
-            }
+            transform
+                .block
+                .as_ref()
+                .map_or(false, |block| block.is_replacement())
         })
     }
 
@@ -1447,13 +1569,13 @@ impl BlockSnapshot {
                 let input_end = Point::new(input_end_row.0, 0);
 
                 match transform.block.as_ref() {
-                    Some(Block::Custom(block))
-                        if matches!(block.placement, BlockPlacement::Replace(_)) =>
-                    {
-                        if ((bias == Bias::Left || search_left) && output_start <= point.0)
-                            || (!search_left && output_start >= point.0)
-                        {
-                            return BlockPoint(output_start);
+                    Some(block) => {
+                        if block.is_replacement() {
+                            if ((bias == Bias::Left || search_left) && output_start <= point.0)
+                                || (!search_left && output_start >= point.0)
+                            {
+                                return BlockPoint(output_start);
+                            }
                         }
                     }
                     None => {
@@ -1472,7 +1594,6 @@ impl BlockSnapshot {
                             return BlockPoint(output_start + input_overshoot);
                         }
                     }
-                    _ => {}
                 }
 
                 if search_left {
@@ -1682,7 +1803,11 @@ impl<'a> Iterator for BlockBufferRows<'a> {
         let transform = self.transforms.item()?;
         if let Some(block) = transform.block.as_ref() {
             if block.is_replacement() && self.transforms.start().0 == self.output_row {
-                Some(self.input_buffer_rows.next().unwrap())
+                if matches!(block, Block::FoldedBuffer { .. }) {
+                    Some(None)
+                } else {
+                    Some(self.input_buffer_rows.next().unwrap())
+                }
             } else {
                 Some(None)
             }
@@ -1806,6 +1931,7 @@ mod tests {
         fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap, wrap_map::WrapMap,
     };
     use gpui::{div, font, px, AppContext, Context as _, Element};
+    use itertools::Itertools;
     use language::{Buffer, Capability};
     use multi_buffer::{ExcerptRange, MultiBuffer};
     use rand::prelude::*;
@@ -2239,16 +2365,16 @@ mod tests {
         let mut block_map = BlockMap::new(wraps_snapshot.clone(), false, 1, 1, 0);
 
         let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
-        writer.insert(vec![BlockProperties {
+        let replace_block_id = writer.insert(vec![BlockProperties {
             style: BlockStyle::Fixed,
             placement: BlockPlacement::Replace(
                 buffer_snapshot.anchor_after(Point::new(1, 3))
-                    ..buffer_snapshot.anchor_before(Point::new(3, 1)),
+                    ..=buffer_snapshot.anchor_before(Point::new(3, 1)),
             ),
             height: 4,
             render: Arc::new(|_| div().into_any()),
             priority: 0,
-        }]);
+        }])[0];
 
         let blocks_snapshot = block_map.read(wraps_snapshot, Default::default());
         assert_eq!(blocks_snapshot.text(), "line1\n\n\n\n\nline5");
@@ -2273,7 +2399,7 @@ mod tests {
             buffer.edit(
                 [(
                     Point::new(1, 5)..Point::new(1, 5),
-                    "\nline 6\nline7\nline 8\nline 9",
+                    "\nline 2.1\nline2.2\nline 2.3\nline 2.4",
                 )],
                 None,
                 cx,
@@ -2292,9 +2418,16 @@ mod tests {
         let blocks_snapshot = block_map.read(wraps_snapshot.clone(), wrap_edits);
         assert_eq!(blocks_snapshot.text(), "line1\n\n\n\n\nline5");
 
-        // Ensure blocks inserted above the start or below the end of the replaced region are shown.
+        // Blocks inserted right above the start or right below the end of the replaced region are hidden.
         let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
         writer.insert(vec![
+            BlockProperties {
+                style: BlockStyle::Fixed,
+                placement: BlockPlacement::Above(buffer_snapshot.anchor_after(Point::new(0, 3))),
+                height: 1,
+                render: Arc::new(|_| div().into_any()),
+                priority: 0,
+            },
             BlockProperties {
                 style: BlockStyle::Fixed,
                 placement: BlockPlacement::Above(buffer_snapshot.anchor_after(Point::new(1, 3))),
@@ -2311,7 +2444,7 @@ mod tests {
             },
         ]);
         let blocks_snapshot = block_map.read(wraps_snapshot.clone(), Default::default());
-        assert_eq!(blocks_snapshot.text(), "line1\n\n\n\n\n\n\nline5");
+        assert_eq!(blocks_snapshot.text(), "\nline1\n\n\n\n\nline5");
 
         // Ensure blocks inserted *inside* replaced region are hidden.
         let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
@@ -2338,8 +2471,470 @@ mod tests {
                 priority: 0,
             },
         ]);
-        let blocks_snapshot = block_map.read(wraps_snapshot, Default::default());
-        assert_eq!(blocks_snapshot.text(), "line1\n\n\n\n\n\n\nline5");
+        let blocks_snapshot = block_map.read(wraps_snapshot.clone(), Default::default());
+        assert_eq!(blocks_snapshot.text(), "\nline1\n\n\n\n\nline5");
+
+        // Removing the replace block shows all the hidden blocks again.
+        let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
+        writer.remove(HashSet::from_iter([replace_block_id]));
+        let blocks_snapshot = block_map.read(wraps_snapshot.clone(), Default::default());
+        assert_eq!(
+            blocks_snapshot.text(),
+            "\nline1\n\nline2\n\n\nline 2.1\nline2.2\nline 2.3\nline 2.4\n\nline4\n\nline5"
+        );
+    }
+
+    #[gpui::test]
+    fn test_custom_blocks_inside_buffer_folds(cx: &mut gpui::TestAppContext) {
+        cx.update(init_test);
+
+        let text = "111\n222\n333\n444\n555\n666";
+
+        let buffer = cx.update(|cx| {
+            MultiBuffer::build_multi(
+                [
+                    (text, vec![Point::new(0, 0)..Point::new(0, 3)]),
+                    (
+                        text,
+                        vec![
+                            Point::new(1, 0)..Point::new(1, 3),
+                            Point::new(2, 0)..Point::new(2, 3),
+                            Point::new(3, 0)..Point::new(3, 3),
+                        ],
+                    ),
+                    (
+                        text,
+                        vec![
+                            Point::new(4, 0)..Point::new(4, 3),
+                            Point::new(5, 0)..Point::new(5, 3),
+                        ],
+                    ),
+                ],
+                cx,
+            )
+        });
+        let buffer_snapshot = cx.update(|cx| buffer.read(cx).snapshot(cx));
+        let buffer_ids = buffer_snapshot
+            .excerpts()
+            .map(|(_, buffer_snapshot, _)| buffer_snapshot.remote_id())
+            .dedup()
+            .collect::<Vec<_>>();
+        assert_eq!(buffer_ids.len(), 3);
+        let buffer_id_1 = buffer_ids[0];
+        let buffer_id_2 = buffer_ids[1];
+        let buffer_id_3 = buffer_ids[2];
+
+        let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+        let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
+        let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
+        let (_, wrap_snapshot) =
+            cx.update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), None, cx));
+        let mut block_map = BlockMap::new(wrap_snapshot.clone(), true, 2, 1, 1);
+        let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default());
+
+        assert_eq!(
+            blocks_snapshot.text(),
+            "\n\n\n111\n\n\n\n\n222\n\n\n333\n\n\n444\n\n\n\n\n555\n\n\n666\n"
+        );
+        assert_eq!(
+            blocks_snapshot.buffer_rows(BlockRow(0)).collect::<Vec<_>>(),
+            vec![
+                None,
+                None,
+                None,
+                Some(0),
+                None,
+                None,
+                None,
+                None,
+                Some(1),
+                None,
+                None,
+                Some(2),
+                None,
+                None,
+                Some(3),
+                None,
+                None,
+                None,
+                None,
+                Some(4),
+                None,
+                None,
+                Some(5),
+                None,
+            ]
+        );
+
+        let mut writer = block_map.write(wrap_snapshot.clone(), Patch::default());
+        let excerpt_blocks_2 = writer.insert(vec![
+            BlockProperties {
+                style: BlockStyle::Fixed,
+                placement: BlockPlacement::Above(buffer_snapshot.anchor_after(Point::new(1, 0))),
+                height: 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,
+                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,
+                render: Arc::new(|_| div().into_any()),
+                priority: 0,
+            },
+        ]);
+        let excerpt_blocks_3 = writer.insert(vec![
+            BlockProperties {
+                style: BlockStyle::Fixed,
+                placement: BlockPlacement::Above(buffer_snapshot.anchor_after(Point::new(4, 0))),
+                height: 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,
+                render: Arc::new(|_| div().into_any()),
+                priority: 0,
+            },
+        ]);
+
+        let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default());
+        assert_eq!(
+            blocks_snapshot.text(),
+            "\n\n\n111\n\n\n\n\n\n222\n\n\n\n333\n\n\n444\n\n\n\n\n\n\n555\n\n\n666\n\n"
+        );
+        assert_eq!(
+            blocks_snapshot.buffer_rows(BlockRow(0)).collect::<Vec<_>>(),
+            vec![
+                None,
+                None,
+                None,
+                Some(0),
+                None,
+                None,
+                None,
+                None,
+                None,
+                Some(1),
+                None,
+                None,
+                None,
+                Some(2),
+                None,
+                None,
+                Some(3),
+                None,
+                None,
+                None,
+                None,
+                None,
+                None,
+                Some(4),
+                None,
+                None,
+                Some(5),
+                None,
+                None,
+            ]
+        );
+
+        let mut writer = block_map.write(wrap_snapshot.clone(), Patch::default());
+        buffer.read_with(cx, |buffer, cx| {
+            writer.fold_buffer(buffer_id_1, buffer, cx);
+        });
+        let excerpt_blocks_1 = writer.insert(vec![BlockProperties {
+            style: BlockStyle::Fixed,
+            placement: BlockPlacement::Above(buffer_snapshot.anchor_after(Point::new(0, 0))),
+            height: 1,
+            render: Arc::new(|_| div().into_any()),
+            priority: 0,
+        }]);
+        let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default());
+        let blocks = blocks_snapshot
+            .blocks_in_range(0..u32::MAX)
+            .collect::<Vec<_>>();
+        for (_, block) in &blocks {
+            if let BlockId::Custom(custom_block_id) = block.id() {
+                assert!(
+                    !excerpt_blocks_1.contains(&custom_block_id),
+                    "Should have no blocks from the folded buffer"
+                );
+                assert!(
+                    excerpt_blocks_2.contains(&custom_block_id)
+                        || excerpt_blocks_3.contains(&custom_block_id),
+                    "Should have only blocks from unfolded buffers"
+                );
+            }
+        }
+        assert_eq!(
+            1,
+            blocks
+                .iter()
+                .filter(|(_, block)| matches!(block, Block::FoldedBuffer { .. }))
+                .count(),
+            "Should have one folded block, prodicing a header of the second buffer"
+        );
+        assert_eq!(
+            blocks_snapshot.text(),
+            "\n\n\n\n\n\n222\n\n\n\n333\n\n\n444\n\n\n\n\n\n\n555\n\n\n666\n\n"
+        );
+        assert_eq!(
+            blocks_snapshot.buffer_rows(BlockRow(0)).collect::<Vec<_>>(),
+            vec![
+                None,
+                None,
+                None,
+                None,
+                None,
+                None,
+                Some(1),
+                None,
+                None,
+                None,
+                Some(2),
+                None,
+                None,
+                Some(3),
+                None,
+                None,
+                None,
+                None,
+                None,
+                None,
+                Some(4),
+                None,
+                None,
+                Some(5),
+                None,
+                None,
+            ]
+        );
+
+        let mut writer = block_map.write(wrap_snapshot.clone(), Patch::default());
+        buffer.read_with(cx, |buffer, cx| {
+            writer.fold_buffer(buffer_id_2, buffer, cx);
+        });
+        let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default());
+        let blocks = blocks_snapshot
+            .blocks_in_range(0..u32::MAX)
+            .collect::<Vec<_>>();
+        for (_, block) in &blocks {
+            if let BlockId::Custom(custom_block_id) = block.id() {
+                assert!(
+                    !excerpt_blocks_1.contains(&custom_block_id),
+                    "Should have no blocks from the folded buffer_1"
+                );
+                assert!(
+                    !excerpt_blocks_2.contains(&custom_block_id),
+                    "Should have no blocks from the folded buffer_2"
+                );
+                assert!(
+                    excerpt_blocks_3.contains(&custom_block_id),
+                    "Should have only blocks from unfolded buffers"
+                );
+            }
+        }
+        assert_eq!(
+            2,
+            blocks
+                .iter()
+                .filter(|(_, block)| matches!(block, Block::FoldedBuffer { .. }))
+                .count(),
+            "Should have two folded blocks, producing headers"
+        );
+        assert_eq!(blocks_snapshot.text(), "\n\n\n\n\n\n\n\n555\n\n\n666\n\n");
+        assert_eq!(
+            blocks_snapshot.buffer_rows(BlockRow(0)).collect::<Vec<_>>(),
+            vec![
+                None,
+                None,
+                None,
+                None,
+                None,
+                None,
+                None,
+                None,
+                Some(4),
+                None,
+                None,
+                Some(5),
+                None,
+                None,
+            ]
+        );
+
+        let mut writer = block_map.write(wrap_snapshot.clone(), Patch::default());
+        buffer.read_with(cx, |buffer, cx| {
+            writer.unfold_buffer(buffer_id_1, buffer, cx);
+        });
+        let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default());
+        let blocks = blocks_snapshot
+            .blocks_in_range(0..u32::MAX)
+            .collect::<Vec<_>>();
+        for (_, block) in &blocks {
+            if let BlockId::Custom(custom_block_id) = block.id() {
+                assert!(
+                    !excerpt_blocks_2.contains(&custom_block_id),
+                    "Should have no blocks from the folded buffer_2"
+                );
+                assert!(
+                    excerpt_blocks_1.contains(&custom_block_id)
+                        || excerpt_blocks_3.contains(&custom_block_id),
+                    "Should have only blocks from unfolded buffers"
+                );
+            }
+        }
+        assert_eq!(
+            1,
+            blocks
+                .iter()
+                .filter(|(_, block)| matches!(block, Block::FoldedBuffer { .. }))
+                .count(),
+            "Should be back to a single folded buffer, producing a header for buffer_2"
+        );
+        assert_eq!(
+            blocks_snapshot.text(),
+            "\n\n\n\n111\n\n\n\n\n\n\n\n555\n\n\n666\n\n",
+            "Should have extra newline for 111 buffer, due to a new block added when it was folded"
+        );
+        assert_eq!(
+            blocks_snapshot.buffer_rows(BlockRow(0)).collect::<Vec<_>>(),
+            vec![
+                None,
+                None,
+                None,
+                None,
+                Some(0),
+                None,
+                None,
+                None,
+                None,
+                None,
+                None,
+                None,
+                Some(4),
+                None,
+                None,
+                Some(5),
+                None,
+                None,
+            ]
+        );
+
+        let mut writer = block_map.write(wrap_snapshot.clone(), Patch::default());
+        buffer.read_with(cx, |buffer, cx| {
+            writer.fold_buffer(buffer_id_3, buffer, cx);
+        });
+        let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default());
+        let blocks = blocks_snapshot
+            .blocks_in_range(0..u32::MAX)
+            .collect::<Vec<_>>();
+        for (_, block) in &blocks {
+            if let BlockId::Custom(custom_block_id) = block.id() {
+                assert!(
+                    excerpt_blocks_1.contains(&custom_block_id),
+                    "Should have no blocks from the folded buffer_1"
+                );
+                assert!(
+                    !excerpt_blocks_2.contains(&custom_block_id),
+                    "Should have only blocks from unfolded buffers"
+                );
+                assert!(
+                    !excerpt_blocks_3.contains(&custom_block_id),
+                    "Should have only blocks from unfolded buffers"
+                );
+            }
+        }
+
+        assert_eq!(
+            blocks_snapshot.text(),
+            "\n\n\n\n111\n\n\n\n\n",
+            "Should have a single, first buffer left after folding"
+        );
+        assert_eq!(
+            blocks_snapshot.buffer_rows(BlockRow(0)).collect::<Vec<_>>(),
+            vec![
+                None,
+                None,
+                None,
+                None,
+                Some(0),
+                None,
+                None,
+                None,
+                None,
+                None,
+            ]
+        );
+    }
+
+    #[gpui::test]
+    fn test_basic_buffer_fold(cx: &mut gpui::TestAppContext) {
+        cx.update(init_test);
+
+        let text = "111";
+
+        let buffer = cx.update(|cx| {
+            MultiBuffer::build_multi([(text, vec![Point::new(0, 0)..Point::new(0, 3)])], cx)
+        });
+        let buffer_snapshot = cx.update(|cx| buffer.read(cx).snapshot(cx));
+        let buffer_ids = buffer_snapshot
+            .excerpts()
+            .map(|(_, buffer_snapshot, _)| buffer_snapshot.remote_id())
+            .dedup()
+            .collect::<Vec<_>>();
+        assert_eq!(buffer_ids.len(), 1);
+        let buffer_id = buffer_ids[0];
+
+        let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+        let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
+        let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
+        let (_, wrap_snapshot) =
+            cx.update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), None, cx));
+        let mut block_map = BlockMap::new(wrap_snapshot.clone(), true, 2, 1, 1);
+        let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default());
+
+        assert_eq!(blocks_snapshot.text(), "\n\n\n111\n");
+
+        let mut writer = block_map.write(wrap_snapshot.clone(), Patch::default());
+        buffer.read_with(cx, |buffer, cx| {
+            writer.fold_buffer(buffer_id, buffer, cx);
+        });
+        let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default());
+        let blocks = blocks_snapshot
+            .blocks_in_range(0..u32::MAX)
+            .collect::<Vec<_>>();
+        assert_eq!(
+            1,
+            blocks
+                .iter()
+                .filter(|(_, block)| {
+                    match block {
+                        Block::FoldedBuffer { prev_excerpt, .. } => {
+                            assert!(prev_excerpt.is_none());
+                            true
+                        }
+                        _ => false,
+                    }
+                })
+                .count(),
+            "Should have one folded block, prodicing a header of the second buffer"
+        );
+        assert_eq!(blocks_snapshot.text(), "\n");
+        assert_eq!(
+            blocks_snapshot.buffer_rows(BlockRow(0)).collect::<Vec<_>>(),
+            vec![None, None],
+            "When fully folded, should be no buffer rows"
+        );
     }
 
     #[gpui::test(iterations = 100)]

crates/editor/src/editor.rs 🔗

@@ -678,6 +678,7 @@ pub struct Editor {
     next_scroll_position: NextScrollCursorCenterTopBottom,
     addons: HashMap<TypeId, Box<dyn Addon>>,
     registered_buffers: HashMap<BufferId, OpenLspBufferHandle>,
+    toggle_fold_multiple_buffers: Task<()>,
     _scroll_cursor_center_top_bottom_task: Task<()>,
 }
 
@@ -1325,6 +1326,7 @@ impl Editor {
             addons: HashMap::default(),
             registered_buffers: HashMap::default(),
             _scroll_cursor_center_top_bottom_task: Task::ready(()),
+            toggle_fold_multiple_buffers: Task::ready(()),
             text_style_refinement: None,
         };
         this.tasks_update_task = Some(this.refresh_runnables(cx));
@@ -10311,22 +10313,53 @@ impl Editor {
     }
 
     pub fn toggle_fold(&mut self, _: &actions::ToggleFold, cx: &mut ViewContext<Self>) {
-        let selection = self.selections.newest::<Point>(cx);
+        if self.is_singleton(cx) {
+            let selection = self.selections.newest::<Point>(cx);
 
-        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
-        let range = if selection.is_empty() {
-            let point = selection.head().to_display_point(&display_map);
-            let start = DisplayPoint::new(point.row(), 0).to_point(&display_map);
-            let end = DisplayPoint::new(point.row(), display_map.line_len(point.row()))
-                .to_point(&display_map);
-            start..end
-        } else {
-            selection.range()
-        };
-        if display_map.folds_in_range(range).next().is_some() {
-            self.unfold_lines(&Default::default(), cx)
+            let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+            let range = if selection.is_empty() {
+                let point = selection.head().to_display_point(&display_map);
+                let start = DisplayPoint::new(point.row(), 0).to_point(&display_map);
+                let end = DisplayPoint::new(point.row(), display_map.line_len(point.row()))
+                    .to_point(&display_map);
+                start..end
+            } else {
+                selection.range()
+            };
+            if display_map.folds_in_range(range).next().is_some() {
+                self.unfold_lines(&Default::default(), cx)
+            } else {
+                self.fold(&Default::default(), cx)
+            }
         } else {
-            self.fold(&Default::default(), cx)
+            let (display_snapshot, selections) = self.selections.all_adjusted_display(cx);
+            let mut toggled_buffers = HashSet::default();
+            for selection in selections {
+                if let Some(buffer_id) = display_snapshot
+                    .display_point_to_anchor(selection.head(), Bias::Right)
+                    .buffer_id
+                {
+                    if toggled_buffers.insert(buffer_id) {
+                        if self.buffer_folded(buffer_id, cx) {
+                            self.unfold_buffer(buffer_id, cx);
+                        } else {
+                            self.fold_buffer(buffer_id, cx);
+                        }
+                    }
+                }
+                if let Some(buffer_id) = display_snapshot
+                    .display_point_to_anchor(selection.tail(), Bias::Left)
+                    .buffer_id
+                {
+                    if toggled_buffers.insert(buffer_id) {
+                        if self.buffer_folded(buffer_id, cx) {
+                            self.unfold_buffer(buffer_id, cx);
+                        } else {
+                            self.fold_buffer(buffer_id, cx);
+                        }
+                    }
+                }
+            }
         }
     }
 
@@ -10355,44 +10388,68 @@ impl Editor {
     }
 
     pub fn fold(&mut self, _: &actions::Fold, cx: &mut ViewContext<Self>) {
-        let mut to_fold = Vec::new();
-        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
-        let selections = self.selections.all_adjusted(cx);
+        if self.is_singleton(cx) {
+            let mut to_fold = Vec::new();
+            let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+            let selections = self.selections.all_adjusted(cx);
 
-        for selection in selections {
-            let range = selection.range().sorted();
-            let buffer_start_row = range.start.row;
+            for selection in selections {
+                let range = selection.range().sorted();
+                let buffer_start_row = range.start.row;
+
+                if range.start.row != range.end.row {
+                    let mut found = false;
+                    let mut row = range.start.row;
+                    while row <= range.end.row {
+                        if let Some(crease) = display_map.crease_for_buffer_row(MultiBufferRow(row))
+                        {
+                            found = true;
+                            row = crease.range().end.row + 1;
+                            to_fold.push(crease);
+                        } else {
+                            row += 1
+                        }
+                    }
+                    if found {
+                        continue;
+                    }
+                }
 
-            if range.start.row != range.end.row {
-                let mut found = false;
-                let mut row = range.start.row;
-                while row <= range.end.row {
+                for row in (0..=range.start.row).rev() {
                     if let Some(crease) = display_map.crease_for_buffer_row(MultiBufferRow(row)) {
-                        found = true;
-                        row = crease.range().end.row + 1;
-                        to_fold.push(crease);
-                    } else {
-                        row += 1
+                        if crease.range().end.row >= buffer_start_row {
+                            to_fold.push(crease);
+                            if row <= range.start.row {
+                                break;
+                            }
+                        }
                     }
                 }
-                if found {
-                    continue;
-                }
             }
 
-            for row in (0..=range.start.row).rev() {
-                if let Some(crease) = display_map.crease_for_buffer_row(MultiBufferRow(row)) {
-                    if crease.range().end.row >= buffer_start_row {
-                        to_fold.push(crease);
-                        if row <= range.start.row {
-                            break;
-                        }
+            self.fold_creases(to_fold, true, cx);
+        } else {
+            let (display_snapshot, selections) = self.selections.all_adjusted_display(cx);
+            let mut folded_buffers = HashSet::default();
+            for selection in selections {
+                if let Some(buffer_id) = display_snapshot
+                    .display_point_to_anchor(selection.head(), Bias::Right)
+                    .buffer_id
+                {
+                    if folded_buffers.insert(buffer_id) {
+                        self.fold_buffer(buffer_id, cx);
+                    }
+                }
+                if let Some(buffer_id) = display_snapshot
+                    .display_point_to_anchor(selection.tail(), Bias::Left)
+                    .buffer_id
+                {
+                    if folded_buffers.insert(buffer_id) {
+                        self.fold_buffer(buffer_id, cx);
                     }
                 }
             }
         }
-
-        self.fold_creases(to_fold, true, cx);
     }
 
     fn fold_at_level(&mut self, fold_at: &FoldAtLevel, cx: &mut ViewContext<Self>) {
@@ -10432,22 +10489,30 @@ impl Editor {
     }
 
     pub fn fold_all(&mut self, _: &actions::FoldAll, cx: &mut ViewContext<Self>) {
-        if !self.buffer.read(cx).is_singleton() {
-            return;
-        }
-
-        let mut fold_ranges = Vec::new();
-        let snapshot = self.buffer.read(cx).snapshot(cx);
+        if self.buffer.read(cx).is_singleton() {
+            let mut fold_ranges = Vec::new();
+            let snapshot = self.buffer.read(cx).snapshot(cx);
 
-        for row in 0..snapshot.max_row().0 {
-            if let Some(foldable_range) =
-                self.snapshot(cx).crease_for_buffer_row(MultiBufferRow(row))
-            {
-                fold_ranges.push(foldable_range);
+            for row in 0..snapshot.max_row().0 {
+                if let Some(foldable_range) =
+                    self.snapshot(cx).crease_for_buffer_row(MultiBufferRow(row))
+                {
+                    fold_ranges.push(foldable_range);
+                }
             }
-        }
 
-        self.fold_creases(fold_ranges, true, cx);
+            self.fold_creases(fold_ranges, true, cx);
+        } else {
+            self.toggle_fold_multiple_buffers = cx.spawn(|editor, mut cx| async move {
+                editor
+                    .update(&mut cx, |editor, cx| {
+                        for buffer_id in editor.buffer.read(cx).excerpt_buffer_ids() {
+                            editor.fold_buffer(buffer_id, cx);
+                        }
+                    })
+                    .ok();
+            });
+        }
     }
 
     pub fn fold_function_bodies(
@@ -10519,22 +10584,45 @@ impl Editor {
     }
 
     pub fn unfold_lines(&mut self, _: &UnfoldLines, cx: &mut ViewContext<Self>) {
-        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
-        let buffer = &display_map.buffer_snapshot;
-        let selections = self.selections.all::<Point>(cx);
-        let ranges = selections
-            .iter()
-            .map(|s| {
-                let range = s.display_range(&display_map).sorted();
-                let mut start = range.start.to_point(&display_map);
-                let mut end = range.end.to_point(&display_map);
-                start.column = 0;
-                end.column = buffer.line_len(MultiBufferRow(end.row));
-                start..end
-            })
-            .collect::<Vec<_>>();
+        if self.is_singleton(cx) {
+            let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+            let buffer = &display_map.buffer_snapshot;
+            let selections = self.selections.all::<Point>(cx);
+            let ranges = selections
+                .iter()
+                .map(|s| {
+                    let range = s.display_range(&display_map).sorted();
+                    let mut start = range.start.to_point(&display_map);
+                    let mut end = range.end.to_point(&display_map);
+                    start.column = 0;
+                    end.column = buffer.line_len(MultiBufferRow(end.row));
+                    start..end
+                })
+                .collect::<Vec<_>>();
 
-        self.unfold_ranges(&ranges, true, true, cx);
+            self.unfold_ranges(&ranges, true, true, cx);
+        } else {
+            let (display_snapshot, selections) = self.selections.all_adjusted_display(cx);
+            let mut unfolded_buffers = HashSet::default();
+            for selection in selections {
+                if let Some(buffer_id) = display_snapshot
+                    .display_point_to_anchor(selection.head(), Bias::Right)
+                    .buffer_id
+                {
+                    if unfolded_buffers.insert(buffer_id) {
+                        self.unfold_buffer(buffer_id, cx);
+                    }
+                }
+                if let Some(buffer_id) = display_snapshot
+                    .display_point_to_anchor(selection.tail(), Bias::Left)
+                    .buffer_id
+                {
+                    if unfolded_buffers.insert(buffer_id) {
+                        self.unfold_buffer(buffer_id, cx);
+                    }
+                }
+            }
+        }
     }
 
     pub fn unfold_recursive(&mut self, _: &UnfoldRecursive, cx: &mut ViewContext<Self>) {
@@ -10574,8 +10662,20 @@ impl Editor {
     }
 
     pub fn unfold_all(&mut self, _: &actions::UnfoldAll, cx: &mut ViewContext<Self>) {
-        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
-        self.unfold_ranges(&[0..display_map.buffer_snapshot.len()], true, true, cx);
+        if self.buffer.read(cx).is_singleton() {
+            let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+            self.unfold_ranges(&[0..display_map.buffer_snapshot.len()], true, true, cx);
+        } else {
+            self.toggle_fold_multiple_buffers = cx.spawn(|editor, mut cx| async move {
+                editor
+                    .update(&mut cx, |editor, cx| {
+                        for buffer_id in editor.buffer.read(cx).excerpt_buffer_ids() {
+                            editor.unfold_buffer(buffer_id, cx);
+                        }
+                    })
+                    .ok();
+            });
+        }
     }
 
     pub fn fold_selected_ranges(&mut self, _: &FoldSelectedRanges, cx: &mut ViewContext<Self>) {
@@ -10662,6 +10762,45 @@ impl Editor {
         });
     }
 
+    pub fn fold_buffer(&mut self, buffer_id: BufferId, cx: &mut ViewContext<Self>) {
+        if self.buffer().read(cx).is_singleton() || self.buffer_folded(buffer_id, cx) {
+            return;
+        }
+        let Some(buffer) = self.buffer().read(cx).buffer(buffer_id) else {
+            return;
+        };
+        let folded_excerpts = self.buffer().read(cx).excerpts_for_buffer(&buffer, cx);
+        self.display_map
+            .update(cx, |display_map, cx| display_map.fold_buffer(buffer_id, cx));
+        cx.emit(EditorEvent::BufferFoldToggled {
+            ids: folded_excerpts.iter().map(|&(id, _)| id).collect(),
+            folded: true,
+        });
+        cx.notify();
+    }
+
+    pub fn unfold_buffer(&mut self, buffer_id: BufferId, cx: &mut ViewContext<Self>) {
+        if self.buffer().read(cx).is_singleton() || !self.buffer_folded(buffer_id, cx) {
+            return;
+        }
+        let Some(buffer) = self.buffer().read(cx).buffer(buffer_id) else {
+            return;
+        };
+        let unfolded_excerpts = self.buffer().read(cx).excerpts_for_buffer(&buffer, cx);
+        self.display_map.update(cx, |display_map, cx| {
+            display_map.unfold_buffer(buffer_id, cx);
+        });
+        cx.emit(EditorEvent::BufferFoldToggled {
+            ids: unfolded_excerpts.iter().map(|&(id, _)| id).collect(),
+            folded: false,
+        });
+        cx.notify();
+    }
+
+    pub fn buffer_folded(&self, buffer: BufferId, cx: &AppContext) -> bool {
+        self.display_map.read(cx).buffer_folded(buffer)
+    }
+
     /// Removes any folds with the given ranges.
     pub fn remove_folds_with_type<T: ToOffset + Clone>(
         &mut self,
@@ -13820,6 +13959,10 @@ pub enum EditorEvent {
     ExcerptsRemoved {
         ids: Vec<ExcerptId>,
     },
+    BufferFoldToggled {
+        ids: Vec<ExcerptId>,
+        folded: bool,
+    },
     ExcerptsEdited {
         ids: Vec<ExcerptId>,
     },

crates/editor/src/editor_tests.rs 🔗

@@ -4064,7 +4064,7 @@ async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
         let snapshot = editor.snapshot(cx);
         let snapshot = &snapshot.buffer_snapshot;
         let placement = BlockPlacement::Replace(
-            snapshot.anchor_after(Point::new(1, 0))..snapshot.anchor_after(Point::new(3, 0)),
+            snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
         );
         editor.insert_blocks(
             [BlockProperties {
@@ -13905,6 +13905,412 @@ async fn test_find_enclosing_node_with_task(cx: &mut gpui::TestAppContext) {
     });
 }
 
+#[gpui::test]
+async fn test_multi_buffer_folding(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
+    let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
+    let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
+
+    let fs = FakeFs::new(cx.executor());
+    fs.insert_tree(
+        "/a",
+        json!({
+            "first.rs": sample_text_1,
+            "second.rs": sample_text_2,
+            "third.rs": sample_text_3,
+        }),
+    )
+    .await;
+    let project = Project::test(fs, ["/a".as_ref()], cx).await;
+    let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+    let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
+    let worktree = project.update(cx, |project, cx| {
+        let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
+        assert_eq!(worktrees.len(), 1);
+        worktrees.pop().unwrap()
+    });
+    let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
+
+    let buffer_1 = project
+        .update(cx, |project, cx| {
+            project.open_buffer((worktree_id, "first.rs"), cx)
+        })
+        .await
+        .unwrap();
+    let buffer_2 = project
+        .update(cx, |project, cx| {
+            project.open_buffer((worktree_id, "second.rs"), cx)
+        })
+        .await
+        .unwrap();
+    let buffer_3 = project
+        .update(cx, |project, cx| {
+            project.open_buffer((worktree_id, "third.rs"), cx)
+        })
+        .await
+        .unwrap();
+
+    let multi_buffer = cx.new_model(|cx| {
+        let mut multi_buffer = MultiBuffer::new(ReadWrite);
+        multi_buffer.push_excerpts(
+            buffer_1.clone(),
+            [
+                ExcerptRange {
+                    context: Point::new(0, 0)..Point::new(3, 0),
+                    primary: None,
+                },
+                ExcerptRange {
+                    context: Point::new(5, 0)..Point::new(7, 0),
+                    primary: None,
+                },
+                ExcerptRange {
+                    context: Point::new(9, 0)..Point::new(10, 4),
+                    primary: None,
+                },
+            ],
+            cx,
+        );
+        multi_buffer.push_excerpts(
+            buffer_2.clone(),
+            [
+                ExcerptRange {
+                    context: Point::new(0, 0)..Point::new(3, 0),
+                    primary: None,
+                },
+                ExcerptRange {
+                    context: Point::new(5, 0)..Point::new(7, 0),
+                    primary: None,
+                },
+                ExcerptRange {
+                    context: Point::new(9, 0)..Point::new(10, 4),
+                    primary: None,
+                },
+            ],
+            cx,
+        );
+        multi_buffer.push_excerpts(
+            buffer_3.clone(),
+            [
+                ExcerptRange {
+                    context: Point::new(0, 0)..Point::new(3, 0),
+                    primary: None,
+                },
+                ExcerptRange {
+                    context: Point::new(5, 0)..Point::new(7, 0),
+                    primary: None,
+                },
+                ExcerptRange {
+                    context: Point::new(9, 0)..Point::new(10, 4),
+                    primary: None,
+                },
+            ],
+            cx,
+        );
+        multi_buffer
+    });
+    let multi_buffer_editor = cx.new_view(|cx| {
+        Editor::new(
+            EditorMode::Full,
+            multi_buffer,
+            Some(project.clone()),
+            true,
+            cx,
+        )
+    });
+
+    let full_text = "\n\n\naaaa\nbbbb\ncccc\n\n\n\nffff\ngggg\n\n\n\njjjj\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n\n1111\n2222\n\n\n\n5555\n";
+    assert_eq!(
+        multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
+        full_text,
+    );
+
+    multi_buffer_editor.update(cx, |editor, cx| {
+        editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
+    });
+    assert_eq!(
+        multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
+        "\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n\n1111\n2222\n\n\n\n5555\n",
+        "After folding the first buffer, its text should not be displayed"
+    );
+
+    multi_buffer_editor.update(cx, |editor, cx| {
+        editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
+    });
+    assert_eq!(
+        multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
+        "\n\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n\n1111\n2222\n\n\n\n5555\n",
+        "After folding the second buffer, its text should not be displayed"
+    );
+
+    multi_buffer_editor.update(cx, |editor, cx| {
+        editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
+    });
+    assert_eq!(
+        multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
+        "\n\n\n\n\n",
+        "After folding the third buffer, its text should not be displayed"
+    );
+
+    // Emulate selection inside the fold logic, that should work
+    multi_buffer_editor.update(cx, |editor, cx| {
+        editor.snapshot(cx).next_line_boundary(Point::new(0, 4));
+    });
+
+    multi_buffer_editor.update(cx, |editor, cx| {
+        editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
+    });
+    assert_eq!(
+        multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
+        "\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n",
+        "After unfolding the second buffer, its text should be displayed"
+    );
+
+    multi_buffer_editor.update(cx, |editor, cx| {
+        editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
+    });
+    assert_eq!(
+        multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
+        "\n\n\naaaa\nbbbb\ncccc\n\n\n\nffff\ngggg\n\n\n\njjjj\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n",
+        "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
+    );
+
+    multi_buffer_editor.update(cx, |editor, cx| {
+        editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
+    });
+    assert_eq!(
+        multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
+        full_text,
+        "After unfolding the all buffers, all original text should be displayed"
+    );
+}
+
+#[gpui::test]
+async fn test_multi_buffer_single_excerpts_folding(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let sample_text_1 = "1111\n2222\n3333".to_string();
+    let sample_text_2 = "4444\n5555\n6666".to_string();
+    let sample_text_3 = "7777\n8888\n9999".to_string();
+
+    let fs = FakeFs::new(cx.executor());
+    fs.insert_tree(
+        "/a",
+        json!({
+            "first.rs": sample_text_1,
+            "second.rs": sample_text_2,
+            "third.rs": sample_text_3,
+        }),
+    )
+    .await;
+    let project = Project::test(fs, ["/a".as_ref()], cx).await;
+    let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+    let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
+    let worktree = project.update(cx, |project, cx| {
+        let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
+        assert_eq!(worktrees.len(), 1);
+        worktrees.pop().unwrap()
+    });
+    let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
+
+    let buffer_1 = project
+        .update(cx, |project, cx| {
+            project.open_buffer((worktree_id, "first.rs"), cx)
+        })
+        .await
+        .unwrap();
+    let buffer_2 = project
+        .update(cx, |project, cx| {
+            project.open_buffer((worktree_id, "second.rs"), cx)
+        })
+        .await
+        .unwrap();
+    let buffer_3 = project
+        .update(cx, |project, cx| {
+            project.open_buffer((worktree_id, "third.rs"), cx)
+        })
+        .await
+        .unwrap();
+
+    let multi_buffer = cx.new_model(|cx| {
+        let mut multi_buffer = MultiBuffer::new(ReadWrite);
+        multi_buffer.push_excerpts(
+            buffer_1.clone(),
+            [ExcerptRange {
+                context: Point::new(0, 0)..Point::new(3, 0),
+                primary: None,
+            }],
+            cx,
+        );
+        multi_buffer.push_excerpts(
+            buffer_2.clone(),
+            [ExcerptRange {
+                context: Point::new(0, 0)..Point::new(3, 0),
+                primary: None,
+            }],
+            cx,
+        );
+        multi_buffer.push_excerpts(
+            buffer_3.clone(),
+            [ExcerptRange {
+                context: Point::new(0, 0)..Point::new(3, 0),
+                primary: None,
+            }],
+            cx,
+        );
+        multi_buffer
+    });
+
+    let multi_buffer_editor = cx.new_view(|cx| {
+        Editor::new(
+            EditorMode::Full,
+            multi_buffer,
+            Some(project.clone()),
+            true,
+            cx,
+        )
+    });
+
+    let full_text = "\n\n\n1111\n2222\n3333\n\n\n\n\n4444\n5555\n6666\n\n\n\n\n7777\n8888\n9999\n";
+    assert_eq!(
+        multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
+        full_text,
+    );
+
+    multi_buffer_editor.update(cx, |editor, cx| {
+        editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
+    });
+    assert_eq!(
+        multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
+        "\n\n\n\n\n4444\n5555\n6666\n\n\n\n\n7777\n8888\n9999\n",
+        "After folding the first buffer, its text should not be displayed"
+    );
+
+    multi_buffer_editor.update(cx, |editor, cx| {
+        editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
+    });
+
+    assert_eq!(
+        multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
+        "\n\n\n\n\n\n\n7777\n8888\n9999\n",
+        "After folding the second buffer, its text should not be displayed"
+    );
+
+    multi_buffer_editor.update(cx, |editor, cx| {
+        editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
+    });
+    assert_eq!(
+        multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
+        "\n\n\n\n\n",
+        "After folding the third buffer, its text should not be displayed"
+    );
+
+    multi_buffer_editor.update(cx, |editor, cx| {
+        editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
+    });
+    assert_eq!(
+        multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
+        "\n\n\n\n\n4444\n5555\n6666\n\n\n",
+        "After unfolding the second buffer, its text should be displayed"
+    );
+
+    multi_buffer_editor.update(cx, |editor, cx| {
+        editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
+    });
+    assert_eq!(
+        multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
+        "\n\n\n1111\n2222\n3333\n\n\n\n\n4444\n5555\n6666\n\n\n",
+        "After unfolding the first buffer, its text should be displayed"
+    );
+
+    multi_buffer_editor.update(cx, |editor, cx| {
+        editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
+    });
+    assert_eq!(
+        multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
+        full_text,
+        "After unfolding all buffers, all original text should be displayed"
+    );
+}
+
+#[gpui::test]
+async fn test_multi_buffer_with_single_excerpt_folding(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
+
+    let fs = FakeFs::new(cx.executor());
+    fs.insert_tree(
+        "/a",
+        json!({
+            "main.rs": sample_text,
+        }),
+    )
+    .await;
+    let project = Project::test(fs, ["/a".as_ref()], cx).await;
+    let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+    let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
+    let worktree = project.update(cx, |project, cx| {
+        let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
+        assert_eq!(worktrees.len(), 1);
+        worktrees.pop().unwrap()
+    });
+    let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
+
+    let buffer_1 = project
+        .update(cx, |project, cx| {
+            project.open_buffer((worktree_id, "main.rs"), cx)
+        })
+        .await
+        .unwrap();
+
+    let multi_buffer = cx.new_model(|cx| {
+        let mut multi_buffer = MultiBuffer::new(ReadWrite);
+        multi_buffer.push_excerpts(
+            buffer_1.clone(),
+            [ExcerptRange {
+                context: Point::new(0, 0)
+                    ..Point::new(
+                        sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
+                        0,
+                    ),
+                primary: None,
+            }],
+            cx,
+        );
+        multi_buffer
+    });
+    let multi_buffer_editor = cx.new_view(|cx| {
+        Editor::new(
+            EditorMode::Full,
+            multi_buffer,
+            Some(project.clone()),
+            true,
+            cx,
+        )
+    });
+
+    let selection_range = Point::new(1, 0)..Point::new(2, 0);
+    multi_buffer_editor.update(cx, |editor, cx| {
+        enum TestHighlight {}
+        let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
+        let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
+        editor.highlight_text::<TestHighlight>(
+            vec![highlight_range.clone()],
+            HighlightStyle::color(Hsla::green()),
+            cx,
+        );
+        editor.change_selections(None, cx, |s| s.select_ranges(Some(highlight_range)));
+    });
+
+    let full_text = format!("\n\n\n{sample_text}\n");
+    assert_eq!(
+        multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
+        full_text,
+    );
+}
+
 fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
     let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
     point..point

crates/editor/src/element.rs 🔗

@@ -27,18 +27,18 @@ use crate::{
 };
 use client::ParticipantIndex;
 use collections::{BTreeMap, HashMap, HashSet};
+use file_icons::FileIcons;
 use git::{blame::BlameEntry, diff::DiffHunkStatus, Oid};
 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,
-    FontId, GlobalElementId, HighlightStyle, Hitbox, Hsla, InteractiveElement, IntoElement, Length,
-    ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad,
-    ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString, Size,
-    StatefulInteractiveElement, Style, Styled, TextRun, TextStyleRefinement, View, ViewContext,
-    WeakView, WindowContext,
+    transparent_black, Action, AnchorCorner, AnyElement, AvailableSpace, Bounds, ClickEvent,
+    ClipboardItem, ContentMask, Corners, CursorStyle, DispatchPhase, Edges, Element,
+    ElementInputHandler, Entity, FontId, GlobalElementId, HighlightStyle, Hitbox, Hsla,
+    InteractiveElement, IntoElement, Length, ModifiersChangedEvent, MouseButton, MouseDownEvent,
+    MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollWheelEvent,
+    ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled, Subscription,
+    TextRun, TextStyleRefinement, View, ViewContext, WeakView, WindowContext,
 };
-use gpui::{ClickEvent, Subscription};
 use itertools::Itertools;
 use language::{
     language_settings::{
@@ -49,8 +49,8 @@ use language::{
 };
 use lsp::DiagnosticSeverity;
 use multi_buffer::{
-    Anchor, AnchorRangeExt, ExcerptId, ExpandExcerptDirection, MultiBufferPoint, MultiBufferRow,
-    MultiBufferSnapshot, ToOffset,
+    Anchor, AnchorRangeExt, ExcerptId, ExcerptInfo, ExpandExcerptDirection, MultiBufferPoint,
+    MultiBufferRow, MultiBufferSnapshot, ToOffset,
 };
 use project::{
     project_settings::{GitGutterSetting, ProjectSettings},
@@ -1713,6 +1713,15 @@ impl EditorElement {
                     }
                     let multibuffer_point = tasks.offset.0.to_point(&snapshot.buffer_snapshot);
                     let multibuffer_row = MultiBufferRow(multibuffer_point.row);
+                    let buffer_folded = snapshot
+                        .buffer_snapshot
+                        .buffer_line_for_row(multibuffer_row)
+                        .map(|(buffer_snapshot, _)| buffer_snapshot.remote_id())
+                        .map(|buffer_id| editor.buffer_folded(buffer_id, cx))
+                        .unwrap_or(false);
+                    if buffer_folded {
+                        return None;
+                    }
 
                     if snapshot.is_line_folded(multibuffer_row) {
                         // Skip folded indicators, unless it's the starting line of a fold.
@@ -2087,6 +2096,7 @@ impl EditorElement {
         is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
         cx: &mut WindowContext,
     ) -> (AnyElement, Size<Pixels>) {
+        let header_padding = px(6.0);
         let mut element = match block {
             Block::Custom(block) => {
                 let block_start = block.start().to_point(&snapshot.buffer_snapshot);
@@ -2136,21 +2146,58 @@ impl EditorElement {
                     .into_any()
             }
 
+            Block::FoldedBuffer {
+                first_excerpt,
+                prev_excerpt,
+                show_excerpt_controls,
+                height,
+                ..
+            } => {
+                let icon_offset = gutter_dimensions.width
+                    - (gutter_dimensions.left_padding + gutter_dimensions.margin);
+
+                let mut result = v_flex().id(block_id).w_full();
+                if let Some(prev_excerpt) = prev_excerpt {
+                    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(
+                                    prev_excerpt.id,
+                                    ExpandExcerptDirection::Down,
+                                    IconName::ArrowDownFromLine,
+                                    cx,
+                                )),
+                        );
+                    }
+                }
+
+                let jump_data = jump_data(snapshot, block_row_start, *height, first_excerpt, cx);
+                result
+                    .child(self.render_buffer_header(
+                        first_excerpt,
+                        header_padding,
+                        true,
+                        jump_data,
+                        cx,
+                    ))
+                    .into_any_element()
+            }
             Block::ExcerptBoundary {
                 prev_excerpt,
                 next_excerpt,
                 show_excerpt_controls,
-                starts_new_buffer,
                 height,
+                starts_new_buffer,
                 ..
             } => {
                 let icon_offset = gutter_dimensions.width
                     - (gutter_dimensions.left_padding + gutter_dimensions.margin);
 
-                let header_padding = px(6.0);
-
                 let mut result = v_flex().id(block_id).w_full();
-
                 if let Some(prev_excerpt) = prev_excerpt {
                     if *show_excerpt_controls {
                         result = result.child(
@@ -2170,115 +2217,15 @@ impl EditorElement {
                 }
 
                 if let Some(next_excerpt) = next_excerpt {
-                    let buffer = &next_excerpt.buffer;
-                    let range = &next_excerpt.range;
-                    let jump_data = {
-                        let jump_path =
-                            project::File::from_dyn(buffer.file()).map(|file| 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 {
-                            excerpt_id: next_excerpt.id,
-                            anchor: jump_anchor,
-                            position: language::ToPoint::to_point(&jump_anchor, buffer),
-                            path: jump_path,
-                            line_offset_from_top,
-                        }
-                    };
-
+                    let jump_data = jump_data(snapshot, block_row_start, *height, next_excerpt, cx);
                     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(
-                                    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(
-                                            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,
-                                                        ))
-                                                    }),
-                                            ),
-                                        )
-                                        .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, e: &ClickEvent, cx| {
-                                                editor.open_excerpts_common(
-                                                    Some(jump_data.clone()),
-                                                    e.down.modifiers.secondary(),
-                                                    cx,
-                                                );
-                                            }
-                                        })),
-                                ),
-                        );
+                        result = result.child(self.render_buffer_header(
+                            next_excerpt,
+                            header_padding,
+                            false,
+                            jump_data,
+                            cx,
+                        ));
                         if *show_excerpt_controls {
                             result = result.child(
                                 h_flex()
@@ -2428,6 +2375,105 @@ impl EditorElement {
         (element, final_size)
     }
 
+    fn render_buffer_header(
+        &self,
+        for_excerpt: &ExcerptInfo,
+        header_padding: Pixels,
+        is_folded: bool,
+        jump_data: JumpData,
+        cx: &mut WindowContext,
+    ) -> Div {
+        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 = for_excerpt.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() + "/"));
+
+        div()
+            .px(header_padding)
+            .pt(header_padding)
+            .w_full()
+            .h(FILE_HEADER_HEIGHT as f32 * cx.line_height())
+            .child(
+                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(
+                        h_flex()
+                            .gap_3()
+                            .map(|header| {
+                                let editor = self.editor.clone();
+                                let buffer_id = for_excerpt.buffer_id;
+                                let toggle_chevron_icon =
+                                    FileIcons::get_chevron_icon(!is_folded, cx)
+                                        .map(Icon::from_path);
+                                header.child(
+                                    ButtonLike::new("toggle-buffer-fold")
+                                        .children(toggle_chevron_icon)
+                                        .on_click(move |_, cx| {
+                                            if is_folded {
+                                                editor.update(cx, |editor, cx| {
+                                                    editor.unfold_buffer(buffer_id, cx);
+                                                });
+                                            } else {
+                                                editor.update(cx, |editor, cx| {
+                                                    editor.fold_buffer(buffer_id, cx);
+                                                });
+                                            }
+                                        }),
+                                )
+                            })
+                            .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),
+                                        )
+                                    }),
+                            ),
+                    )
+                    .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, e: &ClickEvent, cx| {
+                            editor.open_excerpts_common(
+                                Some(jump_data.clone()),
+                                e.down.modifiers.secondary(),
+                                cx,
+                            );
+                        }
+                    })),
+            )
+    }
+
     fn render_expand_excerpt_button(
         &self,
         excerpt_id: ExcerptId,
@@ -4314,6 +4360,46 @@ impl EditorElement {
     }
 }
 
+fn jump_data(
+    snapshot: &EditorSnapshot,
+    block_row_start: DisplayRow,
+    height: u32,
+    for_excerpt: &ExcerptInfo,
+    cx: &mut WindowContext<'_>,
+) -> JumpData {
+    let range = &for_excerpt.range;
+    let buffer = &for_excerpt.buffer;
+    let jump_path = project::File::from_dyn(buffer.file()).map(|file| 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 {
+        excerpt_id: for_excerpt.id,
+        anchor: jump_anchor,
+        position: language::ToPoint::to_point(&jump_anchor, buffer),
+        path: jump_path,
+        line_offset_from_top,
+    }
+}
+
 fn inline_completion_popover_text(
     editor_snapshot: &EditorSnapshot,
     edits: &Vec<(Range<Anchor>, String)>,
@@ -5757,29 +5843,33 @@ impl Element for EditorElement {
                                 if !expanded_add_hunks_by_rows
                                     .contains_key(&newest_selection_display_row)
                                 {
-                                    let buffer = snapshot.buffer_snapshot.buffer_line_for_row(
-                                        MultiBufferRow(newest_selection_point.row),
-                                    );
-                                    if let Some((buffer, range)) = buffer {
-                                        let buffer_id = buffer.remote_id();
-                                        let row = range.start.row;
-                                        let has_test_indicator = self
-                                            .editor
-                                            .read(cx)
-                                            .tasks
-                                            .contains_key(&(buffer_id, row));
-
-                                        if !has_test_indicator {
-                                            code_actions_indicator = self
-                                                .layout_code_actions_indicator(
-                                                    line_height,
-                                                    newest_selection_head,
-                                                    scroll_pixel_position,
-                                                    &gutter_dimensions,
-                                                    &gutter_hitbox,
-                                                    &rows_with_hunk_bounds,
-                                                    cx,
-                                                );
+                                    if !snapshot
+                                        .is_line_folded(MultiBufferRow(newest_selection_point.row))
+                                    {
+                                        let buffer = snapshot.buffer_snapshot.buffer_line_for_row(
+                                            MultiBufferRow(newest_selection_point.row),
+                                        );
+                                        if let Some((buffer, range)) = buffer {
+                                            let buffer_id = buffer.remote_id();
+                                            let row = range.start.row;
+                                            let has_test_indicator = self
+                                                .editor
+                                                .read(cx)
+                                                .tasks
+                                                .contains_key(&(buffer_id, row));
+
+                                            if !has_test_indicator {
+                                                code_actions_indicator = self
+                                                    .layout_code_actions_indicator(
+                                                        line_height,
+                                                        newest_selection_head,
+                                                        scroll_pixel_position,
+                                                        &gutter_dimensions,
+                                                        &gutter_hitbox,
+                                                        &rows_with_hunk_bounds,
+                                                        cx,
+                                                    );
+                                            }
                                         }
                                     }
                                 }

crates/editor/src/indent_guides.rs 🔗

@@ -172,13 +172,7 @@ pub fn indent_guides_in_range(
             let start =
                 MultiBufferRow(indent_guide.multibuffer_row_range.start.0.saturating_sub(1));
             // Filter out indent guides that are inside a fold
-            let is_folded = snapshot.is_line_folded(start);
-            let line_indent = snapshot.line_indent_for_buffer_row(start);
-
-            let contained_in_fold =
-                line_indent.len(indent_guide.tab_size) <= indent_guide.indent_level();
-
-            !(is_folded && contained_in_fold)
+            !snapshot.is_line_folded(start)
         })
         .collect()
 }

crates/multi_buffer/src/multi_buffer.rs 🔗

@@ -195,6 +195,7 @@ pub struct ExcerptInfo {
     pub buffer: BufferSnapshot,
     pub buffer_id: BufferId,
     pub range: ExcerptRange<text::Anchor>,
+    pub text_summary: TextSummary,
 }
 
 impl std::fmt::Debug for ExcerptInfo {
@@ -1546,6 +1547,33 @@ impl MultiBuffer {
         excerpts
     }
 
+    pub fn excerpt_ranges_for_buffer(
+        &self,
+        buffer_id: BufferId,
+        cx: &AppContext,
+    ) -> Vec<Range<Point>> {
+        let snapshot = self.read(cx);
+        let buffers = self.buffers.borrow();
+        let mut cursor = snapshot.excerpts.cursor::<(Option<&Locator>, Point)>(&());
+        buffers
+            .get(&buffer_id)
+            .into_iter()
+            .flat_map(|state| &state.excerpts)
+            .filter_map(move |locator| {
+                cursor.seek_forward(&Some(locator), Bias::Left, &());
+                cursor.item().and_then(|excerpt| {
+                    if excerpt.locator == *locator {
+                        let excerpt_start = cursor.start().1;
+                        let excerpt_end = excerpt_start + excerpt.text_summary.lines;
+                        Some(excerpt_start..excerpt_end)
+                    } else {
+                        None
+                    }
+                })
+            })
+            .collect()
+    }
+
     pub fn excerpt_buffer_ids(&self) -> Vec<BufferId> {
         self.snapshot
             .borrow()
@@ -3559,6 +3587,7 @@ impl MultiBufferSnapshot {
                     buffer: excerpt.buffer.clone(),
                     buffer_id: excerpt.buffer_id,
                     range: excerpt.range.clone(),
+                    text_summary: excerpt.text_summary.clone(),
                 });
 
                 if next.is_none() {
@@ -3574,6 +3603,7 @@ impl MultiBufferSnapshot {
                     buffer: prev_excerpt.buffer.clone(),
                     buffer_id: prev_excerpt.buffer_id,
                     range: prev_excerpt.range.clone(),
+                    text_summary: prev_excerpt.text_summary.clone(),
                 });
                 let row = MultiBufferRow(cursor.start().1.row);
 

crates/outline_panel/src/outline_panel.rs 🔗

@@ -103,6 +103,7 @@ pub struct OutlinePanel {
     active_item: Option<ActiveItem>,
     _subscriptions: Vec<Subscription>,
     updating_fs_entries: bool,
+    new_entries_for_fs_update: HashSet<ExcerptId>,
     fs_entries_update_task: Task<()>,
     cached_entries_update_task: Task<()>,
     reveal_selection_task: Task<anyhow::Result<()>>,
@@ -116,6 +117,7 @@ pub struct OutlinePanel {
     horizontal_scrollbar_state: ScrollbarState,
     hide_scrollbar_task: Option<Task<()>>,
     max_width_item_index: Option<usize>,
+    preserve_selection_on_buffer_fold_toggles: HashSet<BufferId>,
 }
 
 #[derive(Debug)]
@@ -716,6 +718,8 @@ impl OutlinePanel {
                 active_item: None,
                 pending_serialization: Task::ready(None),
                 updating_fs_entries: false,
+                new_entries_for_fs_update: HashSet::default(),
+                preserve_selection_on_buffer_fold_toggles: HashSet::default(),
                 fs_entries_update_task: Task::ready(()),
                 cached_entries_update_task: Task::ready(()),
                 reveal_selection_task: Task::ready(Ok(())),
@@ -811,7 +815,8 @@ impl OutlinePanel {
         if self.filter_editor.focus_handle(cx).is_focused(cx) {
             cx.propagate()
         } else if let Some(selected_entry) = self.selected_entry().cloned() {
-            self.open_entry(&selected_entry, true, false, cx);
+            self.toggle_expanded(&selected_entry, cx);
+            self.scroll_editor_to_entry(&selected_entry, true, false, cx);
         }
     }
 
@@ -834,7 +839,7 @@ impl OutlinePanel {
         } else if let Some((active_editor, selected_entry)) =
             self.active_editor().zip(self.selected_entry().cloned())
         {
-            self.open_entry(&selected_entry, true, true, cx);
+            self.scroll_editor_to_entry(&selected_entry, true, true, cx);
             active_editor.update(cx, |editor, cx| editor.open_excerpts(action, cx));
         }
     }
@@ -849,12 +854,12 @@ impl OutlinePanel {
         } else if let Some((active_editor, selected_entry)) =
             self.active_editor().zip(self.selected_entry().cloned())
         {
-            self.open_entry(&selected_entry, true, true, cx);
+            self.scroll_editor_to_entry(&selected_entry, true, true, cx);
             active_editor.update(cx, |editor, cx| editor.open_excerpts_in_split(action, cx));
         }
     }
 
-    fn open_entry(
+    fn scroll_editor_to_entry(
         &mut self,
         entry: &PanelEntry,
         prefer_selection_change: bool,
@@ -866,18 +871,14 @@ impl OutlinePanel {
         };
         let active_multi_buffer = active_editor.read(cx).buffer().clone();
         let multi_buffer_snapshot = active_multi_buffer.read(cx).snapshot(cx);
-        let offset_from_top = if active_multi_buffer.read(cx).is_singleton() {
-            Point::default()
-        } else {
-            Point::new(0.0, -(active_editor.read(cx).file_header_size() as f32))
-        };
-
         let mut change_selection = prefer_selection_change;
+        let mut scroll_to_buffer = None;
         let scroll_target = match entry {
             PanelEntry::FoldedDirs(..) | PanelEntry::Fs(FsEntry::Directory(..)) => None,
             PanelEntry::Fs(FsEntry::ExternalFile(buffer_id, _)) => {
                 change_selection = false;
-                let scroll_target = multi_buffer_snapshot.excerpts().find_map(
+                scroll_to_buffer = Some(*buffer_id);
+                multi_buffer_snapshot.excerpts().find_map(
                     |(excerpt_id, buffer_snapshot, excerpt_range)| {
                         if &buffer_snapshot.remote_id() == buffer_id {
                             multi_buffer_snapshot
@@ -886,13 +887,12 @@ impl OutlinePanel {
                             None
                         }
                     },
-                );
-                Some(offset_from_top).zip(scroll_target)
+                )
             }
-            PanelEntry::Fs(FsEntry::File(_, file_entry, ..)) => {
+            PanelEntry::Fs(FsEntry::File(_, file_entry, buffer_id, _)) => {
                 change_selection = false;
-                let scroll_target = self
-                    .project
+                scroll_to_buffer = Some(*buffer_id);
+                self.project
                     .update(cx, |project, cx| {
                         project
                             .path_for_entry(file_entry.id, cx)
@@ -907,28 +907,23 @@ impl OutlinePanel {
                         let (excerpt_id, excerpt_range) = excerpts.first()?;
                         multi_buffer_snapshot
                             .anchor_in_excerpt(*excerpt_id, excerpt_range.context.start)
-                    });
-                Some(offset_from_top).zip(scroll_target)
+                    })
             }
             PanelEntry::Outline(OutlineEntry::Outline(_, excerpt_id, outline)) => {
-                let scroll_target = multi_buffer_snapshot
+                multi_buffer_snapshot
                     .anchor_in_excerpt(*excerpt_id, outline.range.start)
                     .or_else(|| {
                         multi_buffer_snapshot.anchor_in_excerpt(*excerpt_id, outline.range.end)
-                    });
-                Some(Point::default()).zip(scroll_target)
+                    })
             }
             PanelEntry::Outline(OutlineEntry::Excerpt(_, excerpt_id, excerpt_range)) => {
-                let scroll_target = multi_buffer_snapshot
-                    .anchor_in_excerpt(*excerpt_id, excerpt_range.context.start);
-                Some(Point::default()).zip(scroll_target)
-            }
-            PanelEntry::Search(SearchEntry { match_range, .. }) => {
-                Some((Point::default(), match_range.start))
+                change_selection = false;
+                multi_buffer_snapshot.anchor_in_excerpt(*excerpt_id, excerpt_range.context.start)
             }
+            PanelEntry::Search(SearchEntry { match_range, .. }) => Some(match_range.start),
         };
 
-        if let Some((offset, anchor)) = scroll_target {
+        if let Some(anchor) = scroll_target {
             let activate = self
                 .workspace
                 .update(cx, |workspace, cx| match self.active_item() {
@@ -949,6 +944,43 @@ impl OutlinePanel {
                         );
                     });
                 } else {
+                    let mut offset = Point::default();
+                    let show_excerpt_controls = active_editor
+                        .read(cx)
+                        .display_map
+                        .read(cx)
+                        .show_excerpt_controls();
+                    let expand_excerpt_control_height = 1.0;
+                    if let Some(buffer_id) = scroll_to_buffer {
+                        let current_folded = active_editor.read(cx).buffer_folded(buffer_id, cx);
+                        if current_folded {
+                            if show_excerpt_controls {
+                                let previous_buffer_id = self
+                                    .fs_entries
+                                    .iter()
+                                    .rev()
+                                    .filter_map(|entry| match entry {
+                                        FsEntry::File(_, _, buffer_id, _)
+                                        | FsEntry::ExternalFile(buffer_id, _) => Some(*buffer_id),
+                                        FsEntry::Directory(..) => None,
+                                    })
+                                    .skip_while(|id| *id != buffer_id)
+                                    .skip(1)
+                                    .next();
+                                if let Some(previous_buffer_id) = previous_buffer_id {
+                                    if !active_editor.read(cx).buffer_folded(previous_buffer_id, cx)
+                                    {
+                                        offset.y += expand_excerpt_control_height;
+                                    }
+                                }
+                            }
+                        } else {
+                            offset.y = -(active_editor.read(cx).file_header_size() as f32);
+                            if show_excerpt_controls {
+                                offset.y -= expand_excerpt_control_height;
+                            }
+                        }
+                    }
                     active_editor.update(cx, |editor, cx| {
                         editor.set_scroll_anchor(ScrollAnchor { offset, anchor }, cx);
                     });
@@ -977,7 +1009,7 @@ impl OutlinePanel {
             self.select_first(&SelectFirst {}, cx)
         }
         if let Some(selected_entry) = self.selected_entry().cloned() {
-            self.open_entry(&selected_entry, true, false, cx);
+            self.scroll_editor_to_entry(&selected_entry, true, false, cx);
         }
     }
 
@@ -996,7 +1028,7 @@ impl OutlinePanel {
             self.select_last(&SelectLast, cx)
         }
         if let Some(selected_entry) = self.selected_entry().cloned() {
-            self.open_entry(&selected_entry, true, false, cx);
+            self.scroll_editor_to_entry(&selected_entry, true, false, cx);
         }
     }
 
@@ -1230,23 +1262,34 @@ impl OutlinePanel {
     }
 
     fn expand_selected_entry(&mut self, _: &ExpandSelectedEntry, cx: &mut ViewContext<Self>) {
-        let entry_to_expand = match self.selected_entry() {
-            Some(PanelEntry::FoldedDirs(worktree_id, dir_entries)) => dir_entries
-                .last()
-                .map(|entry| CollapsedEntry::Dir(*worktree_id, entry.id)),
-            Some(PanelEntry::Fs(FsEntry::Directory(worktree_id, dir_entry))) => {
+        let Some(active_editor) = self.active_editor() else {
+            return;
+        };
+        let Some(selected_entry) = self.selected_entry().cloned() else {
+            return;
+        };
+        let mut buffers_to_unfold = HashSet::default();
+        let entry_to_expand = match &selected_entry {
+            PanelEntry::FoldedDirs(worktree_id, dir_entries) => dir_entries.last().map(|entry| {
+                buffers_to_unfold.extend(self.buffers_inside_directory(*worktree_id, entry));
+                CollapsedEntry::Dir(*worktree_id, entry.id)
+            }),
+            PanelEntry::Fs(FsEntry::Directory(worktree_id, dir_entry)) => {
+                buffers_to_unfold.extend(self.buffers_inside_directory(*worktree_id, dir_entry));
                 Some(CollapsedEntry::Dir(*worktree_id, dir_entry.id))
             }
-            Some(PanelEntry::Fs(FsEntry::File(worktree_id, _, buffer_id, _))) => {
+            PanelEntry::Fs(FsEntry::File(worktree_id, _, buffer_id, _)) => {
+                buffers_to_unfold.insert(*buffer_id);
                 Some(CollapsedEntry::File(*worktree_id, *buffer_id))
             }
-            Some(PanelEntry::Fs(FsEntry::ExternalFile(buffer_id, _))) => {
+            PanelEntry::Fs(FsEntry::ExternalFile(buffer_id, _)) => {
+                buffers_to_unfold.insert(*buffer_id);
                 Some(CollapsedEntry::ExternalFile(*buffer_id))
             }
-            Some(PanelEntry::Outline(OutlineEntry::Excerpt(buffer_id, excerpt_id, _))) => {
+            PanelEntry::Outline(OutlineEntry::Excerpt(buffer_id, excerpt_id, _)) => {
                 Some(CollapsedEntry::Excerpt(*buffer_id, *excerpt_id))
             }
-            None | Some(PanelEntry::Search(_)) | Some(PanelEntry::Outline(..)) => None,
+            PanelEntry::Search(_) | PanelEntry::Outline(..) => return,
         };
         let Some(collapsed_entry) = entry_to_expand else {
             return;
@@ -1254,70 +1297,120 @@ impl OutlinePanel {
         let expanded = self.collapsed_entries.remove(&collapsed_entry);
         if expanded {
             if let CollapsedEntry::Dir(worktree_id, dir_entry_id) = collapsed_entry {
-                self.project.update(cx, |project, cx| {
-                    project.expand_entry(worktree_id, dir_entry_id, cx);
+                let task = self.project.update(cx, |project, cx| {
+                    project.expand_entry(worktree_id, dir_entry_id, cx)
                 });
+                if let Some(task) = task {
+                    task.detach_and_log_err(cx);
+                }
+            };
+
+            active_editor.update(cx, |editor, cx| {
+                buffers_to_unfold.retain(|buffer_id| editor.buffer_folded(*buffer_id, cx));
+            });
+            self.select_entry(selected_entry, true, cx);
+            if buffers_to_unfold.is_empty() {
+                self.update_cached_entries(None, cx);
+            } else {
+                self.toggle_buffers_fold(buffers_to_unfold, false, cx)
+                    .detach();
             }
-            self.update_cached_entries(None, cx);
         } else {
             self.select_next(&SelectNext, cx)
         }
     }
 
     fn collapse_selected_entry(&mut self, _: &CollapseSelectedEntry, cx: &mut ViewContext<Self>) {
+        let Some(active_editor) = self.active_editor() else {
+            return;
+        };
         let Some(selected_entry) = self.selected_entry().cloned() else {
             return;
         };
-        match &selected_entry {
+
+        let mut buffers_to_fold = HashSet::default();
+        let collapsed = match &selected_entry {
             PanelEntry::Fs(FsEntry::Directory(worktree_id, selected_dir_entry)) => {
-                self.collapsed_entries
-                    .insert(CollapsedEntry::Dir(*worktree_id, selected_dir_entry.id));
-                self.select_entry(selected_entry, true, cx);
-                self.update_cached_entries(None, cx);
+                if self
+                    .collapsed_entries
+                    .insert(CollapsedEntry::Dir(*worktree_id, selected_dir_entry.id))
+                {
+                    buffers_to_fold
+                        .extend(self.buffers_inside_directory(*worktree_id, selected_dir_entry));
+                    true
+                } else {
+                    false
+                }
             }
             PanelEntry::Fs(FsEntry::File(worktree_id, _, buffer_id, _)) => {
-                self.collapsed_entries
-                    .insert(CollapsedEntry::File(*worktree_id, *buffer_id));
-                self.select_entry(selected_entry, true, cx);
-                self.update_cached_entries(None, cx);
+                if self
+                    .collapsed_entries
+                    .insert(CollapsedEntry::File(*worktree_id, *buffer_id))
+                {
+                    buffers_to_fold.insert(*buffer_id);
+                    true
+                } else {
+                    false
+                }
             }
             PanelEntry::Fs(FsEntry::ExternalFile(buffer_id, _)) => {
-                self.collapsed_entries
-                    .insert(CollapsedEntry::ExternalFile(*buffer_id));
-                self.select_entry(selected_entry, true, cx);
-                self.update_cached_entries(None, cx);
+                if self
+                    .collapsed_entries
+                    .insert(CollapsedEntry::ExternalFile(*buffer_id))
+                {
+                    buffers_to_fold.insert(*buffer_id);
+                    true
+                } else {
+                    false
+                }
             }
             PanelEntry::FoldedDirs(worktree_id, dir_entries) => {
+                let mut folded = false;
                 if let Some(dir_entry) = dir_entries.last() {
                     if self
                         .collapsed_entries
                         .insert(CollapsedEntry::Dir(*worktree_id, dir_entry.id))
                     {
-                        self.select_entry(selected_entry, true, cx);
-                        self.update_cached_entries(None, cx);
+                        folded = true;
+                        buffers_to_fold
+                            .extend(self.buffers_inside_directory(*worktree_id, dir_entry));
                     }
                 }
+                folded
             }
-            PanelEntry::Outline(OutlineEntry::Excerpt(buffer_id, excerpt_id, _)) => {
-                if self
-                    .collapsed_entries
-                    .insert(CollapsedEntry::Excerpt(*buffer_id, *excerpt_id))
-                {
-                    self.select_entry(selected_entry, true, cx);
-                    self.update_cached_entries(None, cx);
-                }
+            PanelEntry::Outline(OutlineEntry::Excerpt(buffer_id, excerpt_id, _)) => self
+                .collapsed_entries
+                .insert(CollapsedEntry::Excerpt(*buffer_id, *excerpt_id)),
+            PanelEntry::Search(_) | PanelEntry::Outline(..) => false,
+        };
+
+        if collapsed {
+            active_editor.update(cx, |editor, cx| {
+                buffers_to_fold.retain(|buffer_id| !editor.buffer_folded(*buffer_id, cx));
+            });
+            self.select_entry(selected_entry, true, cx);
+            if buffers_to_fold.is_empty() {
+                self.update_cached_entries(None, cx);
+            } else {
+                self.toggle_buffers_fold(buffers_to_fold, true, cx).detach();
             }
-            PanelEntry::Search(_) | PanelEntry::Outline(..) => {}
+        } else {
+            self.select_parent(&SelectParent, cx);
         }
     }
 
     pub fn expand_all_entries(&mut self, _: &ExpandAllEntries, cx: &mut ViewContext<Self>) {
+        let Some(active_editor) = self.active_editor() else {
+            return;
+        };
+        let mut buffers_to_unfold = HashSet::default();
         let expanded_entries =
             self.fs_entries
                 .iter()
                 .fold(HashSet::default(), |mut entries, fs_entry| {
                     match fs_entry {
                         FsEntry::ExternalFile(buffer_id, _) => {
+                            buffers_to_unfold.insert(*buffer_id);
                             entries.insert(CollapsedEntry::ExternalFile(*buffer_id));
                             entries.extend(self.excerpts.get(buffer_id).into_iter().flat_map(
                                 |excerpts| {
@@ -1331,6 +1424,7 @@ impl OutlinePanel {
                             entries.insert(CollapsedEntry::Dir(*worktree_id, entry.id));
                         }
                         FsEntry::File(worktree_id, _, buffer_id, _) => {
+                            buffers_to_unfold.insert(*buffer_id);
                             entries.insert(CollapsedEntry::File(*worktree_id, *buffer_id));
                             entries.extend(self.excerpts.get(buffer_id).into_iter().flat_map(
                                 |excerpts| {
@@ -1340,15 +1434,27 @@ impl OutlinePanel {
                                 },
                             ));
                         }
-                    }
+                    };
                     entries
                 });
         self.collapsed_entries
             .retain(|entry| !expanded_entries.contains(entry));
-        self.update_cached_entries(None, cx);
+        active_editor.update(cx, |editor, cx| {
+            buffers_to_unfold.retain(|buffer_id| editor.buffer_folded(*buffer_id, cx));
+        });
+        if buffers_to_unfold.is_empty() {
+            self.update_cached_entries(None, cx);
+        } else {
+            self.toggle_buffers_fold(buffers_to_unfold, false, cx)
+                .detach();
+        }
     }
 
     pub fn collapse_all_entries(&mut self, _: &CollapseAllEntries, cx: &mut ViewContext<Self>) {
+        let Some(active_editor) = self.active_editor() else {
+            return;
+        };
+        let mut buffers_to_fold = HashSet::default();
         let new_entries = self
             .cached_entries
             .iter()
@@ -1357,9 +1463,11 @@ impl OutlinePanel {
                     Some(CollapsedEntry::Dir(*worktree_id, entry.id))
                 }
                 PanelEntry::Fs(FsEntry::File(worktree_id, _, buffer_id, _)) => {
+                    buffers_to_fold.insert(*buffer_id);
                     Some(CollapsedEntry::File(*worktree_id, *buffer_id))
                 }
                 PanelEntry::Fs(FsEntry::ExternalFile(buffer_id, _)) => {
+                    buffers_to_fold.insert(*buffer_id);
                     Some(CollapsedEntry::ExternalFile(*buffer_id))
                 }
                 PanelEntry::FoldedDirs(worktree_id, entries) => {
@@ -1372,14 +1480,28 @@ impl OutlinePanel {
             })
             .collect::<Vec<_>>();
         self.collapsed_entries.extend(new_entries);
-        self.update_cached_entries(None, cx);
+
+        active_editor.update(cx, |editor, cx| {
+            buffers_to_fold.retain(|buffer_id| !editor.buffer_folded(*buffer_id, cx));
+        });
+        if buffers_to_fold.is_empty() {
+            self.update_cached_entries(None, cx);
+        } else {
+            self.toggle_buffers_fold(buffers_to_fold, true, cx).detach();
+        }
     }
 
     fn toggle_expanded(&mut self, entry: &PanelEntry, cx: &mut ViewContext<Self>) {
+        let Some(active_editor) = self.active_editor() else {
+            return;
+        };
+        let mut fold = false;
+        let mut buffers_to_toggle = HashSet::default();
         match entry {
             PanelEntry::Fs(FsEntry::Directory(worktree_id, dir_entry)) => {
                 let entry_id = dir_entry.id;
                 let collapsed_entry = CollapsedEntry::Dir(*worktree_id, entry_id);
+                buffers_to_toggle.extend(self.buffers_inside_directory(*worktree_id, dir_entry));
                 if self.collapsed_entries.remove(&collapsed_entry) {
                     self.project
                         .update(cx, |project, cx| {
@@ -1389,23 +1511,31 @@ impl OutlinePanel {
                         .detach_and_log_err(cx);
                 } else {
                     self.collapsed_entries.insert(collapsed_entry);
+                    fold = true;
                 }
             }
             PanelEntry::Fs(FsEntry::File(worktree_id, _, buffer_id, _)) => {
                 let collapsed_entry = CollapsedEntry::File(*worktree_id, *buffer_id);
+                buffers_to_toggle.insert(*buffer_id);
                 if !self.collapsed_entries.remove(&collapsed_entry) {
                     self.collapsed_entries.insert(collapsed_entry);
+                    fold = true;
                 }
             }
             PanelEntry::Fs(FsEntry::ExternalFile(buffer_id, _)) => {
                 let collapsed_entry = CollapsedEntry::ExternalFile(*buffer_id);
+                buffers_to_toggle.insert(*buffer_id);
                 if !self.collapsed_entries.remove(&collapsed_entry) {
                     self.collapsed_entries.insert(collapsed_entry);
+                    fold = true;
                 }
             }
             PanelEntry::FoldedDirs(worktree_id, dir_entries) => {
-                if let Some(entry_id) = dir_entries.first().map(|entry| entry.id) {
+                if let Some(dir_entry) = dir_entries.first() {
+                    let entry_id = dir_entry.id;
                     let collapsed_entry = CollapsedEntry::Dir(*worktree_id, entry_id);
+                    buffers_to_toggle
+                        .extend(self.buffers_inside_directory(*worktree_id, dir_entry));
                     if self.collapsed_entries.remove(&collapsed_entry) {
                         self.project
                             .update(cx, |project, cx| {
@@ -1415,6 +1545,7 @@ impl OutlinePanel {
                             .detach_and_log_err(cx);
                     } else {
                         self.collapsed_entries.insert(collapsed_entry);
+                        fold = true;
                     }
                 }
             }
@@ -1427,8 +1558,56 @@ impl OutlinePanel {
             PanelEntry::Search(_) | PanelEntry::Outline(..) => return,
         }
 
+        active_editor.update(cx, |editor, cx| {
+            buffers_to_toggle.retain(|buffer_id| {
+                let folded = editor.buffer_folded(*buffer_id, cx);
+                if fold {
+                    !folded
+                } else {
+                    folded
+                }
+            });
+        });
+
         self.select_entry(entry.clone(), true, cx);
-        self.update_cached_entries(None, cx);
+        if buffers_to_toggle.is_empty() {
+            self.update_cached_entries(None, cx);
+        } else {
+            self.toggle_buffers_fold(buffers_to_toggle, fold, cx)
+                .detach();
+        }
+    }
+
+    fn toggle_buffers_fold(
+        &self,
+        buffers: HashSet<BufferId>,
+        fold: bool,
+        cx: &mut ViewContext<Self>,
+    ) -> Task<()> {
+        let Some(active_editor) = self.active_editor() else {
+            return Task::ready(());
+        };
+        cx.spawn(|outline_panel, mut cx| async move {
+            outline_panel
+                .update(&mut cx, |outline_panel, cx| {
+                    active_editor.update(cx, |editor, cx| {
+                        for buffer_id in buffers {
+                            outline_panel
+                                .preserve_selection_on_buffer_fold_toggles
+                                .insert(buffer_id);
+                            if fold {
+                                editor.fold_buffer(buffer_id, cx);
+                            } else {
+                                editor.unfold_buffer(buffer_id, cx);
+                            }
+                        }
+                    });
+                    if let Some(selection) = outline_panel.selected_entry().cloned() {
+                        outline_panel.scroll_editor_to_entry(&selection, false, false, cx);
+                    }
+                })
+                .ok();
+        })
     }
 
     fn copy_path(&mut self, _: &CopyPath, cx: &mut ViewContext<Self>) {
@@ -1816,7 +1995,7 @@ impl OutlinePanel {
                     icon.unwrap_or_else(empty_icon),
                 )
             }
-            FsEntry::ExternalFile(buffer_id, ..) => {
+            FsEntry::ExternalFile(buffer_id, _) => {
                 let color = entry_label_color(is_active);
                 let (icon, name) = match self.buffer_snapshot_for_id(*buffer_id, cx) {
                     Some(buffer_snapshot) => match buffer_snapshot.file() {
@@ -2037,7 +2216,7 @@ impl OutlinePanel {
                     }
                     let change_focus = event.down.click_count > 1;
                     outline_panel.toggle_expanded(&clicked_entry, cx);
-                    outline_panel.open_entry(&clicked_entry, true, change_focus, cx);
+                    outline_panel.scroll_editor_to_entry(&clicked_entry, true, change_focus, cx);
                 })
             })
             .cursor_pointer()
@@ -2107,8 +2286,7 @@ impl OutlinePanel {
 
     fn update_fs_entries(
         &mut self,
-        active_editor: &View<Editor>,
-        new_entries: HashSet<ExcerptId>,
+        active_editor: View<Editor>,
         debounce: Option<Duration>,
         cx: &mut ViewContext<Self>,
     ) {
@@ -2118,6 +2296,7 @@ impl OutlinePanel {
 
         let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs;
         let active_multi_buffer = active_editor.read(cx).buffer().clone();
+        let new_entries = self.new_entries_for_fs_update.clone();
         self.updating_fs_entries = true;
         self.fs_entries_update_task = cx.spawn(|outline_panel, mut cx| async move {
             if let Some(debounce) = debounce {
@@ -2141,10 +2320,11 @@ impl OutlinePanel {
                         let worktree = file.map(|file| file.worktree.read(cx).snapshot());
                         let is_new = new_entries.contains(&excerpt_id)
                             || !outline_panel.excerpts.contains_key(&buffer_id);
+                        let is_folded = active_editor.read(cx).buffer_folded(buffer_id, cx);
                         buffer_excerpts
                             .entry(buffer_id)
-                            .or_insert_with(|| (is_new, Vec::new(), entry_id, worktree))
-                            .1
+                            .or_insert_with(|| (is_new, is_folded, Vec::new(), entry_id, worktree))
+                            .2
                             .push(excerpt_id);
 
                         let outlines = match outline_panel
@@ -2196,8 +2376,21 @@ impl OutlinePanel {
                     >::default();
                     let mut external_excerpts = HashMap::default();
 
-                    for (buffer_id, (is_new, excerpts, entry_id, worktree)) in buffer_excerpts {
-                        if is_new {
+                    for (buffer_id, (is_new, is_folded, excerpts, entry_id, worktree)) in
+                        buffer_excerpts
+                    {
+                        if is_folded {
+                            match &worktree {
+                                Some(worktree) => {
+                                    new_collapsed_entries
+                                        .insert(CollapsedEntry::File(worktree.id(), buffer_id));
+                                }
+                                None => {
+                                    new_collapsed_entries
+                                        .insert(CollapsedEntry::ExternalFile(buffer_id));
+                                }
+                            }
+                        } else if is_new {
                             match &worktree {
                                 Some(worktree) => {
                                     new_collapsed_entries
@@ -2438,6 +2631,7 @@ impl OutlinePanel {
             outline_panel
                 .update(&mut cx, |outline_panel, cx| {
                     outline_panel.updating_fs_entries = false;
+                    outline_panel.new_entries_for_fs_update.clear();
                     outline_panel.excerpts = new_excerpts;
                     outline_panel.collapsed_entries = new_collapsed_entries;
                     outline_panel.unfolded_dirs = new_unfolded_dirs;
@@ -2475,10 +2669,10 @@ impl OutlinePanel {
             item_handle: new_active_item.downgrade_item(),
             active_editor: new_active_editor.downgrade(),
         });
-        let new_entries =
-            HashSet::from_iter(new_active_editor.read(cx).buffer().read(cx).excerpt_ids());
+        self.new_entries_for_fs_update
+            .extend(new_active_editor.read(cx).buffer().read(cx).excerpt_ids());
         self.selected_entry.invalidate();
-        self.update_fs_entries(&new_active_editor, new_entries, None, cx);
+        self.update_fs_entries(new_active_editor, None, cx);
     }
 
     fn clear_previous(&mut self, cx: &mut WindowContext<'_>) {
@@ -2517,6 +2711,20 @@ impl OutlinePanel {
             .read(cx)
             .excerpt_containing(selection, cx)?;
         let buffer_id = buffer.read(cx).remote_id();
+
+        if editor.read(cx).buffer_folded(buffer_id, cx) {
+            return self
+                .fs_entries
+                .iter()
+                .find(|fs_entry| match fs_entry {
+                    FsEntry::Directory(..) => false,
+                    FsEntry::File(_, _, file_buffer_id, _)
+                    | FsEntry::ExternalFile(file_buffer_id, _) => *file_buffer_id == buffer_id,
+                })
+                .cloned()
+                .map(PanelEntry::Fs);
+        }
+
         let selection_display_point = selection.to_display_point(&editor_snapshot);
 
         match &self.mode {
@@ -2919,6 +3127,9 @@ impl OutlinePanel {
         cx: &mut ViewContext<'_, Self>,
     ) -> Task<(Vec<CachedEntry>, Option<usize>)> {
         let project = self.project.clone();
+        let Some(active_editor) = self.active_editor() else {
+            return Task::ready((Vec::new(), None));
+        };
         cx.spawn(|outline_panel, mut cx| async move {
             let mut generation_state = GenerationState::default();
 
@@ -3149,6 +3360,7 @@ impl OutlinePanel {
                             if is_singleton || query.is_some() || (should_add && is_expanded) {
                                 outline_panel.add_search_entries(
                                     &mut generation_state,
+                                    &active_editor,
                                     entry.clone(),
                                     depth,
                                     query.clone(),
@@ -3173,16 +3385,18 @@ impl OutlinePanel {
                                     None
                                 };
                             if let Some((buffer_id, entry_excerpts)) = excerpts_to_consider {
-                                outline_panel.add_excerpt_entries(
-                                    &mut generation_state,
-                                    buffer_id,
-                                    entry_excerpts,
-                                    depth,
-                                    track_matches,
-                                    is_singleton,
-                                    query.as_deref(),
-                                    cx,
-                                );
+                                if !active_editor.read(cx).buffer_folded(buffer_id, cx) {
+                                    outline_panel.add_excerpt_entries(
+                                        &mut generation_state,
+                                        buffer_id,
+                                        entry_excerpts,
+                                        depth,
+                                        track_matches,
+                                        is_singleton,
+                                        query.as_deref(),
+                                        cx,
+                                    );
+                                }
                             }
                         }
                     }
@@ -3536,15 +3750,13 @@ impl OutlinePanel {
     fn add_search_entries(
         &mut self,
         state: &mut GenerationState,
+        active_editor: &View<Editor>,
         parent_entry: FsEntry,
         parent_depth: usize,
         filter_query: Option<String>,
         is_singleton: bool,
         cx: &mut ViewContext<Self>,
     ) {
-        if self.active_editor().is_none() {
-            return;
-        };
         let ItemsDisplayMode::Search(search_state) = &mut self.mode else {
             return;
         };
@@ -3560,10 +3772,27 @@ impl OutlinePanel {
         .collect::<HashSet<_>>();
 
         let depth = if is_singleton { 0 } else { parent_depth + 1 };
-        let new_search_matches = search_state.matches.iter().filter(|(match_range, _)| {
-            related_excerpts.contains(&match_range.start.excerpt_id)
-                || related_excerpts.contains(&match_range.end.excerpt_id)
-        });
+        let new_search_matches = search_state
+            .matches
+            .iter()
+            .filter(|(match_range, _)| {
+                related_excerpts.contains(&match_range.start.excerpt_id)
+                    || related_excerpts.contains(&match_range.end.excerpt_id)
+            })
+            .filter(|(match_range, _)| {
+                let editor = active_editor.read(cx);
+                if let Some(buffer_id) = match_range.start.buffer_id {
+                    if editor.buffer_folded(buffer_id, cx) {
+                        return false;
+                    }
+                }
+                if let Some(buffer_id) = match_range.start.buffer_id {
+                    if editor.buffer_folded(buffer_id, cx) {
+                        return false;
+                    }
+                }
+                true
+            });
 
         let new_search_entries = new_search_matches
             .map(|(match_range, search_data)| SearchEntry {
@@ -4071,6 +4300,41 @@ impl OutlinePanel {
                 ),
         )
     }
+
+    fn buffers_inside_directory(
+        &self,
+        dir_worktree: WorktreeId,
+        dir_entry: &Entry,
+    ) -> HashSet<BufferId> {
+        if !dir_entry.is_dir() {
+            debug_panic!("buffers_inside_directory called on a non-directory entry {dir_entry:?}");
+            return HashSet::default();
+        }
+
+        self.fs_entries
+            .iter()
+            .skip_while(|fs_entry| match fs_entry {
+                FsEntry::Directory(worktree_id, entry) => {
+                    *worktree_id != dir_worktree || entry != dir_entry
+                }
+                _ => true,
+            })
+            .skip(1)
+            .take_while(|fs_entry| match fs_entry {
+                FsEntry::ExternalFile(..) => false,
+                FsEntry::Directory(worktree_id, entry) => {
+                    *worktree_id == dir_worktree && entry.path.starts_with(&dir_entry.path)
+                }
+                FsEntry::File(worktree_id, entry, ..) => {
+                    *worktree_id == dir_worktree && entry.path.starts_with(&dir_entry.path)
+                }
+            })
+            .filter_map(|fs_entry| match fs_entry {
+                FsEntry::File(_, _, buffer_id, _) => Some(*buffer_id),
+                _ => None,
+            })
+            .collect()
+    }
 }
 
 fn workspace_active_editor(
@@ -4192,12 +4456,7 @@ impl Panel for OutlinePanel {
                             if outline_panel.should_replace_active_item(active_item.as_ref()) {
                                 outline_panel.replace_active_editor(active_item, active_editor, cx);
                             } else {
-                                outline_panel.update_fs_entries(
-                                    &active_editor,
-                                    HashSet::default(),
-                                    None,
-                                    cx,
-                                )
+                                outline_panel.update_fs_entries(active_editor, None, cx)
                             }
                         } else if !outline_panel.pinned {
                             outline_panel.clear_previous(cx);
@@ -4350,12 +4609,10 @@ fn subscribe_for_editor_events(
                 cx.notify();
             }
             EditorEvent::ExcerptsAdded { excerpts, .. } => {
-                outline_panel.update_fs_entries(
-                    &editor,
-                    excerpts.iter().map(|&(excerpt_id, _)| excerpt_id).collect(),
-                    debounce,
-                    cx,
-                );
+                outline_panel
+                    .new_entries_for_fs_update
+                    .extend(excerpts.iter().map(|&(excerpt_id, _)| excerpt_id));
+                outline_panel.update_fs_entries(editor, debounce, cx);
             }
             EditorEvent::ExcerptsRemoved { ids } => {
                 let mut ids = ids.iter().collect::<HashSet<_>>();
@@ -4365,7 +4622,7 @@ fn subscribe_for_editor_events(
                         break;
                     }
                 }
-                outline_panel.update_fs_entries(&editor, HashSet::default(), debounce, cx);
+                outline_panel.update_fs_entries(editor, debounce, cx);
             }
             EditorEvent::ExcerptsExpanded { ids } => {
                 outline_panel.invalidate_outlines(ids);
@@ -4375,6 +4632,73 @@ fn subscribe_for_editor_events(
                 outline_panel.invalidate_outlines(ids);
                 outline_panel.update_non_fs_items(cx);
             }
+            EditorEvent::BufferFoldToggled { ids, .. } => {
+                outline_panel.invalidate_outlines(ids);
+                let mut latest_unfolded_buffer_id = None;
+                let mut latest_folded_buffer_id = None;
+                let mut ignore_selections_change = false;
+                outline_panel.new_entries_for_fs_update.extend(
+                    ids.iter()
+                        .filter(|id| {
+                            outline_panel
+                                .excerpts
+                                .iter()
+                                .find_map(|(buffer_id, excerpts)| {
+                                    if excerpts.contains_key(id) {
+                                        ignore_selections_change |= outline_panel
+                                            .preserve_selection_on_buffer_fold_toggles
+                                            .remove(buffer_id);
+                                        Some(buffer_id)
+                                    } else {
+                                        None
+                                    }
+                                })
+                                .map(|buffer_id| {
+                                    if editor.read(cx).buffer_folded(*buffer_id, cx) {
+                                        latest_folded_buffer_id = Some(*buffer_id);
+                                        false
+                                    } else {
+                                        latest_unfolded_buffer_id = Some(*buffer_id);
+                                        true
+                                    }
+                                })
+                                .unwrap_or(true)
+                        })
+                        .copied(),
+                );
+                if !ignore_selections_change {
+                    if let Some(entry_to_select) = latest_unfolded_buffer_id
+                        .or(latest_folded_buffer_id)
+                        .and_then(|toggled_buffer_id| {
+                            outline_panel
+                                .fs_entries
+                                .iter()
+                                .find_map(|fs_entry| match fs_entry {
+                                    FsEntry::ExternalFile(buffer_id, _) => {
+                                        if *buffer_id == toggled_buffer_id {
+                                            Some(fs_entry.clone())
+                                        } else {
+                                            None
+                                        }
+                                    }
+                                    FsEntry::File(_, _, buffer_id, _) => {
+                                        if *buffer_id == toggled_buffer_id {
+                                            Some(fs_entry.clone())
+                                        } else {
+                                            None
+                                        }
+                                    }
+                                    FsEntry::Directory(..) => None,
+                                })
+                        })
+                        .map(PanelEntry::Fs)
+                    {
+                        outline_panel.select_entry(entry_to_select, true, cx);
+                    }
+                }
+
+                outline_panel.update_fs_entries(editor, debounce, cx);
+            }
             EditorEvent::Reparsed(buffer_id) => {
                 if let Some(excerpts) = outline_panel.excerpts.get_mut(buffer_id) {
                     for (_, excerpt) in excerpts {
@@ -4531,6 +4855,8 @@ mod tests {
         outline_panel.update(cx, |outline_panel, cx| {
             outline_panel.collapse_selected_entry(&CollapseSelectedEntry, cx);
         });
+        cx.executor()
+            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
         cx.run_until_parked();
         outline_panel.update(cx, |outline_panel, cx| {
             assert_eq!(
@@ -4563,6 +4889,8 @@ mod tests {
         outline_panel.update(cx, |outline_panel, cx| {
             outline_panel.expand_all_entries(&ExpandAllEntries, cx);
         });
+        cx.executor()
+            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
         cx.run_until_parked();
         outline_panel.update(cx, |outline_panel, cx| {
             outline_panel.select_parent(&SelectParent, cx);
@@ -4591,6 +4919,8 @@ mod tests {
         outline_panel.update(cx, |outline_panel, cx| {
             outline_panel.collapse_selected_entry(&CollapseSelectedEntry, cx);
         });
+        cx.executor()
+            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
         cx.run_until_parked();
         outline_panel.update(cx, |outline_panel, cx| {
             assert_eq!(
@@ -4615,6 +4945,8 @@ mod tests {
         outline_panel.update(cx, |outline_panel, cx| {
             outline_panel.expand_selected_entry(&ExpandSelectedEntry, cx);
         });
+        cx.executor()
+            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
         cx.run_until_parked();
         outline_panel.update(cx, |outline_panel, cx| {
             assert_eq!(
@@ -5053,6 +5385,8 @@ mod tests {
             }
             outline_panel.collapse_selected_entry(&CollapseSelectedEntry, cx);
         });
+        cx.executor()
+            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
         cx.run_until_parked();
         outline_panel.update(cx, |outline_panel, cx| {
             assert_eq!(
@@ -5072,6 +5406,91 @@ mod tests {
         search: static"#
             );
         });
+
+        outline_panel.update(cx, |outline_panel, cx| {
+            // Move to the next visible non-FS entry
+            for _ in 0..3 {
+                outline_panel.select_next(&SelectNext, cx);
+            }
+        });
+        cx.run_until_parked();
+        outline_panel.update(cx, |outline_panel, cx| {
+            assert_eq!(
+                display_entries(
+                    &snapshot(&outline_panel, cx),
+                    &outline_panel.cached_entries,
+                    outline_panel.selected_entry()
+                ),
+                r#"/
+  public/lottie/
+    syntax-tree.json
+      search: { "something": "static" }
+  src/
+    app/(site)/
+    components/
+      ErrorBoundary.tsx
+        search: static  <==== selected"#
+            );
+        });
+
+        outline_panel.update(cx, |outline_panel, cx| {
+            outline_panel
+                .active_editor()
+                .expect("Should have an active editor")
+                .update(cx, |editor, cx| {
+                    editor.toggle_fold(&editor::actions::ToggleFold, cx)
+                });
+        });
+        cx.executor()
+            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
+        cx.run_until_parked();
+        outline_panel.update(cx, |outline_panel, cx| {
+            assert_eq!(
+                display_entries(
+                    &snapshot(&outline_panel, cx),
+                    &outline_panel.cached_entries,
+                    outline_panel.selected_entry()
+                ),
+                r#"/
+  public/lottie/
+    syntax-tree.json
+      search: { "something": "static" }
+  src/
+    app/(site)/
+    components/
+      ErrorBoundary.tsx  <==== selected"#
+            );
+        });
+
+        outline_panel.update(cx, |outline_panel, cx| {
+            outline_panel
+                .active_editor()
+                .expect("Should have an active editor")
+                .update(cx, |editor, cx| {
+                    editor.toggle_fold(&editor::actions::ToggleFold, cx)
+                });
+        });
+        cx.executor()
+            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
+        cx.run_until_parked();
+        outline_panel.update(cx, |outline_panel, cx| {
+            assert_eq!(
+                display_entries(
+                    &snapshot(&outline_panel, cx),
+                    &outline_panel.cached_entries,
+                    outline_panel.selected_entry()
+                ),
+                r#"/
+  public/lottie/
+    syntax-tree.json
+      search: { "something": "static" }
+  src/
+    app/(site)/
+    components/
+      ErrorBoundary.tsx  <==== selected
+        search: static"#
+            );
+        });
     }
 
     async fn add_outline_panel(