From a4a9f6bd079746c5112948cbfdf9e5ce98c0ed29 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 13 Mar 2025 22:50:42 -0600 Subject: [PATCH] Merge excerpts in project diff (#26739) 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 --- crates/multi_buffer/src/multi_buffer.rs | 139 +++++++++++++++++++++--- 1 file changed, 126 insertions(+), 13 deletions(-) diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 1967008ac2a2a563ee8b65ee362eacdabb7e74b2..a6158a57ebfb8c686411de183dabae43dd9805b3 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -68,7 +68,8 @@ pub struct MultiBuffer { /// Contains the state of the buffers being edited buffers: RefCell>, // only used by consumers using `set_excerpts_for_buffer` - buffers_by_path: BTreeMap>, + excerpts_by_path: BTreeMap>, + paths_by_excerpt: HashMap, diffs: HashMap, // 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 { - 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 { - self.buffers_by_path.keys() + self.excerpts_by_path.keys() + } + + fn expand_excerpts_with_paths( + &mut self, + ids: impl IntoIterator, + line_count: u32, + direction: ExpandExcerptDirection, + cx: &mut Context, + ) { + 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::>()))) + .collect::>(); + 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> = 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_snapshot: &BufferSnapshot, + new: Vec>, + cx: &mut Context, + ) -> 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 + '_ { - 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) { - 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::>();