Mitigate panic in merge conflict resolution (#29678)

Cole Miller created

We have a report of a panic when indexing into
`BufferConflicts.block_ids` using the `old_range` from the
`ConflictsUpdated` event, indicating that the `block_ids` array can get
out of sync with the underlying `ConflictSet`. This PR adds a mitigation
so that we won't panic in this situation, as a stopgap until the bug can
be reproduced in a test and fixed at the root.

Release Notes:

- N/A

Change summary

crates/git_ui/src/conflict_view.rs | 50 ++++++++++++++++++++++++++++---
1 file changed, 45 insertions(+), 5 deletions(-)

Detailed changes

crates/git_ui/src/conflict_view.rs 🔗

@@ -14,6 +14,7 @@ use ui::{
     ActiveTheme, AnyElement, Element as _, StatefulInteractiveElement, Styled,
     StyledTypography as _, div, h_flex, rems,
 };
+use util::{debug_panic, maybe};
 
 pub(crate) struct ConflictAddon {
     buffers: HashMap<BufferId, BufferConflicts>,
@@ -89,12 +90,22 @@ fn excerpt_for_buffer_updated(
     cx: &mut Context<Editor>,
 ) {
     let conflicts_len = conflict_set.read(cx).snapshot().conflicts.len();
+    let buffer_id = conflict_set.read(cx).snapshot().buffer_id;
+    let Some(buffer_conflicts) = editor
+        .addon_mut::<ConflictAddon>()
+        .unwrap()
+        .buffers
+        .get(&buffer_id)
+    else {
+        return;
+    };
+    let addon_conflicts_len = buffer_conflicts.block_ids.len();
     conflicts_updated(
         editor,
         conflict_set,
         &ConflictSetUpdate {
             buffer_range: None,
-            old_range: 0..conflicts_len,
+            old_range: 0..addon_conflicts_len,
             new_range: 0..conflicts_len,
         },
         cx,
@@ -174,10 +185,37 @@ fn conflicts_updated(
         return;
     };
 
+    let old_range = maybe!({
+        let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
+        let buffer_conflicts = conflict_addon.buffers.get(&buffer_id)?;
+        match buffer_conflicts.block_ids.get(event.old_range.clone()) {
+            Some(_) => Some(event.old_range.clone()),
+            None => {
+                debug_panic!(
+                    "conflicts updated event old range is invalid for buffer conflicts view (block_ids len is {:?}, old_range is {:?})",
+                    buffer_conflicts.block_ids.len(),
+                    event.old_range,
+                );
+                if event.old_range.start <= event.old_range.end {
+                    Some(
+                        event.old_range.start.min(buffer_conflicts.block_ids.len())
+                            ..event.old_range.end.min(buffer_conflicts.block_ids.len()),
+                    )
+                } else {
+                    None
+                }
+            }
+        }
+    });
+
     // Remove obsolete highlights and blocks
     let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
-    if let Some(buffer_conflicts) = conflict_addon.buffers.get_mut(&buffer_id) {
-        let old_conflicts = buffer_conflicts.block_ids[event.old_range.clone()].to_owned();
+    if let Some((buffer_conflicts, old_range)) = conflict_addon
+        .buffers
+        .get_mut(&buffer_id)
+        .zip(old_range.clone())
+    {
+        let old_conflicts = buffer_conflicts.block_ids[old_range].to_owned();
         let mut removed_highlighted_ranges = Vec::new();
         let mut removed_block_ids = HashSet::default();
         for (conflict_range, block_id) in old_conflicts {
@@ -263,9 +301,11 @@ fn conflicts_updated(
     let new_block_ids = editor.insert_blocks(blocks, None, cx);
 
     let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
-    if let Some(buffer_conflicts) = conflict_addon.buffers.get_mut(&buffer_id) {
+    if let Some((buffer_conflicts, old_range)) =
+        conflict_addon.buffers.get_mut(&buffer_id).zip(old_range)
+    {
         buffer_conflicts.block_ids.splice(
-            event.old_range.clone(),
+            old_range,
             new_conflicts
                 .iter()
                 .map(|conflict| conflict.range.clone())