From 11bccbcdbf4ecb2304c4f18e21c4de74ea713ab0 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 119699da7fac73121dc0f637005ee5866dcd566d..e17c63c98037b28b38b11a8721d1673d2f2f9398 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) - .expect("unmapped folded buffer"); + 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 633cd8b26f715bc7c2d1d1256beb4a28d36b2de6..7922944cb176559c901c831b47b287fbf83f37d9 100644 --- a/crates/editor/src/split.rs +++ b/crates/editor/src/split.rs @@ -5763,4 +5763,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) + ); + }); + } }