From e2097a4cc23553e313268a4f6079be0813b47092 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 20 Feb 2026 10:49:31 +0100 Subject: [PATCH] Fix stale folded buffers in split diff view (#49699) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a buffer is folded in the split diff view and then removed from the multibuffer (via `remove_excerpts_for_path`), the block map's `folded_buffers` set retains a stale entry for that buffer ID. Later, when the split view syncs folded state from the RHS display map to the LHS, it tries to look up the companion mapping for this stale buffer and fails because the companion no longer tracks a buffer that's been removed from the multibuffer. This originally caused a panic via `.expect()`. This PR: 1. Converts the panic into graceful handling by skipping unmapped buffers. 2. Actively removes stale entries from `folded_buffers` so they don't persist as ghost state — ensuring that if the same buffer is later re-added, it won't incorrectly appear as folded. 3. Adds a regression test covering the full lifecycle: fold → remove → split → re-add → assert both LHS and RHS report the buffer as not folded. Release Notes: - Fixed a crash in split diff view when a folded buffer was removed and re-added. --- crates/editor/src/display_map.rs | 21 +++++-- crates/editor/src/split.rs | 102 +++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 5 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index a55650b51d9f70df5fa3e144b7271af256842802..2131f5c5ccbe6ea73159fdd47fc28d9750d234de 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -546,17 +546,28 @@ impl DisplayMap { let all_blocks: Vec<_> = self.block_map.blocks_raw().map(Clone::clone).collect(); companion_display_map.update(cx, |companion_display_map, cx| { + // Sync folded buffers from RHS to LHS. Also clean up stale + // entries: the block map doesn't remove buffers from + // `folded_buffers` when they leave the multibuffer, so we + // unfold any RHS buffers whose companion mapping is missing. + let mut buffers_to_unfold = Vec::new(); for my_buffer in self.folded_buffers() { - let their_buffer = companion - .read(cx) - .rhs_buffer_to_lhs_buffer - .get(my_buffer) - .unwrap(); + let their_buffer = companion.read(cx).rhs_buffer_to_lhs_buffer.get(my_buffer); + + let Some(their_buffer) = their_buffer else { + buffers_to_unfold.push(*my_buffer); + continue; + }; + companion_display_map .block_map .folded_buffers .insert(*their_buffer); } + for buffer_id in buffers_to_unfold { + self.block_map.folded_buffers.remove(&buffer_id); + } + for block in all_blocks { let Some(their_block) = block_map::balancing_block( &block.properties(), diff --git a/crates/editor/src/split.rs b/crates/editor/src/split.rs index 22fab5d468fd25b1f1171db2cadc8194f0a2878c..7950a39c0a6d5db83e41ded7bf9c12cf0a3b5066 100644 --- a/crates/editor/src/split.rs +++ b/crates/editor/src/split.rs @@ -5756,4 +5756,106 @@ mod tests { &mut cx, ); } + + #[gpui::test] + async fn test_split_after_removing_folded_buffer(cx: &mut gpui::TestAppContext) { + use rope::Point; + use unindent::Unindent as _; + + let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Unified).await; + + let base_text_a = " + aaa + bbb + ccc + " + .unindent(); + let current_text_a = " + aaa + bbb modified + ccc + " + .unindent(); + + let base_text_b = " + xxx + yyy + zzz + " + .unindent(); + let current_text_b = " + xxx + yyy modified + zzz + " + .unindent(); + + let (buffer_a, diff_a) = buffer_with_diff(&base_text_a, ¤t_text_a, &mut cx); + let (buffer_b, diff_b) = buffer_with_diff(&base_text_b, ¤t_text_b, &mut cx); + + let path_a = cx.read(|cx| PathKey::for_buffer(&buffer_a, cx)); + let path_b = cx.read(|cx| PathKey::for_buffer(&buffer_b, cx)); + + editor.update(cx, |editor, cx| { + editor.set_excerpts_for_path( + path_a.clone(), + buffer_a.clone(), + vec![Point::new(0, 0)..buffer_a.read(cx).max_point()], + 0, + diff_a.clone(), + cx, + ); + editor.set_excerpts_for_path( + path_b.clone(), + buffer_b.clone(), + vec![Point::new(0, 0)..buffer_b.read(cx).max_point()], + 0, + diff_b.clone(), + cx, + ); + }); + + cx.run_until_parked(); + + let buffer_a_id = buffer_a.read_with(cx, |buffer, _| buffer.remote_id()); + editor.update(cx, |editor, cx| { + editor.rhs_editor().update(cx, |right_editor, cx| { + right_editor.fold_buffer(buffer_a_id, cx) + }); + }); + + cx.run_until_parked(); + + editor.update(cx, |editor, cx| { + editor.remove_excerpts_for_path(path_a.clone(), cx); + }); + cx.run_until_parked(); + + editor.update_in(cx, |editor, window, cx| editor.split(window, cx)); + cx.run_until_parked(); + + editor.update(cx, |editor, cx| { + editor.set_excerpts_for_path( + path_a.clone(), + buffer_a.clone(), + vec![Point::new(0, 0)..buffer_a.read(cx).max_point()], + 0, + diff_a.clone(), + cx, + ); + assert!( + !editor + .lhs_editor() + .unwrap() + .read(cx) + .is_buffer_folded(buffer_a_id, cx) + ); + assert!( + !editor + .rhs_editor() + .read(cx) + .is_buffer_folded(buffer_a_id, cx) + ); + }); + } }