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
This commit is contained in:
Conrad Irwin 2025-03-13 22:50:42 -06:00 committed by GitHub
parent bfe4c40f73
commit a4a9f6bd07
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -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<_>>();