Merge excerpts in project diff (#26739)

Conrad Irwin created

This adds code to merge excerpts when you expand them and they would
overlap. It is only enabled for callers who use the
`set_excerpts_for_path` API for multibuffers (which is currently just
project diff), as other users of multibuffer care too much about the
exact excerpts that they have.

Release Notes:

- N/A

Change summary

crates/multi_buffer/src/multi_buffer.rs | 139 ++++++++++++++++++++++++--
1 file changed, 126 insertions(+), 13 deletions(-)

Detailed changes

crates/multi_buffer/src/multi_buffer.rs 🔗

@@ -68,7 +68,8 @@ pub struct MultiBuffer {
     /// Contains the state of the buffers being edited
     buffers: RefCell<HashMap<BufferId, BufferState>>,
     // only used by consumers using `set_excerpts_for_buffer`
-    buffers_by_path: BTreeMap<PathKey, Vec<ExcerptId>>,
+    excerpts_by_path: BTreeMap<PathKey, Vec<ExcerptId>>,
+    paths_by_excerpt: HashMap<ExcerptId, PathKey>,
     diffs: HashMap<BufferId, DiffState>,
     // all_diff_hunks_expanded: bool,
     subscriptions: Topic,
@@ -577,7 +578,8 @@ impl MultiBuffer {
             singleton: false,
             capability,
             title: None,
-            buffers_by_path: Default::default(),
+            excerpts_by_path: Default::default(),
+            paths_by_excerpt: Default::default(),
             buffer_changed_since_sync: Default::default(),
             history: History {
                 next_transaction_id: clock::Lamport::default(),
@@ -593,7 +595,8 @@ impl MultiBuffer {
         Self {
             snapshot: Default::default(),
             buffers: Default::default(),
-            buffers_by_path: Default::default(),
+            excerpts_by_path: Default::default(),
+            paths_by_excerpt: Default::default(),
             diffs: HashMap::default(),
             subscriptions: Default::default(),
             singleton: false,
@@ -638,7 +641,8 @@ impl MultiBuffer {
         Self {
             snapshot: RefCell::new(self.snapshot.borrow().clone()),
             buffers: RefCell::new(buffers),
-            buffers_by_path: Default::default(),
+            excerpts_by_path: Default::default(),
+            paths_by_excerpt: Default::default(),
             diffs: diff_bases,
             subscriptions: Default::default(),
             singleton: self.singleton,
@@ -1478,7 +1482,7 @@ impl MultiBuffer {
     }
 
     pub fn location_for_path(&self, path: &PathKey, cx: &App) -> Option<Anchor> {
-        let excerpt_id = self.buffers_by_path.get(path)?.first()?;
+        let excerpt_id = self.excerpts_by_path.get(path)?.first()?;
         let snapshot = self.snapshot(cx);
         let excerpt = snapshot.excerpt(*excerpt_id)?;
         Some(Anchor::in_buffer(
@@ -1489,7 +1493,93 @@ impl MultiBuffer {
     }
 
     pub fn excerpt_paths(&self) -> impl Iterator<Item = &PathKey> {
-        self.buffers_by_path.keys()
+        self.excerpts_by_path.keys()
+    }
+
+    fn expand_excerpts_with_paths(
+        &mut self,
+        ids: impl IntoIterator<Item = ExcerptId>,
+        line_count: u32,
+        direction: ExpandExcerptDirection,
+        cx: &mut Context<Self>,
+    ) {
+        let grouped = ids
+            .into_iter()
+            .chunk_by(|id| self.paths_by_excerpt.get(id).cloned())
+            .into_iter()
+            .flat_map(|(k, v)| Some((k?, v.into_iter().collect::<Vec<_>>())))
+            .collect::<Vec<_>>();
+        let snapshot = self.snapshot(cx);
+
+        for (path, ids) in grouped.into_iter() {
+            let Some(excerpt_ids) = self.excerpts_by_path.get(&path) else {
+                continue;
+            };
+
+            let ids_to_expand = HashSet::from_iter(ids);
+            let expanded_ranges = excerpt_ids.iter().filter_map(|excerpt_id| {
+                let excerpt = snapshot.excerpt(*excerpt_id)?;
+
+                let mut context = excerpt.range.context.to_point(&excerpt.buffer);
+                if ids_to_expand.contains(excerpt_id) {
+                    match direction {
+                        ExpandExcerptDirection::Up => {
+                            context.start.row = context.start.row.saturating_sub(line_count);
+                            context.start.column = 0;
+                        }
+                        ExpandExcerptDirection::Down => {
+                            context.end.row =
+                                (context.end.row + line_count).min(excerpt.buffer.max_point().row);
+                            context.end.column = excerpt.buffer.line_len(context.end.row);
+                        }
+                        ExpandExcerptDirection::UpAndDown => {
+                            context.start.row = context.start.row.saturating_sub(line_count);
+                            context.start.column = 0;
+                            context.end.row =
+                                (context.end.row + line_count).min(excerpt.buffer.max_point().row);
+                            context.end.column = excerpt.buffer.line_len(context.end.row);
+                        }
+                    }
+                }
+
+                Some(ExcerptRange {
+                    context,
+                    primary: excerpt
+                        .range
+                        .primary
+                        .as_ref()
+                        .map(|range| range.to_point(&excerpt.buffer)),
+                })
+            });
+            let mut merged_ranges: Vec<ExcerptRange<Point>> = Vec::new();
+            for range in expanded_ranges {
+                if let Some(last_range) = merged_ranges.last_mut() {
+                    if last_range.context.end >= range.context.start {
+                        last_range.context.end = range.context.end;
+                        continue;
+                    }
+                }
+                merged_ranges.push(range)
+            }
+            let Some(excerpt_id) = excerpt_ids.first() else {
+                continue;
+            };
+            let Some(buffer_id) = &snapshot.buffer_id_for_excerpt(*excerpt_id) else {
+                continue;
+            };
+
+            let Some(buffer) = self
+                .buffers
+                .borrow()
+                .get(buffer_id)
+                .map(|b| b.buffer.clone())
+            else {
+                continue;
+            };
+
+            let buffer_snapshot = buffer.read(cx).snapshot();
+            self.update_path_excerpts(path.clone(), buffer, &buffer_snapshot, merged_ranges, cx);
+        }
     }
 
     /// Sets excerpts, returns `true` if at least one new excerpt was added.
@@ -1503,15 +1593,30 @@ impl MultiBuffer {
     ) -> bool {
         let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
 
+        let (new, _) = build_excerpt_ranges(&buffer_snapshot, &ranges, context_line_count);
+        self.update_path_excerpts(path, buffer, &buffer_snapshot, new, cx)
+    }
+
+    fn update_path_excerpts(
+        &mut self,
+        path: PathKey,
+        buffer: Entity<Buffer>,
+        buffer_snapshot: &BufferSnapshot,
+        new: Vec<ExcerptRange<Point>>,
+        cx: &mut Context<Self>,
+    ) -> bool {
         let mut insert_after = self
-            .buffers_by_path
+            .excerpts_by_path
             .range(..path.clone())
             .next_back()
             .map(|(_, value)| *value.last().unwrap())
             .unwrap_or(ExcerptId::min());
-        let existing = self.buffers_by_path.get(&path).cloned().unwrap_or_default();
 
-        let (new, _) = build_excerpt_ranges(&buffer_snapshot, &ranges, context_line_count);
+        let existing = self
+            .excerpts_by_path
+            .get(&path)
+            .cloned()
+            .unwrap_or_default();
 
         let mut new_iter = new.into_iter().peekable();
         let mut existing_iter = existing.into_iter().peekable();
@@ -1594,20 +1699,23 @@ impl MultiBuffer {
         ));
         self.remove_excerpts(to_remove, cx);
         if new_excerpt_ids.is_empty() {
-            self.buffers_by_path.remove(&path);
+            self.excerpts_by_path.remove(&path);
         } else {
-            self.buffers_by_path.insert(path, new_excerpt_ids);
+            for excerpt_id in &new_excerpt_ids {
+                self.paths_by_excerpt.insert(*excerpt_id, path.clone());
+            }
+            self.excerpts_by_path.insert(path, new_excerpt_ids);
         }
 
         added_a_new_excerpt
     }
 
     pub fn paths(&self) -> impl Iterator<Item = PathKey> + '_ {
-        self.buffers_by_path.keys().cloned()
+        self.excerpts_by_path.keys().cloned()
     }
 
     pub fn remove_excerpts_for_path(&mut self, path: PathKey, cx: &mut Context<Self>) {
-        if let Some(to_remove) = self.buffers_by_path.remove(&path) {
+        if let Some(to_remove) = self.excerpts_by_path.remove(&path) {
             self.remove_excerpts(to_remove, cx)
         }
     }
@@ -2079,6 +2187,7 @@ impl MultiBuffer {
         let mut removed_buffer_ids = Vec::new();
 
         while let Some(excerpt_id) = excerpt_ids.next() {
+            self.paths_by_excerpt.remove(&excerpt_id);
             // Seek to the next excerpt to remove, preserving any preceding excerpts.
             let locator = snapshot.excerpt_locator_for_id(excerpt_id);
             new_excerpts.append(cursor.slice(&Some(locator), Bias::Left, &()), &());
@@ -2643,6 +2752,10 @@ impl MultiBuffer {
             return;
         }
         self.sync(cx);
+        if !self.excerpts_by_path.is_empty() {
+            self.expand_excerpts_with_paths(ids, line_count, direction, cx);
+            return;
+        }
         let mut snapshot = self.snapshot.borrow_mut();
 
         let ids = ids.into_iter().collect::<Vec<_>>();