Unfold buffers in multibuffers when editing them (#25677)

João Marcos and Max Brunsfeld created

Release Notes:

- Multibuffers: Unfold excerpts when editing their contents.

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>

Change summary

crates/editor/src/display_map.rs           | 16 +++++-
crates/editor/src/display_map/block_map.rs | 53 ++++++++++++++++-------
crates/editor/src/editor.rs                | 19 ++++++--
crates/editor/src/editor_tests.rs          | 32 +++++++++----
crates/multi_buffer/src/multi_buffer.rs    | 13 ++++-
5 files changed, 94 insertions(+), 39 deletions(-)

Detailed changes

crates/editor/src/display_map.rs 🔗

@@ -330,7 +330,11 @@ impl DisplayMap {
         block_map.remove_intersecting_replace_blocks(offset_ranges, inclusive);
     }
 
-    pub fn fold_buffer(&mut self, buffer_id: language::BufferId, cx: &mut Context<Self>) {
+    pub fn fold_buffers(
+        &mut self,
+        buffer_ids: impl IntoIterator<Item = language::BufferId>,
+        cx: &mut Context<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);
@@ -341,10 +345,14 @@ impl DisplayMap {
             .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)
+        block_map.fold_buffers(buffer_ids, self.buffer.read(cx), cx)
     }
 
-    pub fn unfold_buffer(&mut self, buffer_id: language::BufferId, cx: &mut Context<Self>) {
+    pub fn unfold_buffers(
+        &mut self,
+        buffer_ids: impl IntoIterator<Item = language::BufferId>,
+        cx: &mut Context<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);
@@ -355,7 +363,7 @@ impl DisplayMap {
             .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)
+        block_map.unfold_buffers(buffer_ids, self.buffer.read(cx), cx)
     }
 
     pub(crate) fn is_buffer_folded(&self, buffer_id: language::BufferId) -> bool {

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

@@ -1239,26 +1239,45 @@ impl<'a> BlockMapWriter<'a> {
         self.remove(blocks_to_remove);
     }
 
-    pub fn fold_buffer(&mut self, buffer_id: BufferId, multi_buffer: &MultiBuffer, cx: &App) {
-        self.0.folded_buffers.insert(buffer_id);
-        self.recompute_blocks_for_buffer(buffer_id, multi_buffer, cx);
+    pub fn fold_buffers(
+        &mut self,
+        buffer_ids: impl IntoIterator<Item = BufferId>,
+        multi_buffer: &MultiBuffer,
+        cx: &App,
+    ) {
+        self.fold_or_unfold_buffers(true, buffer_ids, multi_buffer, cx);
     }
 
-    pub fn unfold_buffer(&mut self, buffer_id: BufferId, multi_buffer: &MultiBuffer, cx: &App) {
-        self.0.folded_buffers.remove(&buffer_id);
-        self.recompute_blocks_for_buffer(buffer_id, multi_buffer, cx);
+    pub fn unfold_buffers(
+        &mut self,
+        buffer_ids: impl IntoIterator<Item = BufferId>,
+        multi_buffer: &MultiBuffer,
+        cx: &App,
+    ) {
+        self.fold_or_unfold_buffers(false, buffer_ids, multi_buffer, cx);
     }
 
-    fn recompute_blocks_for_buffer(
+    fn fold_or_unfold_buffers(
         &mut self,
-        buffer_id: BufferId,
+        fold: bool,
+        buffer_ids: impl IntoIterator<Item = BufferId>,
         multi_buffer: &MultiBuffer,
         cx: &App,
     ) {
-        let wrap_snapshot = self.0.wrap_snapshot.borrow().clone();
+        let mut ranges = Vec::new();
+        for buffer_id in buffer_ids {
+            if fold {
+                self.0.folded_buffers.insert(buffer_id);
+            } else {
+                self.0.folded_buffers.remove(&buffer_id);
+            }
+            ranges.extend(multi_buffer.excerpt_ranges_for_buffer(buffer_id, cx));
+        }
+        ranges.sort_unstable_by_key(|range| range.start);
 
         let mut edits = Patch::default();
-        for range in multi_buffer.excerpt_ranges_for_buffer(buffer_id, cx) {
+        let wrap_snapshot = self.0.wrap_snapshot.borrow().clone();
+        for range in ranges {
             let last_edit_row = cmp::min(
                 wrap_snapshot.make_wrap_point(range.end, Bias::Right).row() + 1,
                 wrap_snapshot.max_point().row(),
@@ -2730,7 +2749,7 @@ mod tests {
 
         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);
+            writer.fold_buffers([buffer_id_1], buffer, cx);
         });
         let excerpt_blocks_1 = writer.insert(vec![BlockProperties {
             style: BlockStyle::Fixed,
@@ -2805,7 +2824,7 @@ mod tests {
 
         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);
+            writer.fold_buffers([buffer_id_2], buffer, cx);
         });
         let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default());
         let blocks = blocks_snapshot
@@ -2861,7 +2880,7 @@ mod tests {
 
         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);
+            writer.unfold_buffers([buffer_id_1], buffer, cx);
         });
         let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default());
         let blocks = blocks_snapshot
@@ -2922,7 +2941,7 @@ mod tests {
 
         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);
+            writer.fold_buffers([buffer_id_3], buffer, cx);
         });
         let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default());
         let blocks = blocks_snapshot
@@ -3000,7 +3019,7 @@ mod tests {
 
         let mut writer = block_map.write(wrap_snapshot.clone(), Patch::default());
         buffer.read_with(cx, |buffer, cx| {
-            writer.fold_buffer(buffer_id, buffer, cx);
+            writer.fold_buffers([buffer_id], buffer, cx);
         });
         let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default());
         let blocks = blocks_snapshot
@@ -3250,7 +3269,7 @@ mod tests {
                             );
                             folded_count += 1;
                             unfolded_count -= 1;
-                            block_map.fold_buffer(buffer_to_fold, buffer, cx);
+                            block_map.fold_buffers([buffer_to_fold], buffer, cx);
                         }
                         if unfold {
                             let buffer_to_unfold =
@@ -3258,7 +3277,7 @@ mod tests {
                             log::info!("Unfolding {buffer_to_unfold:?}");
                             unfolded_count += 1;
                             folded_count -= 1;
-                            block_map.unfold_buffer(buffer_to_unfold, buffer, cx);
+                            block_map.unfold_buffers([buffer_to_unfold], buffer, cx);
                         }
                         log::info!(
                             "Unfolded buffers: {unfolded_count}, folded buffers: {folded_count}"

crates/editor/src/editor.rs 🔗

@@ -13314,8 +13314,9 @@ impl Editor {
             return;
         }
         let folded_excerpts = self.buffer().read(cx).excerpts_for_buffer(buffer_id, cx);
-        self.display_map
-            .update(cx, |display_map, cx| display_map.fold_buffer(buffer_id, cx));
+        self.display_map.update(cx, |display_map, cx| {
+            display_map.fold_buffers([buffer_id], cx)
+        });
         cx.emit(EditorEvent::BufferFoldToggled {
             ids: folded_excerpts.iter().map(|&(id, _)| id).collect(),
             folded: true,
@@ -13329,7 +13330,7 @@ impl Editor {
         }
         let unfolded_excerpts = self.buffer().read(cx).excerpts_for_buffer(buffer_id, cx);
         self.display_map.update(cx, |display_map, cx| {
-            display_map.unfold_buffer(buffer_id, cx);
+            display_map.unfold_buffers([buffer_id], cx);
         });
         cx.emit(EditorEvent::BufferFoldToggled {
             ids: unfolded_excerpts.iter().map(|&(id, _)| id).collect(),
@@ -15212,8 +15213,16 @@ impl Editor {
                     .retain(|buffer_id, _| buffer.buffer(*buffer_id).is_some());
                 cx.emit(EditorEvent::ExcerptsRemoved { ids: ids.clone() })
             }
-            multi_buffer::Event::ExcerptsEdited { ids } => {
-                cx.emit(EditorEvent::ExcerptsEdited { ids: ids.clone() })
+            multi_buffer::Event::ExcerptsEdited {
+                excerpt_ids,
+                buffer_ids,
+            } => {
+                self.display_map.update(cx, |map, cx| {
+                    map.unfold_buffers(buffer_ids.iter().copied(), cx)
+                });
+                cx.emit(EditorEvent::ExcerptsEdited {
+                    ids: excerpt_ids.clone(),
+                })
             }
             multi_buffer::Event::ExcerptsExpanded { ids } => {
                 self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);

crates/editor/src/editor_tests.rs 🔗

@@ -15644,7 +15644,7 @@ async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
 }
 
 #[gpui::test]
-async fn test_multi_buffer_folding(cx: &mut TestAppContext) {
+async fn test_folding_buffers(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
     let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
@@ -15751,7 +15751,7 @@ async fn test_multi_buffer_folding(cx: &mut TestAppContext) {
     let multi_buffer_editor = cx.new_window_entity(|window, cx| {
         Editor::new(
             EditorMode::Full,
-            multi_buffer,
+            multi_buffer.clone(),
             Some(project.clone()),
             true,
             window,
@@ -15759,10 +15759,9 @@ async fn test_multi_buffer_folding(cx: &mut TestAppContext) {
         )
     });
 
-    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,
+        "\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",
     );
 
     multi_buffer_editor.update(cx, |editor, cx| {
@@ -15808,12 +15807,25 @@ async fn test_multi_buffer_folding(cx: &mut TestAppContext) {
         "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)
+    // Typing inside of buffer 1 causes that buffer to be unfolded.
+    multi_buffer_editor.update_in(cx, |editor, window, cx| {
+        assert_eq!(
+            multi_buffer
+                .read(cx)
+                .snapshot(cx)
+                .text_for_range(Point::new(1, 0)..Point::new(1, 4))
+                .collect::<String>(),
+            "bbbb"
+        );
+        editor.change_selections(None, window, cx, |selections| {
+            selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
+        });
+        editor.handle_input("B", window, 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",
+        "\n\n\nB\n\n\n\n\n\n\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"
     );
 
@@ -15822,13 +15834,13 @@ async fn test_multi_buffer_folding(cx: &mut TestAppContext) {
     });
     assert_eq!(
         multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
-        full_text,
+        "\n\n\nB\n\n\n\n\n\n\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 unfolding the all buffers, all original text should be displayed"
     );
 }
 
 #[gpui::test]
-async fn test_multi_buffer_single_excerpts_folding(cx: &mut TestAppContext) {
+async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
     let sample_text_1 = "1111\n2222\n3333".to_string();
@@ -15977,7 +15989,7 @@ async fn test_multi_buffer_single_excerpts_folding(cx: &mut TestAppContext) {
 }
 
 #[gpui::test]
-async fn test_multi_buffer_with_single_excerpt_folding(cx: &mut TestAppContext) {
+async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
     let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();

crates/multi_buffer/src/multi_buffer.rs 🔗

@@ -98,7 +98,8 @@ pub enum Event {
         ids: Vec<ExcerptId>,
     },
     ExcerptsEdited {
-        ids: Vec<ExcerptId>,
+        excerpt_ids: Vec<ExcerptId>,
+        buffer_ids: Vec<BufferId>,
     },
     DiffHunksToggled,
     Edited {
@@ -767,7 +768,9 @@ impl MultiBuffer {
                 this.convert_edits_to_buffer_edits(edits, &snapshot, &original_start_columns);
             drop(snapshot);
 
+            let mut buffer_ids = Vec::new();
             for (buffer_id, mut edits) in buffer_edits {
+                buffer_ids.push(buffer_id);
                 edits.sort_by_key(|edit| edit.range.start);
                 this.buffers.borrow()[&buffer_id]
                     .buffer
@@ -844,7 +847,8 @@ impl MultiBuffer {
             }
 
             cx.emit(Event::ExcerptsEdited {
-                ids: edited_excerpt_ids,
+                excerpt_ids: edited_excerpt_ids,
+                buffer_ids,
             });
         }
     }
@@ -1004,7 +1008,9 @@ impl MultiBuffer {
                 this.convert_edits_to_buffer_edits(edits, &snapshot, &[]);
             drop(snapshot);
 
+            let mut buffer_ids = Vec::new();
             for (buffer_id, mut edits) in buffer_edits {
+                buffer_ids.push(buffer_id);
                 edits.sort_unstable_by_key(|edit| edit.range.start);
 
                 let mut ranges: Vec<Range<usize>> = Vec::new();
@@ -1026,7 +1032,8 @@ impl MultiBuffer {
             }
 
             cx.emit(Event::ExcerptsEdited {
-                ids: edited_excerpt_ids,
+                excerpt_ids: edited_excerpt_ids,
+                buffer_ids,
             });
         }
     }